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
Authorization: Bearer rb_live_…
Content-Type: application/json
{
"sent_at": "2026-11-09T10:00:03.120Z",
"library": { "name": "http", … },
"events": [
{ "event": "invoice_paid", … }✓
{ "event": "$renewal", … }✓
{ "event": "$signup", … }reserved_name
]
}
{
"accepted": 2,
"rejected": [
{ "index": 2, "reason": "reserved_name" }
],
"dropped": 0
}
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.
| Endpoint | Purpose |
|---|---|
| POST /v1/batch | Send 1–500 events |
| GET /v1/config?key=rb_pub_… | The project’s settings for client SDKs. Cacheable for 5 minutes |
| Key | Prefix | Use it |
|---|---|---|
| Publishable | rb_pub_ | In browsers and apps. It can only send events, and can’t send revenue |
| Secret, production | rb_live_ | On servers only. The only keys that can send $purchase, $renewal and $refund |
| Secret, test | rb_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 }
}
]
}
JSONHeaders and body
| Part | Rule |
|---|---|
| Authorization | Bearer <key>. Or the envelope’s key field, for sendBeacon, which can’t set headers. If both are sent, they must match |
| Content-Type | application/json, or text/plain;charset=UTF-8 so browsers skip the CORS preflight. The body is JSON either way |
| Content-Encoding | gzip is optional |
| Size | At most 1 MB on the wire and 5 MB unpacked |
The envelope
| Field | Type | Notes |
|---|---|---|
| sent_at | RFC 3339 timestamp, required | When the request is sent, not when the batch was built. Used to correct device clocks |
| library.name | string, required | http for your own client. The SDKs send rabolic-web, rabolic-node or rabolic-flutter |
| library.version | string, required | Your client’s version |
| events | array, 1–500, required | Each event is checked on its own: one bad event never rejects the batch |
| key | string | Only when the key can’t go in a header |
Each event
| Field | Type | Notes |
|---|---|---|
| event_id | UUID, required | Created once with the event and kept on every retry. UUIDv7 is preferred |
| type | required | track, page ($pageview), screen ($screen) or identify ($identify) |
| event | string, 1–200, required | snake_case by convention. Names starting with $ must be reserved events |
| timestamp | RFC 3339, required | When it happened, on the sender’s clock |
| anonymous_id | string, 1–128, required | The device’s random id. From a server with only a user id, send the user id here too |
| user_id | string, 1–255, or null | Your own id for the person |
| session_id | UUIDv7 | Client SDKs send one; its time is the session start |
| properties | object | At most 255 keys per level and 5 levels deep |
| traits | object | identify only: person properties to set |
| context | object | Platform, 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.
| Status | Body | What 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.
| reason | The event |
|---|---|
| invalid_event | Doesn’t match the event schema |
| reserved_name | Starts with $ but isn’t a reserved event |
| revenue_needs_secret_key | Is $purchase, $renewal or $refund, sent with a publishable key |
| too_old | Happened more than 30 days ago, after clock correction |
| too_large | Is larger than 32 KB (32,768 bytes of UTF-8 JSON) |
| blocked_event | Has a name the project blocks. Reserved: no project blocks events yet |
// 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
| Limit | Value |
|---|---|
| Events per batch | 1–500 |
| Request body | 1 MB on the wire, 5 MB unpacked |
| One event | 32 KB (32,768 bytes of UTF-8 JSON) |
| Properties | 255 keys per level, 5 levels deep, keys up to 255 characters |
| Event age | 30 days |
| Requests | 240 a minute per publishable key and client IP, 6,000 per secret key and IP |
curl "https://ingest.rabolic.com/v1/config?key=rb_pub_…"// 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
Create a project
Sign up, name your project, and copy its publishable key.
- 2
Add an SDK
Web, React Native, Flutter or Node.js, or the HTTP API. Or run npx rabolic init.
- 3
Watch the first event arrive
It shows in live events within seconds, and your trial starts then.