Walker Formulation Academy for developers API reference Error catalog OpenAPI
Sections

#Events API

Status: Planned. The POST /analytics/events method is being designed and is not yet available. This page records the agreed contract; values marked "provisional" will be finalized in OpenAPI 3.1. Until it ships, use the JS tracker and the modules.

Purpose: accept a batch of events from your systems: an SPA site, a mobile app, a server. This is the fourth path of analytics collection.

#Contents

  1. When to use it
  2. Access
  3. Request
  4. Event types
  5. Event fields
  6. Response
  7. Errors
  8. Deduplication and time
  9. Examples
  10. Recipes
  11. Relation to chat conversations and lead attribution
  12. Notes
  13. Open questions

#When to use it

SituationWhat to choose
A single-page application (SPA)this method; the JS tracker does not support SPAs
A mobile appthis method, through your server
You need events: a request, a sign-up, a purchasethis method
Only page visits on an ordinary sitethe JS tracker is simpler
Payments and deals in a CRM that are not tied to a browseroffline conversions

#Access

Method and pathPOST https://<cell domain>/api/public/v1/analytics/events
AuthenticationAuthorization: Bearer <key> (Authentication)
Scopeanalytics:write (Authentication); there are no paid scopes here
Paidno
Limit120 requests per minute per key and tenant; at most 100 events per batch (Conventions)
Personal datanot accepted. The analytics:write scope allows only pseudonymous identifiers. Email and phone in properties are rejected with validation_failed
Idempotencythe Idempotency-Key header (24-hour window, Conventions) and the event_id field on each event
Key in the browsernot allowed. The key is a server secret; send events through your own server

Cell addresses: RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id. A key works only in the cell where it was issued.

#Request

Headers

HeaderRequiredValue
AuthorizationyesBearer <key>
Content-Typeyesapplication/json
Idempotency-Keyyesa unique string (usually a UUID v4); a repeat with the same key and body returns the earlier result

Body

FieldTypeRequiredConstraintsExample
eventsarrayyesfrom 1 to 100 events; body ≤ 256 KBsee below

One batch may mix event types.

{
  "events": [
    {
      "event_id": "evt_2026_10_11_0001",
      "type": "page_view",
      "session_id": "sess_EXAMPLE_01",
      "anonymous_id": "anon_EXAMPLE_01",
      "page_key": "/blog/emulsions",
      "title": "Как сделать эмульсию",
      "referrer": "https://www.google.com/",
      "utm": { "source": "google", "medium": "cpc", "campaign": "emulsion-course", "content": "banner-a", "term": "эмульсия курс" },
      "occurred_at": "2026-10-11T08:15:30Z",
      "consent": true
    }
  ]
}

#Event types

typeMeaningSpecial fields
page_viewa page or screen viewpage_key, title, referrer
eventa goal or action (a button click, a sign-up)properties (the goal name goes in properties.name, provisional)
lead_forma request form submissionproperties without contacts
conversionan action with an amount (a payment, a subscription)value in the money format

#Event fields

FieldTypeRequiredConstraintsExample
event_idstringyesunique within the integration; serves as the deduplication keyevt_2026_10_11_0001
typestringyespage_view | event | lead_form | conversionpage_view
session_idstringyesa session identifier on the client side; the length will be finalizedsess_EXAMPLE_01
anonymous_idstringyesa visitor pseudonym; not an email or a phone numberanon_EXAMPLE_01
user_idstringnoyour internal pseudonymous id; not a contactuser_EXAMPLE_42
utmobjectnosource, medium, campaign, content, term: the five UTM tags{"source":"google"}
click_idsobjectnoad-platform click identifiers, for example gclid, yclid, fbclid; the exact list will be finalized{"yclid":"123"}
page_keystringfor page_viewpath without a language prefix, for example /blog/emulsions; the home page is /home/blog/emulsions
titlestringnopage titleКак сделать эмульсию
referrerstringnosource addresshttps://www.google.com/
occurred_atstringnoISO 8601 UTC; defaults to the receipt time; no earlier than 7 days ago (provisional)2026-10-11T08:15:30Z
consentbooleanyesthe visitor's consent to analytics at the time of the eventtrue
propertiesobjectnoarbitrary key-to-scalar pairs; no personal data{"plan":"pro"}
valueobjectfor conversion{"amount":"4990.00","currency":"RUB"}; the amount is a string{"amount":"4990.00","currency":"RUB"}

Events with consent: false are accepted but are not used for user attribution (exactly how will be finalized; see the open questions).

#Response

Success is 202 Accepted: the batch is accepted, and data aggregation runs after the response. This is not an asynchronous operation with an operation object: the receipt result for each event is already in the response body, in the same order as in the request.

{
  "accepted": 2,
  "rejected": 1,
  "results": [
    { "event_id": "evt_2026_10_11_0001", "status": "accepted" },
    { "event_id": "evt_2026_10_11_0002", "status": "duplicate" },
    {
      "event_id": "evt_2026_10_11_0003",
      "status": "rejected",
      "error": { "code": "validation_failed", "field": "/events/2/properties/email", "message": "Контактные данные не принимаются" }
    }
  ]
}
FieldTypeDescription
acceptedintegerthe number of accepted events (duplicates are not counted)
rejectedintegerthe number of rejected events
results[].statusstringaccepted, duplicate, rejected
results[].errorobjectfor rejected events: code, field (JSON Pointer), message, the same shape as an errors[] element in the Conventions

