Node.js
@rabolic/node is Rabolic for servers: server-side events, verified revenue and checkout metadata. It needs Node 20+ (global fetch) and has no runtime dependencies. Source: github.com/rabolic/sdks/js/node.
Use a secret key: rb_live_… for production, rb_test_… for staging and development. Publishable keys (rb_pub_…) are refused. Keep the key out of browsers and apps. See Keys.
Install
npm install @rabolic/node
npx rabolic init in a Node.js project does this, adds src/rabolic.ts and puts RABOLIC_SECRET_KEY in .env.
Initialize and send an event
import { Rabolic } from '@rabolic/node'
const rabolic = new Rabolic({ secretKey: process.env.RABOLIC_SECRET_KEY })
await rabolic.track({
userId: 'usr_8f2k1x9q',
event: 'subscription_renewed',
properties: { amount: 9.99, currency: 'USD' },
idempotencyKey: invoice.id,
})
// before the process exits (and at the end of a serverless invocation)
await rabolic.shutdown()
Create one client per process. Its timers are unref'd, so it never keeps your process alive. Call shutdown() to send what is queued.
Options
| Option | Default | |
|---|---|---|
secretKey | required | rb_live_… or rb_test_…. Publishable keys (rb_pub_…) are refused |
host | https://ingest.rabolic.com | |
flushAt | 20 | Send when this many events are queued (1–500) |
flushIntervalMs | 10000 | Send at least this often |
maxQueueSize | 10000 | Kept in memory; the oldest are dropped beyond it |
timeoutMs | 10000 | Per request |
debug | false | Log every call |
API
| Method | Does |
|---|---|
track({ event, userId?, anonymousId?, properties?, timestamp?, idempotencyKey? }) | Queues a custom event. Resolves once queued |
identify({ userId, anonymousId?, traits? }) | Sets person traits; with anonymousId, links that visitor to the user |
purchase / renewal / refund({ userId?, anonymousId?, amount, currency, transactionId, product, properties?, timestamp?, idempotencyKey? }) | Verified revenue: $purchase, $renewal, $refund with amount, currency, transaction_id, product. The idempotency key defaults to transactionId |
flush() | Sends the queue. Respects a backoff the server asked for |
shutdown() | Stops the timer and sends everything, skipping any backoff wait. Idempotent; later events are dropped |
checkoutMetadata({ userId?, anonymousId? }) | { rabolic_user_id?, rabolic_anonymous_id? } for Dodo Payments checkout metadata, so the payment webhook links the revenue to the person. Also a standalone export |
Calls never throw or reject; invalid ones are dropped and, with debug: true, logged. The constructor does throw for a missing or wrong key: that is a deployment bug, not a tracking call.
Identity
Every event needs userId or anonymousId.
- With only
userId, it is also sent as theanonymous_id(the protocol requires one), so it must then be at most 128 characters. The server treats it as a user without a device id. - With both, the event belongs to that visitor and that user.
identify({ userId, anonymousId })links a website or app visitor to the user. That helps when sign-up happens on your backend: pass the anonymous id from the browser (rabolic.getAnonymousId()in the web SDK) to your server.
Server events carry no context.platform. Rabolic gives them the person's last client platform from the past 24 hours, else server.
See Identity.
Revenue
await rabolic.purchase({
userId: 'usr_8f2k1x9q',
amount: 49,
currency: 'USD',
transactionId: 'pay_123',
product: 'pro_yearly',
})
purchase(), renewal() and refund() send $purchase, $renewal and $refund. Only secret keys can send them, so a page view can't fake a sale.
One revenue source per project. If the project has Dodo Payments connected in Rabolic, its webhooks are the revenue: purchase(), renewal() and refund() are still recorded as events (funnels and journeys use them) but add no revenue, so a payment never counts twice. Use them as the revenue source for any other provider. With Dodo, pass checkoutMetadata() to the checkout instead:
import { checkoutMetadata } from '@rabolic/node'
// in the metadata of your Dodo Payments checkout session
const metadata = checkoutMetadata({ userId: user.id })
Other providers aren't supported by checkoutMetadata() yet. See Revenue and attribution.
Idempotency
idempotencyKey becomes the event_id: UUIDv5 (SHA-1) of `${event}\n${idempotencyKey}` in the namespace 405a74de-a60c-4882-b7da-344cfaaeb42c (exported as EVENT_ID_NAMESPACE, with eventIdFor(event, key)). A webhook handled twice, or a retry from another process, produces the same id, and the server drops an id it has received in the last 48 hours. The event name is part of the hash, so one invoice id can key both a $renewal and a custom event.
Delivery
- Batches of up to 500 events go to
POST /v1/batchwithAuthorization: Bearer, one request at a time. - Server errors (including
503) and timeouts are retried with exponential backoff, up to 5 minutes. 429waits forRetry-After.413splits the batch.401stops sending and keeps the queue. Other4xxresponses drop the batch.- Events still queued after 7 days are dropped.
- The server rejects events whose
timestampis more than 30 days old, so don't backfill older history.
Privacy and limits
- The key is sent only in the
Authorizationheader and never logged. - Properties are copied safely (
__proto__,constructorandprototypekeys are dropped) and capped at 255 keys per level and depth 5. - Events over 32 KB (32,768 bytes of UTF-8 JSON) are dropped; the server applies the same limit.
- Events sent with a secret key get no location lookup.
Flutter
Install rabolic_flutter for iOS, Android and Flutter web. Configuration, lifecycle events, screens with Navigator, go_router and auto_route, identity, in-app purchase tokens, deep links, consent, app store privacy forms and limits.
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.