Skip to content

POST /v1/batch

Send events to Rabolic from any language.

The ingest API is the protocol every Rabolic SDK speaks: JSON batches over HTTPS, one answer per batch, and rules for retrying that never count an event twice. Use it from any backend, or from a platform without an SDK yet.

  • 1–500 events per request
  • gzip accepted
  • Deduplicated by event_id

https://ingest.rabolic.com

Two endpoints, three kinds of key.

Create keys in the dashboard, per project. Each key belongs to an environment (production, staging or development), and events take theirs from the key. A secret key is shown once; a revoked key stops working on its next request.

Ingest API endpoints
EndpointPurpose
POST /v1/batchSend 1–500 events
GET /v1/config?key=rb_pub_…The project’s settings for client SDKs. Cacheable for 5 minutes
Ingest API keys
KeyPrefixUse it
Publishablerb_pub_In browsers and apps. It can only send events, and can’t send revenue
Secret, productionrb_live_On servers only. The only keys that can send $purchase, $renewal and $refund
Secret, testrb_test_On servers only, for staging and development

POST /v1/batch

The request.

A JSON envelope with 1 to 500 events. Each event is validated on its own against the same JSON Schema the SDKs are tested with, so one bad event never fails the batch.

# One event from your server. Keep its event_id
# on every retry: Rabolic stores each id once.
NOW=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)

curl https://ingest.rabolic.com/v1/batch \
  -H "Authorization: Bearer $RABOLIC_SECRET_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON
{
  "sent_at": "$NOW",
  "library": { "name": "http", "version": "1.0.0" },
  "events": [
    {
      "event_id": "0193f1c2-7a4e-7b3d-9c1a-5e8f2d4b6a10",
      "type": "track",
      "event": "invoice_paid",
      "timestamp": "$NOW",
      "anonymous_id": "usr_8f2k1x9q",
      "user_id": "usr_8f2k1x9q",
      "properties": { "plan": "pro", "seats": 5 }
    }
  ]
}
JSON

Headers and body

Request headers and size
PartRule
AuthorizationBearer <key>. Or the envelope’s key field, for sendBeacon, which can’t set headers. If both are sent, they must match
Content-Typeapplication/json, or text/plain;charset=UTF-8 so browsers skip the CORS preflight. The body is JSON either way
Content-Encodinggzip is optional
SizeAt most 1 MB on the wire and 5 MB unpacked

The envelope

Batch envelope fields
FieldTypeNotes
sent_atRFC 3339 timestamp, requiredWhen the request is sent, not when the batch was built. Used to correct device clocks
library.namestring, requiredhttp for your own client. The SDKs send rabolic-web, rabolic-node or rabolic-flutter
library.versionstring, requiredYour client’s version
eventsarray, 1–500, requiredEach event is checked on its own: one bad event never rejects the batch
keystringOnly when the key can’t go in a header

Each event

Event fields
FieldTypeNotes
event_idUUID, requiredCreated once with the event and kept on every retry. UUIDv7 is preferred
typerequiredtrack, page ($pageview), screen ($screen) or identify ($identify)
eventstring, 1–200, requiredsnake_case by convention. Names starting with $ must be reserved events
timestampRFC 3339, requiredWhen it happened, on the sender’s clock
anonymous_idstring, 1–128, requiredThe device’s random id. From a server with only a user id, send the user id here too
user_idstring, 1–255, or nullYour own id for the person
session_idUUIDv7Client SDKs send one; its time is the session start
propertiesobjectAt most 255 keys per level and 5 levels deep
traitsobjectidentify only: person properties to set
contextobjectPlatform, app, device, OS, browser, locale, time zone, screen, page, campaign, screen name, user agent

The schema is spec/schema/batch.schema.json, next to the protocol in the SDK repository. Custom names are snake_case; the reserved $ names are listed in the protocol.

200 { accepted, rejected, dropped }

The response, and what to do with it.

Ingest API responses
StatusBodyWhat to do
200{ accepted, rejected, dropped }Done: remove the whole batch from your queue. Rejected and dropped events are final
400{ error: "invalid_batch", message }The envelope is malformed. Drop the batch and fix the client; don’t retry
401{ error: "invalid_key" }Unknown or revoked key. Stop sending, and keep the queue
413{ error: "payload_too_large" }Split the batch in half and send each half
429{ error: "rate_limited" }Wait for the seconds in Retry-After, then retry
5xx, timeout—Retry with backoff: min(300 s, 1 s × 2^attempt) × random(0.5–1.0)

When events are dropped

Valid events over a limit are counted in dropped, and the answer is still 200, so no client retries them: the project’s monthly cap, the plan’s allowance (plus what the spending limit buys with overage on), or the trial’s 250,000 events. All events are dropped while the organization has no plan or trial, or after its trial ended. Dropped events aren’t billed.

Why an event is rejected

A rejected event has an index into your batch and a stable reason. It is final: sending it again gets the same answer. Rejected events aren’t billed.

Rejection reasons
reasonThe event
invalid_eventDoesn’t match the event schema
reserved_nameStarts with $ but isn’t a reserved event
revenue_needs_secret_keyIs $purchase, $renewal or $refund, sent with a publishable key
too_oldHappened more than 30 days ago, after clock correction
too_largeIs larger than 32 KB (32,768 bytes of UTF-8 JSON)
blocked_eventHas a name the project blocks. Reserved: no project blocks events yet
responses
// 200: done. Don't resend it, even with rejections
{ "accepted": 1, "rejected": [], "dropped": 0 }

// 200: one event invalid, two over a cap
{
  "accepted": 3,
  "rejected": [{ "index": 1, "reason": "reserved_name" }],
  "dropped": 2
}

// 400: a malformed envelope. Fix it; don't retry
{
  "error": "invalid_batch",
  "message": "/events must NOT have more than 500 items"
}

// 401: unknown or revoked key. Stop sending
{ "error": "invalid_key" }

safe to retry

Send twice, count once.

  • event_id

    Deduplicated by event id

    Rabolic stores each event_id once per project. Create the id with the event and keep it on every retry, and a batch sent twice is counted once.

  • sent_at

    Clocks corrected

    If sent_at and the arrival time differ by more than 30 seconds, every event in the batch moves by the difference. Timestamps more than a minute ahead become the arrival time.

  • one at a time

    Sent in order

    The SDKs keep one batch request in flight and send events in the order they happened. Do the same, and back off after errors.

GET /v1/config

Settings for client SDKs.

Client SDKs fetch the project’s settings once at start, cache them, and never wait for them to track. Today they carry the session timeouts you set for the project: minutes of inactivity on the web, and seconds in the background in apps.

It takes a publishable key, as ?key= or a Bearer header. A secret key gets 401.

Limits

Ingest API limits
LimitValue
Events per batch1–500
Request body1 MB on the wire, 5 MB unpacked
One event32 KB (32,768 bytes of UTF-8 JSON)
Properties255 keys per level, 5 levels deep, keys up to 255 characters
Event age30 days
Requests240 a minute per publishable key and client IP, 6,000 per secret key and IP
Request
curl "https://ingest.rabolic.com/v1/config?key=rb_pub_…"
Response
// Cache-Control: public, max-age=300
{
  "enabled": true,
  "sessionTimeoutWebMin": 30,
  "sessionBackgroundSec": 30,
  "autocapture": { "pageviews": true, "pageLeave": true, "lifecycle": true, "screens": false },
  "blockedEvents": [],
  "sampleRate": 1
}

ingest api faq

The ingest API, answered.

When should I use the HTTP API instead of an SDK?

When your code runs where there is no Rabolic SDK: a backend in Python, Go, Ruby, PHP or Java, a job runner, or a platform the SDKs don’t cover yet. In a browser, a Flutter app or Node.js, the SDK already does the queueing, batching, retries, ids and sessions for you.

Is it safe to retry a batch?

Yes, as long as each event keeps its event_id. Rabolic deduplicates events by event_id within the project, so a batch sent twice after a timeout is still counted once. Generate the id when the event happens and store it with the event, not when you build the request.

What does dropped mean in the response?

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, or after its trial ended. The answer is still 200 so clients don’t retry them; dropped events aren’t billed.

What if the sender’s clock is wrong?

Rabolic compares the batch’s sent_at with the time the request arrives. If they differ by more than 30 seconds, every event in the batch moves by the difference. Timestamps more than a minute in the future are set to the arrival time.

Can I call the API from a browser?

Yes, with a publishable key: CORS allows every origin, and a text/plain body needs no preflight. For websites, the web SDK does this already and handles page hide with sendBeacon, so the API is mainly for servers and platforms without an SDK.

Which key does the config endpoint take?

A publishable key, as ?key= or a Bearer header; a secret key gets 401. It returns the session timeouts set for the project and the autocapture switches, and may be cached for 5 minutes.

Can I send revenue through the API?

Yes, from your server with a secret key: $purchase, $renewal and $refund with amount, currency, transaction_id and product in the properties. With a publishable key they are rejected as revenue_needs_secret_key.

Get a key and send your first batch.

Start a 3-day trial without a card. It begins with your first event, so you can set up the SDKs first.

  1. 1

    Create a project

    Sign up, name your project, and copy its publishable key.

  2. 2

    Add an SDK

    Web, React Native, Flutter or Node.js, or the HTTP API. Or run npx rabolic init.

  3. 3

    Watch the first event arrive

    It shows in live events within seconds, and your trial starts then.