Walker Formulation Academy for developers API reference Error catalog OpenAPI
Sections

#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

  1. When to use it
  2. Access
  3. Common to all methods
  4. Time series
  5. Traffic sources
  6. Funnel
  7. Visibility
  8. Data freshness: data_as_of
  9. Errors
  10. Examples
  11. Open questions

#When to use it

You needMethod
A chart of one or more metrics by day or weekGET /analytics/timeseries
Where visitors come from, including AI assistantsGET /analytics/traffic-sources
The path from search impressions to revenueGET /analytics/funnel
Visibility share, positions, AI citationsGET /analytics/visibility
The complete ready-made Overview and Pulse screensDashboards

#Access

Base addresshttps://<cell domain>/api/public/v1 (RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id)
AuthenticationAuthorization: Bearer <key> (Authentication)
Scopeanalytics:read
Paidno
Personal datanone: aggregates only. Visit rows, IPs, and hashes are not returned
Read limit600 requests per minute (Conventions)
Paginationnot 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

#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

ParameterTypeRequiredConstraintsExample
metricstringyesfrom 1 to 8 unique names, comma-separatedsessions,clicks
fromstringyesa YYYY-MM-DD date2026-09-01
tostringyesa 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: 4222026-09-30
granularitystringnoday (default) or weekday
enginestringnogoogle, yandex, or bing; only for search metrics (impressions, clicks, ctr, position). The value bing is available after the source is verifiedgoogle
clusterstringnoa query cluster identifier (UUID); a foreign or missing one gives 404 not_found3f1c8a52-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.

MetricWhat it measuresSourceUnitRollup over the period
sessionssessions on the siteYandex Metrica, Google Analytics 4countsum
pageviewspage viewsGoogle Analytics 4countsum
visitorsunique visitors by dayplatform trackercountsum of daily values
bounce_ratebounce rateGoogle Analytics 4percentaverage
impressionssearch impressionssearch consolescountsum
clicksclicks from searchsearch consolescountsum
ctrclick-through ratesearch consolespercentratio of the click and impression sums
positionaverage positionsearch consolespositionaverage
ai_referralsreferrals from AI assistantsplatform trackercountsum
bot_hitsvisits by bots and AI crawlersthe module's server beaconcountsum
leadsnew leadsplatform CRMcountsum
deals_wonwon dealsplatform CRM and offline conversionscountsum
revenuerevenueplatform CRM and offline conversionsmoneysum

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" }
  }
}
FieldTypeDescription
availabilitystringok, no_data, or no_source; each series has its own
reasonstring | nulla reason code when availability is not ok
seriesarraythe series of values; days with no data are filled with zeros
aggstringthe rollup method: sum, avg
comparison.current / prevnumberthe value for the period and for the equivalent previous one
comparison.delta_pctnumber | nullthe change in percent; null if the base is zero
comparison.data_throughstringthe last date through which the data is complete
summaryobject | nulltotal, latest, non_zero_days; null if there is no series
enginestring | nullthe series search engine; null for metrics not tied to search
cluster_appliedbooleantrue if the cluster filter was applied; always false for metrics not tied to queries
data_as_ofobjectthe 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

ParameterTypeRequiredConstraintsExample
fromstringyesYYYY-MM-DD2026-10-01
tostringyesnot earlier than from2026-10-07
includestringnoseries: add a by-day series to each itemseries

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 }
  ]
}
FieldTypeDescription
sourcestringa source from the table below
kindstringthe source group: search, ai_assistant, social, ads, direct, other
sessionsintegersessions (visits) from this source for the period
visitorsinteger | nullunique visitors per day, summed over the period; null for sites in the anonymous beacon mode
leadsintegerleads whose source was determined to be this one
seriesarrayonly with include=series: the same sessions, visitors, leads by day
sourcekindWhat it is
google, yandex, bingsearchorganic search
chatgpt, perplexity, ai_otherai_assistantreferrals from AI assistants (see the labels); ai_other covers the other assistants
socialsocialsocial networks
adsadspaid advertising
directdirectdirect visits
otherotherother

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

ParameterTypeRequiredConstraintsExample
from, tostringyesYYYY-MM-DD2026-10-01, 2026-10-28
enginestringnoyandex, google, both; by default, the search engine of the cell's market. both shows the engines separately and does not add them togethergoogle

#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" }
}
FieldTypeDescription
steps[].keystringimpressions, clicks, sessions, leads, deals_created, deals_won, revenue
steps[].availabilitystringok, no_data, no_source
steps[].unitstringcount or money
steps[].sourcesarraywhat the step is calculated from
steps[].current / prevnumber | object | nullthe value and the previous period; for unit: "money", a money object {amount, currency}
steps[].data_throughstringthe source's last complete date
by_channelarraythe 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_productarraya breakdown by product: course_slug, name, sales_mode, leads, leads_prev, deals_won, deals_won_prev
search_by_enginearrayonly with engine=both: the engines separately
crmobjectavailability (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

ParameterTypeRequiredConstraintsExample
from, tostringyesYYYY-MM-DD2026-10-01, 2026-10-28
comparebooleannocompare with the previous periodtrue
enginestringnoyandex, google, bothgoogle
clusterstringnoa query cluster identifier (UUID); a foreign or missing one gives 404 not_found3f1c8a52-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": [] } }
  }
}
SectionWhat it shows
sharevisibility share: positions weighted by query volume, in percent; value: null means "nothing to compute from", not zero
distributionhow many queries are in the top 3, top 10, top 30, and beyond them; the median and average position
sovshare 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
aeothe 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
regionsnot 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:

#Errors

The format is application/problem+json; the shared catalog is in the Conventions.

HTTPCodeWhen
404not_foundcluster belongs to someone else or does not exist
422validation_failedan 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
401invalid_api_keythe key is wrong, revoked, or expired
403scope_missingthe analytics:read scope is missing
403ip_not_allowedthe request comes from an address outside the integration's IP list
429rate_limitedthe limit was exceeded
5xxn/aa 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

  1. Technical. Weekly granularity. The design specifies from, to, and granularity=day|week, while the current dashboard route accepts days (1–365) and does not know weeks. Weeks are added when the public service is extracted (aggregation in SQL); the day of the week in date will be finalized.
  2. Technical. The Bing source in series is enabled after the source is verified: the dashboard route accepts only engine=google|yandex.
  3. Technical. The definitions of sessions and visitors in traffic-sources are built from tracker hits and IP and User-Agent hashes over a day, not from counter sessions; the units of bounce_rate and ctr (percent) and the shape of series in traffic-sources will be finalized in OpenAPI.
  4. Technical. summary.delta_pct in the internal response compares halves of the window, while an honest comparison with the previous period is comparison. Only comparison remains in the public response.
  5. Technical. Limits for the heavy methods (funnel, visibility) separate from the general read limit are not defined.
  6. 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.
  7. For the owner. The by-country breakdown (regions) is not available: a separate piece of work.