Install

HTTP API

Send events from any language or engine with one POST request.

Every SDK sends events with the same HTTP request, and so can you. Use it from engines and languages without an SDK (Unreal, Flutter, Python, Go, Rust…), or to import events from your own systems.

terminalsh
curl -X POST https://api.anyanalytics.org/v1/batch \
  -H "Authorization: Bearer vxw_YOUR_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batch":[{"uuid":"'"$(uuidgen | tr A-Z a-z)"'","event":"$session_start","distinct_id":"user_123","session_id":"s_1","context":{"platform":"custom","app_version":"1.0.0"}}]}'

Run it with your write key and the event appears in Live events within a second.

Endpoint and authentication

POST https://api.anyanalytics.org/v1/batch

Send your project's write key in one of these ways. The first one found is used:

WhereExample
Authorization headerAuthorization: Bearer vxw_YOUR_WRITE_KEY
X-Write-Key headerX-Write-Key: vxw_YOUR_WRITE_KEY
Query parameter?api_key=vxw_YOUR_WRITE_KEY
Body field"api_key": "vxw_YOUR_WRITE_KEY"

Write keys only send events; they can't read any data, so it's fine to ship them in apps and games. The body is JSON, whatever the Content-Type header says. Browsers can call the endpoint from any origin.

The request body

body.jsonjson
{
  "sent_at": "2026-05-04T10:15:02.120Z",
  "batch": [
    {
      "uuid": "0196a3c2-5b1e-7c4a-9f10-2b6d8e4a1c37",
      "event": "level_completed",
      "distinct_id": "player_8f2c",
      "session_id": "s_20260504_1",
      "timestamp": "2026-05-04T10:14:58.900Z",
      "properties": { "level": 5, "score": 1200 },
      "context": { "platform": "windows", "app_version": "1.4.0" }
    }
  ]
}
FieldWhat it is
batchRequired. 1 to 500 events.
sent_atOptional. Your device's clock when the request was sent. Used to correct event timestamps from devices whose clock is wrong.
api_keyOptional. Your write key, if you don't send it in a header.

Event fields

FieldRules
eventRequired. The event name, 1 to 200 characters.
distinct_idRequired. Who did it: your user id, or an anonymous id you keep per device. 1 to 200 characters.
uuidRecommended. A UUID you create with the event. A uuid the project has already received is counted as a duplicate, not stored again.
timestampWhen it happened: ISO 8601 with a time zone (Z or +02:00), or epoch milliseconds. Defaults to when it's received.
session_idGroups events into a session. Any string up to 64 characters.
anonymous_idThe device's anonymous id, when distinct_id is already your user id. Links the two. Up to 200 characters.
propertiesAny JSON object, up to 32 KB. Defaults to {}.
contextWhere it happened: device and app fields (below), up to 8 KB.

Context fields

These become filters and breakdowns in every report. Other keys are allowed and kept with the event.

FieldExample
platformweb, ios, android, windows, server…
app_version1.4.0
app_build212
osWindows, iOS, Android
os_version17.5
deviceiPhone15,2
device_typedesktop, mobile or tablet
browserChrome
browser_version126
localeen-US
timezoneEurope/Paris
countryTwo-letter code, such as US
regionCalifornia
citySan Francisco

Leave out country, region and city and they're filled in from the request's IP address, except when platform is server or node: a server isn't where your user is. When platform is web or missing, browser and operating system are read from the request's User-Agent.

Timestamps

  • With sent_at, every timestamp in the batch is shifted by the difference between your device's clock and ours. Send a fresh sent_at on each retry.
  • Events older than 60 days are rejected.
  • Timestamps more than an hour in the future are replaced by the time received.

Track and identify

A custom event:

terminalsh
curl -X POST https://api.anyanalytics.org/v1/batch \
  -H "Authorization: Bearer vxw_YOUR_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batch":[{"uuid":"'"$(uuidgen | tr A-Z a-z)"'","event":"signup","distinct_id":"user_123","properties":{"plan":"pro","source":"pricing_page"}}]}'

To tell us who a user is, send a $identify event from their user id. Put the anonymous id they had before in $anon_distinct_id, and the earlier events are linked to the same person. Traits in $set are saved on their profile.

terminalsh
curl -X POST https://api.anyanalytics.org/v1/batch \
  -H "Authorization: Bearer vxw_YOUR_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batch":[{"uuid":"'"$(uuidgen | tr A-Z a-z)"'","event":"$identify","distinct_id":"user_123","properties":{"$anon_distinct_id":"anon_456","$set":{"email":"[email protected]"}}}]}'

$set works on any event, not only $identify. Use $set_once for traits that should keep their first value, such as a signup date. See Events and people.

Limits

WhatLimit
Events per request500
Request body1 MiB (after decompression)
Event name, distinct_id200 characters
properties32 KB of JSON
context8 KB of JSON
session_id64 characters
Event age60 days

Requests are also rate limited per project and per sender. When you go over, you get a 429 with a Retry-After header.

