Skip to content

@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), transactionId and product are required
  • The transaction id is the idempotency key unless you pass one
  • Each event lands on the person, with the source that brought them
Revenue analytics
webhooks.ts
// 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
ids.ts
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_id once they are signed in
  • rabolic_anonymous_id from the browser before sign-up
  • Also a standalone export
checkout.ts
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

Node.js SDK methods
MethodDoes
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

Node.js SDK options
OptionDefaultNotes
secretKeyrequiredrb_live_… or rb_test_…. Publishable keys are refused
hostRabolic’s ingesthttps://ingest.rabolic.com, or your own proxy
flushAt20Send when this many events are queued (1–500)
flushIntervalMs10000Send at least this often
maxQueueSize10000Kept in memory; the oldest are dropped beyond it
timeoutMs10000Per request
debugfalseLog 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.

How the Node.js SDK delivers events
WhenThe SDK
20 events are queued, or 10 seconds passSends a batch of up to 500 events to POST /v1/batch, one request at a time
A server error or a timeoutRetries with exponential backoff, up to 5 minutes
429Waits for Retry-After
413Splits the batch in half and sends both
401Stops sending and keeps the queue
Your process exitsNothing 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. 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.