If the batch itself is invalid (not JSON, no events, more than 100 events), an error for the whole request is returned and no event is accepted.

#Errors

The format is application/problem+json; the shared code catalog is in the API conventions.

HTTPCodeWhen
401invalid_api_keythe key is wrong, revoked, or expired
403scope_missingthe key lacks analytics:write
403ip_not_allowedthe request comes from an address outside the integration's IP list
403feature_not_in_planthe license does not include api_access
409idempotency_conflictthe same Idempotency-Key with a different body
413payload_too_largethe body is larger than 256 KB
422validation_failedthe batch failed validation: not JSON, no events, more than 100 events (the list of fields is in errors)
429rate_limitedthe rate limit was exceeded
503service_unavailablethe limits store is temporarily unavailable

#Deduplication and time

#Examples

#curl

curl -X POST 'https://formula-cream.pro/api/public/v1/analytics/events' \
  -H 'Authorization: Bearer sap_ru_live_EXAMPLE0KEYI_EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLE' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: batch-2026-10-11-0001' \
  -d '{
    "events": [
      {
        "event_id": "evt_2026_10_11_0001",
        "type": "event",
        "session_id": "sess_EXAMPLE_01",
        "anonymous_id": "anon_EXAMPLE_01",
        "page_key": "/pricing",
        "properties": { "name": "signup_clicked" },
        "occurred_at": "2026-10-11T08:15:30Z",
        "consent": true
      }
    ]
  }'

#JavaScript (on your server)

// Node.js 18+. The key is kept in a server environment variable, not in the browser.
const response = await fetch('https://wfacademy.org/api/public/v1/analytics/events', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    events: [{
      event_id: crypto.randomUUID(),
      type: 'conversion',
      session_id: 'sess_EXAMPLE_01',
      anonymous_id: 'anon_EXAMPLE_01',
      value: { amount: '49.00', currency: 'GBP' },
      consent: true,
    }],
  }),
});
if (response.status !== 202) throw new Error(`API: ${response.status}`);
console.log(await response.json());

#Python

import os, uuid, requests

response = requests.post(
    "https://wfacademy.id/api/public/v1/analytics/events",
    headers={
        "Authorization": f"Bearer {os.environ['API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"events": [{
        "event_id": str(uuid.uuid4()),
        "type": "page_view",
        "session_id": "sess_EXAMPLE_01",
        "anonymous_id": "anon_EXAMPLE_01",
        "page_key": "/home",
        "consent": True,
    }]},
    timeout=5,
)
response.raise_for_status()

#PHP

<?php
$payload = json_encode(['events' => [[
    'event_id'     => bin2hex(random_bytes(16)),
    'type'         => 'lead_form',
    'session_id'   => 'sess_EXAMPLE_01',
    'anonymous_id' => 'anon_EXAMPLE_01',
    'properties'   => ['form' => 'callback'],
    'consent'      => true,
]]], JSON_UNESCAPED_UNICODE);

$ch = curl_init('https://formula-cream.pro/api/public/v1/analytics/events');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 5,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

#Recipes

#Single-page application

  1. In the browser, create an anonymous_id (a random string) on load and keep it on the client side; start a new session_id after every 30 minutes of inactivity.
  2. On every route change, send a page_view event to your server address (for example, /analytics), not directly to Walker Formulation Academy.
  3. Your server adds event_id, checks consent, and forwards the batch to Walker Formulation Academy with the key.
  4. Read UTM once on first load and pass it in all events of the session.

Forward events in batches of up to 100 every few seconds; this is cheaper than one at a time.

#Mobile app

  1. The app keeps anonymous_id in secure storage and manages sessions according to platform rules.
  2. Screens are page_view with a page_key such as /screen/checkout.
  3. An offline queue on the device is allowed for up to 7 days (the occurred_at window); event_id makes resending safe.
  4. Events go to your backend, which forwards them to Walker Formulation Academy. Do not embed the key in the app.

#Relation to chat conversations and lead attribution

Walker Formulation Academy determines a lead's source from the session. When a visitor writes in the site chat, the session is linked to the conversation, and the attribution is saved in the lead card: source, UTM, entry page, the number of visits and page views, and the basis of the calculation (basis: funnel, form, or none).

The plan is to pass session_id from the Events API into the widget, so that events from your systems enter the visit history and the lead attribution (basis: funnel). For now the link is built on the JS tracker's session; for API events it is not yet defined (see the open questions).

#Notes

#Open questions

  1. For the owner. The time window. "No older than 7 days" comes from the documentation brief; the project documents do not fix it.
  2. For the owner. The semantics of consent: false: drop the event, or store it without identifiers.
  3. Technical. The event_id format, the session_id length, the click_ids list, and the goal name in the event type are not fixed.
  4. Technical. Linking session_id to the widget conversation. The design plans "sid in the iframe → attribution basis: funnel", but the contract is not defined.
  5. Technical. The response format (accepted/rejected/results) is provisional.
  6. Technical. Place in the stages: event intake is not assigned to a specific release stage anywhere.