Compression

Large batches compress well. Gzip the body and add Content-Encoding: gzip:

terminalsh
gzip -c events.json | curl -X POST https://api.anyanalytics.org/v1/batch \
  -H "Authorization: Bearer vxw_YOUR_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -H "Content-Encoding: gzip" \
  --data-binary @-

The response

A 200 means the batch was processed. Each event is checked on its own, so one bad event never fails the others:

response.jsonjson
{
  "accepted": 1,
  "duplicates": 0,
  "rejected": [],
  "filtered": 0
}
FieldWhat it means
acceptedEvents stored.
duplicatesEvents whose uuid was already received. Not stored again.
rejectedInvalid events, each with its position in the batch (index), its uuid when it had a valid one, and the errors.
filteredValid events dropped on purpose: known bots, referrer spam, or traffic from IP addresses you excluded in the project's settings. Not an error.
response.jsonjson
{
  "accepted": 1,
  "duplicates": 0,
  "rejected": [
    { "index": 1, "uuid": "0196a3c2-5b20-7d11-8a4e-6c3f0b9d2e81", "errors": ["distinct_id: Too small: expected string to have >=1 characters"] }
  ],
  "filtered": 0
}

Tip

Rejected events also appear in Live events with the reason, which is the quickest way to debug a new integration.

Status codes

StatusMeaningWhat to do
200Processed. Check rejected.Done.
400invalid_json or invalid_batch (with details)Fix the request. Don't retry.
401invalid_write_keyCheck the key. Don't retry.
413payload_too_largeSplit the batch and send the halves.
429rate_limitedRetry after Retry-After seconds.
503Temporarily unavailableRetry after Retry-After seconds.

Retries

Network errors happen, especially on phones and consoles. To never lose or double count:

  • Give every event a uuid when you create it, and keep it. Without one, a retried event is stored twice.
  • Retry network errors, 408, 429 and 5xx with the same events and uuids. Wait for Retry-After when it's sent, otherwise back off exponentially (1 s, 2 s, 4 s… up to a minute or so).
  • On 413, split the batch. Never retry other 4xx responses: they'll fail the same way again.
  • Keep unsent events on disk, so they survive the app being closed.

The Unity, Godot, Swift and Kotlin files are short working examples of all of this, if you want to port one to another engine.

Examples

Python

send_events.pypy
# pip install requests
import time
import uuid
from datetime import datetime, timezone

import requests

HOST = "https://api.anyanalytics.org"
WRITE_KEY = "vxw_YOUR_WRITE_KEY"


def now():
    return datetime.now(timezone.utc).isoformat()


def event(name, distinct_id, properties=None):
    # The uuid is created once, with the event, and reused on every retry
    return {
        "uuid": str(uuid.uuid4()),
        "event": name,
        "distinct_id": distinct_id,
        "timestamp": now(),
        "properties": properties or {},
    }


def send(events, attempts=6):
    for attempt in range(attempts):
        res = None
        try:
            res = requests.post(
                HOST + "/v1/batch",
                json={"sent_at": now(), "batch": events},
                headers={"Authorization": "Bearer " + WRITE_KEY},
                timeout=10,
            )
        except requests.RequestException:
            pass  # network error: retry below

        if res is not None and res.ok:
            for r in res.json()["rejected"]:
                print("rejected", events[r["index"]]["event"], r["errors"])
            return
        if res is not None and res.status_code not in (408, 429) and res.status_code < 500:
            # Bad key or bad payload: retrying won't help
            raise RuntimeError(f"HTTP {res.status_code}: {res.text}")

        retry_after = res.headers.get("Retry-After") if res is not None else None
        time.sleep(float(retry_after) if retry_after else 2**attempt)
    raise RuntimeError("gave up; keep the events and try again later")


send([event("order_completed", "user_123", {"order_id": "ord_123", "total": 49.9})])

C# (.NET)

Program.csc#
// .NET 6 or newer
using System;
using System.Net.Http;
using System.Net.Http.Json;

var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.Add("Authorization", "Bearer vxw_YOUR_WRITE_KEY");

var batch = new[]
{
    new
    {
        uuid = Guid.NewGuid().ToString(), // keep it if you resend this event
        @event = "order_completed",
        distinct_id = "user_123",
        timestamp = DateTime.UtcNow.ToString("o"),
        properties = new { order_id = "ord_123", total = 49.9 },
    },
};

var response = await http.PostAsJsonAsync("https://api.anyanalytics.org/v1/batch", new
{
    sent_at = DateTime.UtcNow.ToString("o"),
    batch,
});

var status = (int)response.StatusCode;
if (status == 408 || status == 429 || status >= 500)
{
    // Send the same batch again later, after Retry-After when it's given
    var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(5);
}
else if (!response.IsSuccessStatusCode)
{
    // Bad key or bad payload: don't retry
    Console.Error.WriteLine(await response.Content.ReadAsStringAsync());
}