SDKs and API

Ingest HTTP API

Send events to Rabolic from any language with POST /v1/batch. Authentication, the batch envelope and event fields, reserved events, responses and retries, rejection reasons, deduplication, clock correction and remote config.

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 pathPurpose
POST /v1/batchSend 1–500 events
GET /v1/config?key=<publishable key>Remote config for client SDKs. Cacheable for 5 minutes

Authentication

KeyPrefixWhere it may be used
Publishablerb_pub_Browsers and apps. Write-only. Can't send revenue events
Secret, productionrb_live_Servers only
Secret, testrb_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/json or text/plain;charset=UTF-8. Browsers use text/plain so the request needs no CORS preflight. The body is JSON either way. Any other content type gets a 415.
  • Compression: Content-Encoding: gzip is optional. Any encoding other than gzip or identity gets a 415.
  • Size: at most 1 MB on the wire and 5 MB decompressed, with 1–500 events per batch.
Terminal
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

FieldTypeRequiredNotes
sent_atRFC 3339 UTC timestamp with millisecondsyesSet when the request is sent, not when the batch was built. Used for clock-skew correction
library.namestringyeshttp for your own client. One of rabolic-web, rabolic-node, rabolic-react-native, rabolic-ios, rabolic-android, rabolic-flutter, http
library.versionstringyesYour client's version
keystringnoOnly when the key can't go in a header
eventsarray, 1–500yesEach event is checked on its own: one bad event never rejects the batch

Event

FieldTypeRequiredNotes
event_idUUID (v7 preferred)yesGenerated 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
typetrack | page | screen | identifyyespage is always $pageview, screen is always $screen, identify is always $identify
eventstring, 1–200 charsyessnake_case by convention. Names starting with $ must be reserved events
timestampRFC 3339 UTC with millisecondsyesWhen the event happened, on the sender's clock
anonymous_idstring, 1–128 charsyesThe device's random id. From a server with only a user id, send the user id here too
user_idstring, 1–255 chars, or nullnoYour own id for the person
session_idUUIDv7client SDKs: yes; server: noIts embedded timestamp is the session start
propertiesobjectnoAt most 255 keys per level, depth at most 5, keys at most 255 chars
traitsobjectnoidentify only: person properties to set
contextobjectnoBelow

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_id get 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, else server.
  • Web senders don't need browser or os: the server parses user_agent, or the request's User-Agent header 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__, constructor and prototype;
  • 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 in card_number and cc_number);
  • the project's blocked properties (Project settings → Privacy & data).

Responses

StatusBodyWhenWhat to do
200{ "accepted": n, "rejected": [{ "index": i, "reason": "…" }], "dropped": n }The batch was readRemove 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 schemaDrop the batch, log it, don't retry
401{ "error": "invalid_key" }The key is missing, unknown or revoked, or the header and body keys differStop sending and keep the queue
403{ "error": "origin_not_allowed" }Reserved for keys limited to certain origins. No key has an origin list todayDrop the batch, don't retry
413{ "error": "payload_too_large" }Over 1 MB on the wire or 5 MB decompressedSplit the batch in half and retry each half
415{ "error": "invalid_batch", "message": "…" }Unsupported Content-Type or Content-EncodingDrop the batch, don't retry
429{ "error": "rate_limited" } with Retry-After: 60Too many requests per minute from one key and client IPRetry after Retry-After seconds
503{ "error": "unavailable" } with Retry-After: 5The server couldn't store the batch. None of its events were acceptedRetry with backoff. Event ids make the retry safe
Other 5xx, 408, timeout or network errorvariesRetry with backoff
Any other 4xxvariesSuch as a 404 from a wrong hostDrop 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

ReasonMeaning
invalid_eventThe event fails the schema
reserved_nameA $ name that isn't a reserved event
revenue_needs_secret_key$purchase, $renewal or $refund sent with a publishable key
too_oldThe event is more than 30 days old, after clock correction
too_largeThe serialized event is larger than 32 KB (32,768 bytes of UTF-8 JSON)
blocked_eventReserved. The server doesn't return it yet

Timestamps and clock correction

  • Device clocks: when sent_at differs 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
}
  • sessionTimeoutWebMin and sessionBackgroundSec come 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.