Skip to content

Repository files navigation

Siren SDK for Node.js

Record affiliate and incentive events, and verify signed webhooks, in a few lines of TypeScript.

npm versionCILicense: MITNode

What is Siren?

Siren is headless affiliate and incentive tracking for any commerce stack. It tracks the full lifecycle from customer interaction to payout — clicks, sales, signups, course completions — and computes rewards from flexible rules you define. Affiliate programs, referral programs, partner and reseller commissions, creator royalties, sales commissions, and loyalty rewards all run on the same engine.

What this SDK does

This SDK is the Node.js and TypeScript integration point for Siren. In a few lines you can:

  • Record events (events.sale, events.refund, events.siteVisited) so conversions and payouts compute.
  • Verify signed webhooks (webhooks.constructEvent) so you can trust inbound deliveries.

It runs on Node 18+ with zero runtime dependencies (it uses the global fetch), and ships ESM, CJS, and full type declarations.

Install

npm install @novatorius/siren

Quickstart: record a sale

Mint an API key in the Siren dashboard (Settings → API Keys), then:

import{Siren}from'@novatorius/siren';constsiren=newSiren({apiKey: process.env.SIREN_API_KEY!});// Record a completed sale so conversions and payouts compute.// `total` is in MAJOR currency units: 49.99 means $49.99.const{ opportunityId }=awaitsiren.events.sale({source: 'stripe',// your commerce sourceexternalId: 'cs_test_a1b2c3',// your order id — used to match refunds latertotal: 49.99,trackingId: 4021,// opportunity id from the Siren tracking cookie});console.log(`Recorded against opportunity ${opportunityId}`);

With line items (per-unit amount; a missing quantity defaults to 1):

