Ingest HTTP API
The ingest API is the protocol every Rabolic SDK speaks: JSON batches over HTTPS, one answer per batch, and retry rules that never count an event twice. Use it from a backend in any language, or from a platform without an SDK. The full specification is spec/protocol.md, with a JSON Schema in spec/schema/batch.schema.json.
In a browser, a Flutter or React Native app, or Node.js, use the SDK: it already handles queueing, batching, retries, ids and sessions.
Endpoints
Host: https://ingest.rabolic.com.
| Method and path | Purpose |
|---|---|
POST /v1/batch | Send 1–500 events |
GET /v1/config?key=<publishable key> | Remote config for client SDKs. Cacheable for 5 minutes |
Authentication
| Key | Prefix | Where it may be used |
|---|---|---|
| Publishable | rb_pub_ | Browsers and apps. Write-only. Can't send revenue events |
| Secret, production | rb_live_ | Servers only |
| Secret, test | rb_test_ | Servers only |
Send the key in a header (preferred):
Authorization: Bearer rb_live_…
Or in the envelope's key field, for navigator.sendBeacon, which can't set headers. When both are present, they must be equal.
The key's environment (production, staging or development) is stored with the key. Events take it from the key.
Request
- Content type:
application/jsonortext/plain;charset=UTF-8. Browsers usetext/plainso the request needs no CORS preflight. The body is JSON either way. Any other content type gets a415. - Compression:
Content-Encoding: gzipis optional. Any encoding other thangziporidentitygets a415. - Size: at most 1 MB on the wire and 5 MB decompressed, with 1–500 events per batch.
curl https://ingest.rabolic.com/v1/batch \
-H "Authorization: Bearer $RABOLIC_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"sent_at": "2026-11-08T10:00:03.120Z",
"library": { "name": "http", "version": "1.0.0" },
"events": [{
"event_id": "019a6b2e-6f3c-7d1a-9b2f-3c4d5e6f7a8b",
"type": "track",
"event": "signup_completed",
"timestamp": "2026-11-08T10:00:01.000Z",
"anonymous_id": "usr_8f2k1x9q",
"user_id": "usr_8f2k1x9q",
"properties": { "method": "google" }
}]
}'
Batch envelope
| Field | Type | Required | Notes |
|---|---|---|---|
sent_at | RFC 3339 UTC timestamp with milliseconds | yes | Set when the request is sent, not when the batch was built. Used for clock-skew correction |
library.name | string | yes | http for your own client. One of rabolic-web, rabolic-node, rabolic-react-native, rabolic-ios, rabolic-android, rabolic-flutter, http |
library.version | string | yes | Your client's version |
key | string | no | Only when the key can't go in a header |
events | array, 1–500 | yes | Each event is checked on its own: one bad event never rejects the batch |
Event
| Field | Type | Required | Notes |
|---|---|---|---|
event_id | UUID (v7 preferred) | yes | Generated once when the event is created and kept on every retry. The server drops an event whose id it has received in the last 48 hours |
type | track | page | screen | identify | yes | page is always $pageview, screen is always $screen, identify is always $identify |
event | string, 1–200 chars | yes | snake_case by convention. Names starting with $ must be reserved events |
timestamp | RFC 3339 UTC with milliseconds | yes | When the event happened, on the sender's clock |
anonymous_id | string, 1–128 chars | yes | The device's random id. From a server with only a user id, send the user id here too |
user_id | string, 1–255 chars, or null | no | Your own id for the person |
session_id | UUIDv7 | client SDKs: yes; server: no | Its embedded timestamp is the session start |
properties | object | no | At most 255 keys per level, depth at most 5, keys at most 255 chars |
traits | object | no | identify only: person properties to set |
context | object | no | Below |
When anonymous_id equals user_id, the server treats the event as having no device id: it attaches the event to that user and links no anonymous id.
Sessions:
- Events sent with a publishable key and no
session_idget one from Rabolic, per anonymous id, with the project's web session timeout. - Events sent with a secret key are server events and belong to no session. Purchases are still credited to the session they happened in.
Context
Every field is optional:
{
"platform": "web | ios | android",
"app": { "version": "4.12.0", "build": "412", "bundle": "com.acme.fit" },
"device": { "type": "mobile | tablet | desktop | tv", "model": "iPhone15,2", "manufacturer": "Apple" },
"os": { "name": "iOS", "version": "18.1" },
"browser": { "name": "Chrome", "version": "131" },
"locale": "en-US",
"timezone": "Europe/Berlin",
"screen": { "width": 393, "height": 852, "density": 3 },
"page": { "url": "https://…", "path": "/pricing", "title": "Pricing", "referrer": "https://google.com/" },
"campaign": { "source": "google", "medium": "cpc", "name": "q4", "content": "ad1", "term": "fitness", "click_id": "gclid" },
"screen_name": "HomeScreen",
"user_agent": "Mozilla/5.0 …"
}
- Server senders leave out
platform. The server then uses the person's last client platform from the past 24 hours, elseserver. - Web senders don't need
browseroros: the server parsesuser_agent, or the request'sUser-Agentheader when it is missing.
What the server removes
These property and trait keys are removed without rejecting the event:
- keys longer than 255 characters;
__proto__,constructorandprototype;- objects nested deeper than 5 levels;
- sensitive names:
password,passwd,pwd,card_number,cc_number,cvv,cvc,iban,ssn,social_security_number(any case,_optional incard_numberandcc_number); - the project's blocked properties (Project settings → Privacy & data).
Responses
| Status | Body | When | What to do |
|---|---|---|---|
200 | { "accepted": n, "rejected": [{ "index": i, "reason": "…" }], "dropped": n } | The batch was read | Remove the whole batch from your queue. Rejected and dropped events are final |
400 | { "error": "invalid_batch", "message": "…" } | The body isn't JSON, a gzip body is corrupt, or the envelope fails the schema | Drop the batch, log it, don't retry |
401 | { "error": "invalid_key" } | The key is missing, unknown or revoked, or the header and body keys differ | Stop sending and keep the queue |
403 | { "error": "origin_not_allowed" } | Reserved for keys limited to certain origins. No key has an origin list today | Drop the batch, don't retry |
413 | { "error": "payload_too_large" } | Over 1 MB on the wire or 5 MB decompressed | Split the batch in half and retry each half |
415 | { "error": "invalid_batch", "message": "…" } | Unsupported Content-Type or Content-Encoding | Drop the batch, don't retry |
429 | { "error": "rate_limited" } with Retry-After: 60 | Too many requests per minute from one key and client IP | Retry after Retry-After seconds |
503 | { "error": "unavailable" } with Retry-After: 5 | The server couldn't store the batch. None of its events were accepted | Retry with backoff. Event ids make the retry safe |
Other 5xx, 408, timeout or network error | varies | Retry with backoff | |
Any other 4xx | varies | Such as a 404 from a wrong host | Drop the batch, don't retry |
Backoff: min(300 s, 1 s × 2^attempt) × random(0.5–1.0), reset after any 200. Send one request at a time, in the order the events were created, with a 10 s timeout.
dropped counts valid events that weren't accepted because a limit was reached: the project's monthly cap, the plan's allowance (plus what the spending limit buys with overage on), or the trial's cap. Events are also dropped while the organization has no plan or trial. The answer is still 200, so don't retry them. Dropped events aren't billed.
Rejection reasons
| Reason | Meaning |
|---|---|
invalid_event | The event fails the schema |
reserved_name | A $ name that isn't a reserved event |
revenue_needs_secret_key | $purchase, $renewal or $refund sent with a publishable key |
too_old | The event is more than 30 days old, after clock correction |
too_large | The serialized event is larger than 32 KB (32,768 bytes of UTF-8 JSON) |
blocked_event | Reserved. The server doesn't return it yet |
Timestamps and clock correction
- Device clocks: when
sent_atdiffers from the server's arrival time by more than 30 s, every event in the batch moves by that difference. - Future times: an event still more than 60 s in the future after that is set to the arrival time.
- Old events: an event more than 30 days old after correction is rejected with
too_old.
Revenue
Send $purchase, $renewal and $refund with a secret key, with amount, currency, transaction_id and product in the properties. With a publishable key they are rejected as revenue_needs_secret_key. If the project has Dodo Payments connected, these events are kept for funnels and journeys but add no revenue. See Revenue and attribution.
Rate limits
240 requests a minute per publishable key and client IP, and 6,000 per secret key and IP. Over that, the answer is 429 with Retry-After.
Remote config
GET /v1/config?key=rb_pub_… returns, with Cache-Control: public, max-age=300:
{
"enabled": true,
"sessionTimeoutWebMin": 30,
"sessionBackgroundSec": 30,
"autocapture": { "pageviews": true, "pageLeave": true, "lifecycle": true, "screens": false },
"blockedEvents": [],
"sampleRate": 1
}
sessionTimeoutWebMinandsessionBackgroundSeccome from the project's settings.- Every other field is fixed at the values above today. The SDKs honor them, but no project setting changes them yet.
- The key must be publishable. An unknown, revoked or secret key gets
401 { "error": "invalid_key" }.
Compatibility
Version 1 only gets additive changes: new optional fields and new reserved names. The server accepts every SDK version released in the last 24 months.
Node.js
Install @rabolic/node to send server-side events and verified revenue (purchases, renewals, refunds) with a secret key. Options, identity, idempotency, Dodo Payments checkout metadata, delivery and limits.
Events
What a Rabolic event is, how to name events and properties, super properties, and every reserved $ event the SDKs and the server use.