Skip to content

decisa-ai/decisa-node

Repository files navigation

@decisa/node

Official server-side Node.js SDK for the Decisa ingest API. Record conversions and events from your backend with typed inputs, typed errors, and safe automatic retries.

  • Zero runtime dependencies (uses built-in fetch / AbortController)
  • First-class TypeScript types, ESM + CJS
  • camelCase in, snake_case on the wire — you never touch the wire format
  • A money guard that catches the #1 install footgun (sending decimals instead of integer cents)
  • Idempotent by design: re-sending the same id is safe

Server-side only — never ship this in a browser bundle. This SDK authenticates with a secret API key (dcs_ak_…). If that key reaches a browser it can be stolen and used to forge conversions. Browser tracking is a different job handled by Decisa's pixel.js. Run this only on a server you control (an API route, a worker, a webhook handler).

Install

npm install @decisa/node

Requires Node.js 18+ (for the built-in fetch).

Quickstart — record a sale

import { Decisa, cents } from '@decisa/node';

const decisa = new Decisa({ apiKey: process.env.DECISA_API_KEY! });

const result = await decisa.conversions.record({
  type: 'sale',
  externalId: 'order_1001', // your order id — also the idempotency key
  valueCents: cents(199.9), // 199.90 -> 19990 integer cents
  currency: 'BRL',
  method: 'pix',
  customerEmail: 'buyer@example.com', // hashed server-side; raw is never stored
});

console.log(result.data.id, result.data.attributed);

valueCents is integer cents19990, not 199.90. Use the cents() helper to convert from major units; it rounds half-up. Passing a decimal to valueCents throws before any network call:

import { cents } from '@decisa/node';

cents(199.9); // 19990
cents(19.99); // 1999

await decisa.conversions.record({ type: 'sale', externalId: 'o1', valueCents: 19.99 });
// throws DecisaError: `valueCents` must be an integer number of cents...

Configuration

const decisa = new Decisa({
  apiKey: process.env.DECISA_API_KEY!, // required, starts with dcs_ak_
  baseUrl: 'https://api.decisa.ai', // default
  timeoutMs: 10_000, // default per-request timeout
  maxRetries: 2, // default retry attempts for transient failures
});

Record an event

Events are top-of-funnel and signal events (PageView, AddToCart, Purchase, …). Pass an array to pixelKeys and the SDK joins it to a comma-separated string on the wire automatically (events differ from conversions, which take an array):

await decisa.events.record({
  eventName: 'Purchase',
  eventId: 'evt_7f3a91c0', // 8–64 chars; the idempotency key for events
  valueCents: cents(49.0),
  currency: 'USD',
  url: 'https://shop.example.com/checkout/complete',
  pixelKeys: ['px_main', 'px_secondary'], // -> "px_main,px_secondary"
});

occurredAt is optional but recommended when you record after the fact — use ISO-8601 with an explicit UTC offset (2025-06-01T12:00:00Z).

Match signals

Both conversions.record and events.record accept the same optional click-id and cookie match signals to improve match quality downstream (fbclid, gclid, gbraid, wbraid, ttclid, msclkid, twclid, epik, fbp, fbc, ttp, ip, userAgent, utmSource). Forward whatever you captured at the edge:

await decisa.conversions.record({
  type: 'sale',
  externalId: 'order_2002',
  valueCents: cents(99.0),
  currency: 'USD',
  fbclid: req.query.fbclid,
  gclid: req.query.gclid,
  ip: req.ip,
  userAgent: req.headers['user-agent'],
});

Errors

Every failure maps to a typed error. Catch the base DecisaError to handle anything, or narrow to a subclass:

import {
  DecisaError,
  DecisaValidationError,
  DecisaAuthError,
  DecisaForbiddenError,
  DecisaRateLimitError,
  DecisaServerError,
} from '@decisa/node';

try {
  await decisa.conversions.record({ type: 'sale', externalId: 'order_1001', valueCents: 1999 });
} catch (err) {
  if (err instanceof DecisaValidationError) {
    // 422 — per-field messages: { external_id: ["..."] }
    console.error(err.fields);
  } else if (err instanceof DecisaAuthError) {
    // 401 — key missing / malformed / unknown / revoked / expired
  } else if (err instanceof DecisaForbiddenError) {
    // 403 — err.code is "INSUFFICIENT_SCOPE" or "ENTITLEMENT_MISSING"
    if (err.code === 'ENTITLEMENT_MISSING') {
      // workspace lacks the attribution capability
    }
  } else if (err instanceof DecisaRateLimitError) {
    // 429 — err.retryAfter (seconds) when the server provided Retry-After
  } else if (err instanceof DecisaServerError) {
    // 5xx — transient (already retried by the SDK before throwing)
  } else if (err instanceof DecisaError) {
    // network/timeout/other
  }
}

Every error carries httpStatus, code, message, and requestId (when the response provides one). The API key is never included in any error message or in console.log / util.inspect output.

Retries

The SDK retries automatically — but only on transient failures: network errors, 5xx, and 429. Deterministic 4xx errors (401, 403, 422) are never retried. Backoff is exponential with jitter; on a 429 the SDK honors the Retry-After header when present. Tune with maxRetries (default 2) and timeoutMs (default 10000).

Idempotency

externalId (conversions) and eventId (events) are idempotency keys. The server dedupes on them, so re-sending the same id is safe — it returns the stored record and still responds with a success. A duplicate is not an error. This is what makes retries safe: if you're unsure whether a request landed, just send it again with the same id.

// Both calls return the same conversion; the second is a no-op server-side.
await decisa.conversions.record({ type: 'sale', externalId: 'order_1001', valueCents: 1999 });
await decisa.conversions.record({ type: 'sale', externalId: 'order_1001', valueCents: 1999 });

camelCase → snake_case mapping

You write camelCase; the SDK sends snake_case. You never construct the wire body yourself.

SDK input Wire field
externalId external_id
eventId event_id
eventName event_name
valueCents value_cents
customerEmail customer_email
pixelKey pixel_key
pixelKeys pixel_keys
clickId click_id
occurredAt occurred_at
refundsExternalId refunds_external_id
isTest is_test
userAgent user_agent
utmSource utm_source

pixelKeys is an array for conversions but a comma-separated string on the events wire — pass an array in both cases and the SDK handles the difference.

License

MIT

About

Official server-side Node.js SDK for the Decisa ingest API (conversions + events)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors