#Reading Analytics
Status: Available. All four methods (
timeseries,traffic-sources,funnel,visibility) work. The field sets below are checked against the implementation; differences from the dashboard screens are noted in the text.
Purpose: pull aggregated Walker Formulation Academy analytics into your own systems and reports: time series, traffic sources, the funnel, and search visibility.
#Contents
- When to use it
- Access
- Common to all methods
- Time series
- Traffic sources
- Funnel
- Visibility
- Data freshness: data_as_of
- Errors
- Examples
- Open questions
#When to use it
| You need | Method |
|---|---|
| A chart of one or more metrics by day or week | GET /analytics/timeseries |
| Where visitors come from, including AI assistants | GET /analytics/traffic-sources |
| The path from search impressions to revenue | GET /analytics/funnel |
| Visibility share, positions, AI citations | GET /analytics/visibility |
| The complete ready-made Overview and Pulse screens | Dashboards |
#Access
| Base address | https://<cell domain>/api/public/v1 (RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id) |
| Authentication | Authorization: Bearer <key> (Authentication) |
| Scope | analytics:read |
| Paid | no |
| Personal data | none: aggregates only. Visit rows, IPs, and hashes are not returned |
| Read limit | 600 requests per minute (Conventions) |
| Pagination | not used: all methods return aggregates for the whole requested period |
The data belongs to the key's tenant. Other tenants' data and platform data are not available.
#Common to all methods
- The period is set by the
fromandtoparameters: dates inYYYY-MM-DDform (inclusive). The dashboard'sdaysparameter is not used in the public API. - Windows are counted in whole days; the current incomplete day does not take part in comparison calculations.
- Money is always an object
{"amount": "string", "currency": "RUB"}; the amount is a string (Conventions). The currency is the cell's currency. - Data freshness. Every block and every series carries a
data_as_offield, an object{source: date}: the date up to which the source's data is complete (section below). In list responses (datais an array), the field sits at the top level next todata. - A lack of data is not zero. Where a source is not connected or there is no data yet, the field is marked
availability(ok,no_data,no_source,not_connected), and the value isnull. - A database read error is a
5xx, not an empty response. - New fields may appear in responses: the client ignores them.
#Time series
GET /analytics/timeseries
One or more series by day or week. The public API takes the period as from/to and the step as granularity.
#Query parameters
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
metric | string | yes | from 1 to 8 unique names, comma-separated | sessions,clicks |
from | string | yes | a YYYY-MM-DD date | 2026-09-01 |
to | string | yes | a date not earlier than from; the window is at most 365 days. A value later than the last full day (yesterday, UTC) is clamped to yesterday; the actual boundary is returned in the response to. If from is also later than yesterday: 422 | 2026-09-30 |
granularity | string | no | day (default) or week | day |
engine | string | no | google, yandex, or bing; only for search metrics (impressions, clicks, ctr, position). The value bing is available after the source is verified | google |
cluster | string | no | a query cluster identifier (UUID); a foreign or missing one gives 404 not_found | 3f1c8a52-7d0e-4b69-9a14-c2e5b86d0f37 |
#Metrics
The public list for the first version has 13 metrics. The dashboard's internal metrics (engagement cost api_cost, comments_*, yt_*) are not published.
| Metric | What it measures | Source | Unit | Rollup over the period |
|---|---|---|---|---|
sessions | sessions on the site | Yandex Metrica, Google Analytics 4 | count | sum |
pageviews | page views | Google Analytics 4 | count | sum |
visitors | unique visitors by day | platform tracker | count | sum of daily values |
bounce_rate | bounce rate | Google Analytics 4 | percent | average |
impressions | search impressions | search consoles | count | sum |
clicks | clicks from search | search consoles | count | sum |
ctr | click-through rate | search consoles | percent | ratio of the click and impression sums |
position | average position | search consoles | position | average |
ai_referrals | referrals from AI assistants | platform tracker | count | sum |
bot_hits | visits by bots and AI crawlers | the module's server beacon | count | sum |
leads | new leads | platform CRM | count | sum |
deals_won | won deals | platform CRM and offline conversions | count | sum |
revenue | revenue | platform CRM and offline conversions | money | sum |
With the engine parameter, search metrics come from the selected system: google is Search Console, yandex is Yandex Webmaster, bing is Bing Webmaster. bot_hits requires a server beacon (a module or the PHP variant of the tracker); without it, the metric gets availability: "no_source".
deals_won and revenue do not count the same deal twice: the rules for replacing a deal with a conversion and for refunds are on the Offline conversions page.
#Response (one metric)
{
"data": {
"metric": "clicks",
"availability": "ok",
"reason": null,
"from": "2026-09-01",
"to": "2026-09-30",
"granularity": "day",
"agg": "sum",
"engine": "yandex",
"series": [
{ "date": "2026-09-01", "value": 412 },
{ "date": "2026-09-02", "value": 398 }
],
"comparison": {
"current": 12340,
"prev": 11020,
"delta_pct": 11.98,
"data_through": "2026-09-28"
},
"summary": { "total": 12340, "latest": 401, "non_zero_days": 30 },
"cluster_applied": false,
"data_as_of": { "yandex_webmaster": "2026-09-28" }
}
}
| Field | Type | Description |
|---|---|---|
availability | string | ok, no_data, or no_source; each series has its own |
reason | string | null | a reason code when availability is not ok |
series | array | the series of values; days with no data are filled with zeros |
agg | string | the rollup method: sum, avg |
comparison.current / prev | number | the value for the period and for the equivalent previous one |
comparison.delta_pct | number | null | the change in percent; null if the base is zero |
comparison.data_through | string | the last date through which the data is complete |
summary | object | null | total, latest, non_zero_days; null if there is no series |
engine | string | null | the series search engine; null for metrics not tied to search |
cluster_applied | boolean | true if the cluster filter was applied; always false for metrics not tied to queries |
data_as_of | object | the date up to which this series' source data is complete |
With several metrics, {"data": {"items": [ … ]}} is returned in the order of the request; each item has its own data_as_of and availability.
For the revenue metric, the values series[].value, comparison.current, comparison.prev, summary.total, and summary.latest are money objects:
{ "date": "2026-09-01", "value": { "amount": "1240.00", "currency": "GBP" } }
With granularity=week, a series point describes a week; date is the first day of the week (Monday, UTC). The first and last weeks of the period may be partial.
For comparison, the platform reads 2 × period + 10 days, to align sources with different delays.
#Traffic sources
GET /analytics/traffic-sources
A list of first-visit sources for the period: sessions, unique visitors, and leads for each source.
#Query parameters
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
from | string | yes | YYYY-MM-DD | 2026-10-01 |
to | string | yes | not earlier than from | 2026-10-07 |
include | string | no | series: add a by-day series to each item | series |
The dashboard screen accepts the period only as a number of days, 7, 14, 28, or 90; the public method accepts any from and to.
#Response
{
"data": [
{ "source": "google", "kind": "search", "sessions": 820, "visitors": 640, "leads": 9 },
{ "source": "yandex", "kind": "search", "sessions": 460, "visitors": 390, "leads": 5 },
{ "source": "chatgpt", "kind": "ai_assistant", "sessions": 190, "visitors": 150, "leads": 4 },
{ "source": "social", "kind": "social", "sessions": 96, "visitors": 88, "leads": 1 },
{ "source": "direct", "kind": "direct", "sessions": 410, "visitors": 330, "leads": 2 }
],
"data_as_of": { "tracker": "2026-10-07" }
}
With include=series, each item gets a series field with by-day values:
{
"source": "google", "kind": "search", "sessions": 820, "visitors": 640, "leads": 9,
"series": [
{ "date": "2026-10-01", "sessions": 120, "visitors": 95, "leads": 1 },
{ "date": "2026-10-02", "sessions": 131, "visitors": 101, "leads": 2 }
]
}
| Field | Type | Description |
|---|---|---|
source | string | a source from the table below |
kind | string | the source group: search, ai_assistant, social, ads, direct, other |
sessions | integer | sessions (visits) from this source for the period |
visitors | integer | null | unique visitors per day, summed over the period; null for sites in the anonymous beacon mode |
leads | integer | leads whose source was determined to be this one |
series | array | only with include=series: the same sessions, visitors, leads by day |
source | kind | What it is |
|---|---|---|
google, yandex, bing | search | organic search |
chatgpt, perplexity, ai_other | ai_assistant | referrals from AI assistants (see the labels); ai_other covers the other assistants |
social | social | social networks |
ads | ads | paid advertising |
direct | direct | direct visits |
other | other | other |
Sources with no visits for the period do not appear in the list. The same format is used in the Quickstart.
A "unique visitor" is counted from IP and User-Agent hashes over a day; for sites in the anonymous beacon mode this estimate is not available, and visitors is null.
#Funnel
GET /analytics/funnel
The path "impressions → clicks → sessions → leads → deals → revenue" for the period.
#Query parameters
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
from, to | string | yes | YYYY-MM-DD | 2026-10-01, 2026-10-28 |
engine | string | no | yandex, google, both; by default, the search engine of the cell's market. both shows the engines separately and does not add them together | google |
#Response
{
"data": {
"engine": "google",
"from": "2026-10-01",
"to": "2026-10-28",
"steps": [
{
"key": "clicks",
"availability": "ok",
"sources": ["g:search_console"],
"unit": "count",
"current": 4210,
"prev": 3880,
"delta_pct": 8.5,
"data_through": "2026-10-25"
},
{
"key": "revenue",
"availability": "ok",
"sources": ["crm"],
"unit": "money",
"current": { "amount": "5240.00", "currency": "GBP" },
"prev": { "amount": "4100.00", "currency": "GBP" },
"delta_pct": 27.8,
"data_through": "2026-10-28"
}
],
"by_channel": [
{ "channel": "search", "availability": "ok", "sources": ["g:search_console"], "clicks": 4210, "cost": null, "leads": 31 }
],
"crm": { "availability": "ok" }
},
"data_as_of": { "g:search_console": "2026-10-25", "crm": "2026-10-28" }
}
| Field | Type | Description |
|---|---|---|
steps[].key | string | impressions, clicks, sessions, leads, deals_created, deals_won, revenue |
steps[].availability | string | ok, no_data, no_source |
steps[].unit | string | count or money |
steps[].sources | array | what the step is calculated from |
steps[].current / prev | number | object | null | the value and the previous period; for unit: "money", a money object {amount, currency} |
steps[].data_through | string | the source's last complete date |
by_channel | array | the channels search, ads, social, direct_other: availability, sources, clicks, spend (cost, a money object), leads; null means "not measured by this channel" and is not 0; there is currently no way to measure the direct_other channel |
by_product | array | a breakdown by product: course_slug, name, sales_mode, leads, leads_prev, deals_won, deals_won_prev |
search_by_engine | array | only with engine=both: the engines separately |
crm | object | availability (ok or no_data) and an optional reason |
Leads and deals are counted the same way as in the strategist's reports; the deals_won and revenue steps come from the platform CRM or from offline conversions without double counting (rules). The server does not return conversion rates between steps: the client computes them from the numerator and denominator, and with a zero or unknown denominator it shows "no data", not "0%". The no_source status means the cell's market has no source at all (for example, Yandex and Direct on EN and ID), and the value is not zero. CRM data is also fed by offline conversions.
#Visibility
GET /analytics/visibility
#Query parameters
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
from, to | string | yes | YYYY-MM-DD | 2026-10-01, 2026-10-28 |
compare | boolean | no | compare with the previous period | true |
engine | string | no | yandex, google, both | google |
cluster | string | no | a query cluster identifier (UUID); a foreign or missing one gives 404 not_found | 3f1c8a52-7d0e-4b69-9a14-c2e5b86d0f37 |
#Response
Each section is self-contained and carries its own availability (availability: ok, no_data, not_connected), a reason code reason, a source code source, and data_as_of, the date up to which the data is complete. An empty section comes in three kinds: the source is not connected, there is no data for the window, or there is nothing to compute the metric from; a zero in all three cases would mean "we are not visible" where we did not ask.
{
"data": {
"share": {
"availability": "ok", "source": "g:search_console", "data_as_of": { "g:search_console": "2026-10-25" },
"data": {
"value": 14.2,
"comparison": { "current": 14.2, "prev": 12.9, "delta_pct": 10.1 },
"keywords": 312,
"volume_total": 48200
}
},
"distribution": {
"availability": "ok", "source": "g:search_console", "data_as_of": { "g:search_console": "2026-10-25" },
"data": { "top3": 18, "top10": 74, "top30": 210, "outside": 530, "total": 832, "median": 21.5, "average": 29.3 }
},
"sov": {
"availability": "ok", "source": "serp", "data_as_of": { "serp": "2026-10-25" },
"data": {
"rows": [ { "domain": "example.com", "appearances": 140, "share_pct": 22.4, "keywords": 96, "is_ours": true, "competitor_id": null, "competitor_status": null } ],
"keywords": 120,
"appearances_total": 625,
"redacted": true,
"others": { "domains": 41, "appearances": 485, "share_pct": 77.6 }
}
},
"aeo": {
"availability": "ok", "source": "aeo", "data_as_of": { "aeo": "2026-10-27" },
"data": {
"series": [ { "date": "2026-10-27", "value": 12.5, "probes": 24 } ],
"engines": [ { "engine": "perplexity", "probes": 24, "cited": 3, "rate": 12.5 } ],
"probes": 96, "cited": 11, "rate": 11.5,
"comparison": { "current": 11.5, "prev": 9.0, "delta_pct": 27.8 }
}
},
"regions": { "availability": "not_connected", "reason": "no_country_dimension", "source": "g:search_console", "data_as_of": {}, "data": { "rows": [] } }
}
}
| Section | What it shows |
|---|---|
share | visibility share: positions weighted by query volume, in percent; value: null means "nothing to compute from", not zero |
distribution | how many queries are in the top 3, top 10, top 30, and beyond them; the median and average position |
sov | share of voice: how many times a domain appeared in the top 10 for the window's keys. Without the permission for raw competitor data (competitors.raw), names are hidden (redacted: true) and the rest is rolled up into others |
aeo | the site's citation in AI assistant answers: a by-day series, a breakdown by engine (probes is the number of probes, cited the number with a citation), and the citation rate in percent |
regions | not connected: the by-country breakdown is not yet available (reason: no_country_dimension) |
The values in the example are fictitious. The section names in the public response (share, distribution, sov, aeo, regions) are taken from the internal response and may be renamed.
#Data freshness: data_as_of
Sources arrive with different delays: Search Console and Bing lag by about 2–3 days, while counters and the CRM have almost no delay. So that you do not mistake missing days for a drop, the response states the date up to which each source's data is complete.
"data_as_of": {
"tracker": "2026-10-10",
"yandex_webmaster": "2026-10-08",
"g:search_console": "2026-10-08"
}
Rules:
- The
data_as_offield, an object{source: date}, is present in every block and every series: in thedataelement (an object), in each element ofitems, and in each visibility section. For responses wheredatais an array (traffic-sources), the field sits at the top level of the response next todata. - The last days of the window for sources with a delay may be incomplete; the platform aligns comparisons by the source's
data_through. - A source with no data at all does not appear in the object;
availabilityreports the reason.
#Errors
The format is application/problem+json; the shared catalog is in the Conventions.
| HTTP | Code | When |
|---|---|---|
| 404 | not_found | cluster belongs to someone else or does not exist |
| 422 | validation_failed | an unknown metric; more than 8 metrics; a period longer than 365 days; to earlier than from; an invalid granularity; an invalid engine or include value |
| 401 | invalid_api_key | the key is wrong, revoked, or expired |
| 403 | scope_missing | the analytics:read scope is missing |
| 403 | ip_not_allowed | the request comes from an address outside the integration's IP list |
| 429 | rate_limited | the limit was exceeded |
| 5xx | n/a | a data read error; not an empty response |
#Examples
#curl
curl -sS 'https://formula-cream.pro/api/public/v1/analytics/timeseries?metric=clicks,impressions&from=2026-09-01&to=2026-09-30&engine=yandex' \
-H 'Authorization: Bearer sap_ru_live_EXAMPLE0KEYI_EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLE'
#JavaScript
const base = 'https://wfacademy.org/api/public/v1';
const url = new URL(`${base}/analytics/funnel`);
url.search = new URLSearchParams({ from: '2026-10-01', to: '2026-10-28', engine: 'google' });
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
if (!response.ok) throw new Error(`API: ${response.status}`);
const { data } = await response.json();
console.log(data.steps.map((s) => `${s.key}: ${s.current}`).join('\n'));
#Python
import os, requests
response = requests.get(
"https://wfacademy.id/api/public/v1/analytics/visibility",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
params={"from": "2026-10-01", "to": "2026-10-28", "compare": "true"},
timeout=10,
)
response.raise_for_status()
print(response.json()["data"]["share"]["data"]["value"])
#PHP
<?php
$ch = curl_init('https://formula-cream.pro/api/public/v1/analytics/traffic-sources?from=2026-10-01&to=2026-10-07');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('API_KEY')],
]);
$data = json_decode(curl_exec($ch), true);
foreach ($data['data'] as $row) {
echo $row['source'], ': ', $row['sessions'], PHP_EOL;
}
#Open questions
- Technical. Weekly granularity. The design specifies
from,to, andgranularity=day|week, while the current dashboard route acceptsdays(1–365) and does not know weeks. Weeks are added when the public service is extracted (aggregation in SQL); the day of the week indatewill be finalized. - Technical. The Bing source in series is enabled after the source is verified: the dashboard route accepts only
engine=google|yandex. - Technical. The definitions of
sessionsandvisitorsintraffic-sourcesare built from tracker hits and IP and User-Agent hashes over a day, not from counter sessions; the units ofbounce_rateandctr(percent) and the shape ofseriesintraffic-sourceswill be finalized in OpenAPI. - Technical.
summary.delta_pctin the internal response compares halves of the window, while an honest comparison with the previous period iscomparison. Onlycomparisonremains in the public response. - Technical. Limits for the heavy methods (
funnel,visibility) separate from the general read limit are not defined. - Technical. The read error code. The code catalog has no code for an internal read error, so the error tables show it as "5xx"; a code needs to be added to the catalog.
- For the owner. The by-country breakdown (
regions) is not available: a separate piece of work.