awaitsiren.events.sale({source: 'woocommerce',externalId: 'order-88',total: 159.97,trackingId: 4021,items: [{name: 'Pro Plan (annual)',amount: 49.99},// quantity defaults to 1{externalId: 'sku-2',name: 'Add-on seat',quantity: 3,amount: 36.66},],});

Refunds and referred visits work the same way:

awaitsiren.events.refund({source: 'stripe',externalId: 'cs_test_a1b2c3'});awaitsiren.events.siteVisited({collaboratorId: 88,userId: 12345});awaitsiren.events.ingest('loyalty-points-earned',{userId: 42,points: 100});

Quickstart: verify a webhook

Siren signs every delivery with X-Siren-Signature: sha256=<hmac> — an HMAC-SHA256 of the raw request body keyed by your subscription's signing secret. constructEvent verifies the signature (constant-time) and parses the event in one call.

⚠️ You MUST pass the RAW request body bytes to constructEvent. Body parsers like express.json() re-serialize the payload, and the HMAC will never match a re-serialized body. Use express.raw() (or your framework's raw-body equivalent) on the webhook route so you hand constructEvent the exact bytes Siren sent.

importexpressfrom'express';import{Siren,SignatureVerificationError}from'@novatorius/siren';constsiren=newSiren({apiKey: process.env.SIREN_API_KEY!});constapp=express();app.post('/webhooks/siren',express.raw({type: 'application/json'}),(req,res)=>{letevent;try{event=siren.webhooks.constructEvent(req.body,// raw Buffer — exact bytes receivedreq.header('X-Siren-Signature'),process.env.SIREN_WEBHOOK_SECRET!,);}catch(err){if(errinstanceofSignatureVerificationError){returnres.status(400).send('invalid signature');}throwerr;}switch(event.type){case'conversion.approved':
// ...break;case'payout.paid':
// ...break;}res.sendStatus(200);});

Create the subscription (the signingSecret is returned once — store it):

import{WebhookEventType}from'@novatorius/siren';constsub=awaitsiren.webhooks.subscriptions.create({targetUrl: 'https://example.com/webhooks/siren',events: [WebhookEventType.ConversionApproved,WebhookEventType.PayoutPaid],// or: events: [WebhookEventType.All]});awaitsaveSecretSomewhereSafe(sub.signingSecret);

Features

  • Event ingestion — record sales, refunds, referred visits, and custom event types (events.sale, events.refund, events.siteVisited, events.ingest). Ingestion is auto-retried on network errors and 429/5xx.
  • Signed-webhook verification and subscriptions — constant-time signature checks (webhooks.constructEvent, webhooks.verifySignature) plus subscription management (webhooks.subscriptions.create / list / delete).
  • API keys — mint, list, and revoke keys (apiKeys.create / list / revoke). The raw key is returned once and cannot be retrieved later.
  • Reconciliation reads — thin paginated readers over Siren's ledger for conversions, transactions, obligations, and payouts.
  • Typed errors — every failure throws a typed subclass of SirenError carrying message, code, and statusCode (NotFoundError, RateLimitError, ValidationError, and more).
constconversions=awaitsiren.conversions.list({page: 1,perPage: 50});console.log(conversions.estimatedCount);// total across all pages, if knownconstkey=awaitsiren.apiKeys.create({label: 'Production server'});// key.rawKey (sk_live_...) is returned ONCE and cannot be retrieved later.

Configuration

constsiren=newSiren({apiKey: 'sk_live_...',// requiredbaseUrl: 'https://api.sirenaffiliates.com/siren/v1',// default; point at staging/local as neededtimeout: 30_000,// ms, default 30smaxRetries: 2,// default 2});

Idempotent reads and event ingestion automatically retry network errors and 429/5xx responses with exponential backoff (honoring Retry-After). Management writes — apiKeys.create and webhooks.subscriptions.create — are never auto-retried, so a flaky connection can't mint duplicate credentials.

Other SDKs

Siren ships official SDKs in three languages, all built against the same API:

Links

Contributing

Contributions are welcome. See CONTRIBUTING.md for how to clone, build, test, and open a pull request. Please also review our Code of Conduct.

Error handling

API and network failures throw a typed subclass of SirenError. Catch the base class to handle any SDK error, or branch on a specific subclass. Every error has a message; code (the API's machine-readable error code) and statusCode (the HTTP status) are set when the failing response provides them — ConnectionError and SignatureVerificationError, for example, carry no statusCode:

ErrorWhen it's thrown
AuthenticationErrorMissing or invalid API key (401)
PermissionErrorKey lacks permission for the operation (403)
NotFoundErrorThe requested resource does not exist (404)
ValidationErrorRequest payload failed validation (422)
BadRequestErrorMalformed request (400)
ConflictErrorConflicting state, e.g. a duplicate (409)
RateLimitErrorRate limit exceeded (429); honors Retry-After
ApiErrorUnexpected server error (5xx)
ConnectionErrorNetwork failure reaching Siren
SignatureVerificationErrorA webhook signature did not verify

Two exceptions to the subclass rule: constructing a client without an apiKey throws the base SirenError itself, and webhooks.constructEvent throws a native SyntaxError — not a SirenError — when a correctly signed body is not valid JSON.

import{Siren,SirenError,RateLimitError,ValidationError,NotFoundError,}from'@novatorius/siren';constsiren=newSiren({apiKey: process.env.SIREN_API_KEY!});try{awaitsiren.events.sale({source: 'stripe',externalId: 'cs_test_a1b2c3',total: 49.99,trackingId: 4021,});}catch(err){if(errinstanceofRateLimitError){// already retried with backoff; back off further or queue for later}elseif(errinstanceofValidationError){console.error('Invalid payload:',err.message,err.code);}elseif(errinstanceofNotFoundError){// nothing matched — e.g. a refund for a sale Siren never recorded}elseif(errinstanceofSirenError){// statusCode/code are set when the API produced the failureconsole.error(`Siren error ${err.statusCode??'n/a'} (${err.name}):`,err.message);}else{throwerr;// not a Siren error}}

Resources

License

MIT © 2026 Novatorius LLC

About

Official Siren SDK for Node.js & TypeScript — affiliate & incentive tracking for any commerce stack.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages