Docs · Reference
HTTP API
POST /api/e: authentication, event fields, batch and size limits, the rate limit and every response.
Every collector sends events to one endpoint. Use it directly from any server, edge function or log pipeline when there's no ready-made collector for your stack.
POST https://ethogram.si/api/eThe same reference is available as an OpenAPI 3.1 spec, and this page as markdown.
Authentication
Send your project's server key as a bearer token, and the body as JSON:
Authorization: Bearer $ETHOGRAM_KEY
Content-Type: application/jsonThe key proves the events come from your site, so keep it on your server. Create it on the project's setup page or under Settings, Server keys. To change keys, add a second key, switch your collector, then revoke the old one; a revoked key stops within a minute.
Example
# Up to 50 events per request. Send after your response, and leave out cookies and credentials.# Skip images, CSS and fonts, but send requests for .js and .mjs files: Ethogram counts the scripts agents fetch.# Send every other request header as received, signature, signature-input and signature-agent included, with the# full URL as requested: signed agents (Web Bot Auth) are verified against them.# Add "action": { "name": "Added to cart", "target": "Blue T-shirt" } to an event to report a named action.# Add "response" with your answer's content-type, content-length and cf-mitigated headers to see why agents get stuck.curl https://ethogram.si/api/e \ -H "Authorization: Bearer $ETHOGRAM_KEY" \ -H "Content-Type: application/json" \ -d '{ "collector": "0.12", "events": [{ "url": "https://example.com/pricing?plan=pro", "method": "GET", "time": "2026-10-10T12:00:00Z", "status": 200, "duration": 42, "ip": "203.0.113.7", "headers": { "user-agent": "Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)", "accept": "text/html" }, "response": { "content-type": "text/html; charset=utf-8", "content-length": 48213 } }] }'# Optional browser check: serve https://ethogram.si/ethogram.js at /_ethogram/check.js on your site, add# <script src="/_ethogram/check.js" defer></script> to your pages, and answer each POST to /_ethogram/report with a 204. Then send# its JSON body as the event's "browser" field, with that request's own url, ip and headers:# { "url": "https://example.com/_ethogram/report", "method": "POST", "ip": "203.0.113.7",# "headers": { "user-agent": "Mozilla/5.0 …" }, "browser": { "v": 1, "phase": "load", … } }Body
An object with an events list, { "events": [ ... ] }, or the list on its own. A batch holds 1 to 50 events, and the body may be up to 64 KB (65,536 bytes).
The object may also carry collector, the version of our snippets your code follows, currently "0.12". With it, your dashboard can tell you when a feature needs a newer snippet. It's optional, and a value that isn't a version is ignored.
Event fields
| Field | Type | Meaning |
|---|---|---|
url | string | Required. The full URL requested, with scheme and host (http or https). Up to 4,000 characters. |
method | string | The HTTP method. Defaults to GET. |
time | number or string | When the request arrived: epoch milliseconds or an ISO 8601 string. Missing, or more than a day in the past or five minutes in the future, it becomes the time we received it. |
status | number | The response status code, 100 to 599, when known. |
duration | number | Milliseconds taken to respond, 0 to 600,000, when known. |
ip | string | The visitor's IPv4 or IPv6 address. |
headers | object | The request headers, name to value. Names are lowercased; up to 100 headers of up to 2,000 characters each. Leave out cookie, authorization and proxy-authorization. |
response | object | What you answered: { "content-type": string, "content-length": number or numeric string, "cf-mitigated": string }, all optional. Kept for agents only, to explain why they get stuck (a challenge page, a JavaScript-only page, a slow answer). |
action | object | A named action: { "name": up to 100 characters, "target": up to 200 }. See Named actions. |
edge | object | What your host's edge saw of the connection: { "asn": number, "org": string, "protocol": "HTTP/2" and the like, "tls": "TLSv1.3" and the like, "botScore": 1 to 99 }, all optional. Read to tell people from automation, never kept. The Cloudflare Worker sends it from request.cf. |
browser | object | A browser check report: the JSON body the script posted to /_ethogram/report on your site, as received. The event's ip and headers are that request's own. It adds evidence to the visit and isn't counted as a request. |
Longer text is cut to its limit, and a number out of range is ignored. An event without a usable url is dropped; the rest of the batch still counts.
Headers that carry credentials or sessions are dropped on arrival even if you send them: cookie, set-cookie, authorization, proxy-authorization, x-api-key, x-auth-token, x-csrf-token, x-xsrf-token, and any header whose name contains token, secret, session or password. Send signature, signature-input and signature-agent as received: they're how signed agents are verified.
Rate limit
Up to 6,000 events per project per minute. The count is kept in memory on each of our servers, so treat it as a ceiling, not an exact figure. Over it, the batch is refused with 429 and Retry-After: 60. Requests for .js and .mjs files have an allowance of their own, also 6,000 a minute, so they never crowd out page views: over it they're dropped, not refused, and a person's never count toward it.
Responses
The answer comes as soon as the batch is checked. Detection, verification and storage run afterwards, so a collector never waits on them. Errors are checked in this order: key, plan, size, JSON, batch, rate limit.
| Status | Body | When |
|---|---|---|
| 202 | { "accepted": 1 } | The batch was accepted. accepted counts the events that could be used. |
| 400 | { "error": "Body must be JSON." } | The body isn't JSON. |
| 400 | { "error": "Send 1 to 50 events." } | No events list, an empty one, or more than 50 events. |
| 401 | { "error": "Unknown server key." } | The Authorization header is missing, malformed, or holds a key no project has, or one that was revoked. |
| 402 | { "error": "This project has no active plan, ..." } | No owner of the project has an active plan, so nothing is recorded. |
| 413 | { "error": "Batch too large." } | The body is over 64 KB. |
| 429 | { "error": "Too many events." } | Over the rate limit. Comes with Retry-After: 60. |
The collectors ignore every answer, so a lapsed plan or a wrong key never breaks your site. When a plan starts or ends, it can take up to a minute for the answer to change.