@rabolic/node
Server events and verified revenue, from Node.js.
Some events only your server knows: a renewal, a refund, an invoice paid. Send them with a secret key, keyed by your own ids, so a webhook handled twice is still one event.
- Node 20+, no dependencies
- Never keeps your process alive
- Calls never reject
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()$purchase · $renewal · $refund
Revenue that only your server can send.
Purchases, renewals and refunds are reserved events. Rabolic accepts them only with a secret key, so the revenue in your reports comes from your backend, not from a browser anyone can script. Refunds are stored as negative revenue, so totals net out.
amount,currency(ISO 4217),transactionIdandproductare required- The transaction id is the idempotency key unless you pass one
- Each event lands on the person, with the source that brought them
// The idempotency key defaults to transactionId:
// a webhook handled twice is still one event
await rabolic.renewal({
userId: 'usr_8f2k1x9q',
amount: 89,
currency: 'USD',
transactionId: invoice.id,
product: 'Pro, yearly',
})
await rabolic.refund({
userId: 'usr_8f2k1x9q',
amount: 89,
currency: 'USD',
transactionId: refund.id,
product: 'Pro, yearly',
})idempotencyKey
A webhook handled twice is still one event.
Pass your own id, such as an invoice id, as idempotencyKey. It becomes the event id, so a retried webhook, a second worker or a redeployed job sends the same id, and Rabolic keeps one event.
- UUIDv5 of the event name and your key
- The same key can name a renewal and a custom event
eventIdFor()computes the id yourself
import { eventIdFor } from '@rabolic/node'
// idempotencyKey becomes the event_id: a UUIDv5 of `${event}\n${key}`,
// so every process that handles the same invoice computes the same id
eventIdFor('subscription_renewed', invoice.id)checkoutMetadata()
Checkouts that know who is paying.
Before you send someone to a Dodo Payments checkout, put their Rabolic ids in its metadata. With Dodo Payments connected to the project, the payment webhook brings them back, and the purchase is linked to that person, even before they have signed up.
rabolic_user_idonce they are signed inrabolic_anonymous_idfrom the browser before sign-up- Also a standalone export
import { checkoutMetadata } from '@rabolic/node'
const metadata = checkoutMetadata({ userId: user.id, anonymousId: req.body.anonymousId })
// { rabolic_user_id: 'usr_8f2k1x9q', rabolic_anonymous_id: '4f1c2b7e-…' }
// Pass it as the metadata of your Dodo Payments checkout session. When the
// payment webhook arrives, Rabolic links the purchase to that person.new Rabolic({ secretKey })
The API and its options.
Every event needs a userId or an anonymousId. Calls never throw or reject; only the constructor throws, for a missing or wrong key.
Methods
| 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({ … }) | Verified revenue: $purchase, $renewal, $refund with amount, currency, transaction id and product |
| checkoutMetadata({ userId?, anonymousId? }) | The metadata that links a Dodo Payments checkout to the person |
| flush() | Sends the queue now, respecting a backoff the server asked for |
| shutdown() | Stops the timer and sends everything. Later events are dropped |
Options
| Option | Default | Notes |
|---|---|---|
| secretKey | required | rb_live_… or rb_test_…. Publishable keys are refused |
| host | Rabolic’s ingest | https://ingest.rabolic.com, or your own proxy |
| 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, and why a call was dropped |
await rabolic.shutdown()
Batched, retried, and drained on exit.
The SDK never keeps your process alive. In a serverless function, await shutdown() before you return, so the queue is sent before the runtime freezes.
The key travels only in the Authorization header and is never logged. Properties are copied safely and capped at the protocol’s limits: 255 keys per level, 5 levels deep, 32 KB per event.
| When | The SDK |
|---|---|
| 20 events are queued, or 10 seconds pass | Sends a batch of up to 500 events to POST /v1/batch, one request at a time |
| A server error or a timeout | Retries with exponential backoff, up to 5 minutes |
429 | Waits for Retry-After |
413 | Splits the batch in half and sends both |
401 | Stops sending and keeps the queue |
| Your process exits | Nothing holds it open: timers are unref’d. Call shutdown() to drain first |
node.js sdk faq
The Node.js SDK, answered.
Why do server events need a secret key?
Secret keys (rb_live_ for production, rb_test_ for staging and development) are the only keys that can send revenue: $purchase, $renewal and $refund. Rabolic rejects revenue sent with a publishable key, so a browser can’t inflate your numbers. Keep secret keys on your server; the browser and Flutter SDKs refuse them.
How do I avoid counting a webhook twice?
Pass your own id as idempotencyKey, such as the invoice id. The SDK turns it into the event id (a UUIDv5 of the event name and the key), so a webhook handled twice, or a retry from another process, sends the same id and Rabolic keeps one event. Revenue calls use the transactionId when you pass no key.
Which person does a server event belong to?
The one you name. Every event needs a userId or an anonymousId; with only a userId, it is also sent as the anonymous id. To link a visitor who signed up on your backend, call identify({ userId, anonymousId }) with the anonymous id from the browser or the app.
Do server events have a platform?
Server events carry no platform of their own. Rabolic gives each one the platform the person last used, so a renewal from your server counts toward iOS when that person uses your iOS app.
How do I use it in a serverless function?
Await shutdown() before the function returns, so the queued events are sent before the runtime freezes. In a long-running server, call it when the process is stopping.
What does checkoutMetadata do?
It returns { rabolic_user_id, rabolic_anonymous_id } for the person at checkout. Put it in your Dodo Payments checkout session’s metadata; when the project has Dodo Payments connected, the payment webhook arrives with it and the purchase is linked to that person and their sources.
Can a tracking call throw?
No. Calls never throw or reject; invalid ones are dropped and, with debug: true, logged. Only the constructor throws, for a missing or wrong key, because that is a deployment mistake. The key is sent only in the Authorization header and never logged.
Count revenue where it really happens.
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.