creem-datafast-sdk
v0.1.0
Published
Connect CREEM payments with DataFast revenue attribution. Automatically track payment conversions from traffic sources.
Maintainers
Readme
creem-datafast-sdk
Connect CREEM payments with DataFast revenue attribution. Automatically track which traffic sources drive your revenue.
What problem this solves
When using CREEM for payments and DataFast for analytics, you need to connect the two: attribute each payment to the visitor who made it. This package handles the full pipeline:
- Client-side: Read the
datafast_visitor_idcookie set by the DataFast tracking script - Checkout: Inject the visitor ID into CREEM checkout metadata
- Webhook: Verify CREEM webhook signatures, extract payment data, and send it to DataFast's payment tracking API
Without this, you'd write the same glue code in every project.
Features
- Edge Runtime Support — Uses Web Crypto API (
crypto.subtle) instead ofnode:crypto. Works on Vercel Edge Functions, Cloudflare Workers, and Node.js 18+. - Retry with Exponential Backoff — Automatic retries (3 attempts with jitter) for transient DataFast API failures. Configurable.
- Dead Letter Callback —
onDeadLetterfires when a webhook event cannot be forwarded after all retries. Log to DB, alert Slack, or queue for replay. - Refund Support — Handles
refund.createdevents with negative amounts for accurate revenue attribution. - Idempotency with TTL — Built-in in-memory store with 7-day auto-expiry prevents memory leaks. Production-ready Upstash Redis adapter included.
- Webhook Event Filtering — Process only the events you care about. Silently acknowledge the rest.
- Dry-Run Mode — Run the full pipeline without sending to DataFast. Logs what would be sent.
- Webhook Replay — Re-process failed webhooks manually, bypassing idempotency checks.
- Transaction Hydration — Optionally fetch full order details from CREEM API for richer attribution data.
- Checkout URL Builder — Build CREEM checkout URLs with attribution baked in. No server required.
- Health Check — Verify CREEM config, webhook secret, and DataFast API reachability in one call.
- Auto-Attribute Browser Links — Automatically append visitor IDs to CREEM checkout links on the page.
- Batch Payment API — Backfill historical transactions into DataFast in bulk.
- DataFast Query SDK — Look up payments by visitor ID for debugging attribution gaps.
Install
npm install creem-datafast-sdk creemcreem is a peer dependency — you need it installed alongside this package.
Configuration
import { createCreemDataFast, InMemoryIdempotencyStore } from 'creem-datafast-sdk';
const cd = createCreemDataFast({
// Required
creemApiKey: process.env.CREEM_API_KEY!,
creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
datafastApiKey: process.env.DATAFAST_API_KEY!,
// Optional
creemServerIdx: 0, // 0 = production, 1 = sandbox
datafastApiBaseUrl: 'https://datafa.st', // default
debug: false, // enable debug logging
requestTimeoutMs: 10_000, // DataFast API timeout
requireVisitorId: false, // throw if visitor ID missing
idempotencyStore: new InMemoryIdempotencyStore(), // dedupe webhooks (7-day TTL)
onError: (err, ctx) => console.error(err, ctx), // error callback
// Retry (defaults shown; set to false to disable)
retry: {
maxAttempts: 3,
initialDelayMs: 1000,
maxDelayMs: 16000,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
},
// Dead letter callback — fires after all retries fail
onDeadLetter: async ({ eventType, eventId, transactionId, payment, error, attempts }) => {
console.error(`Dead letter: ${eventType} ${eventId} failed after ${attempts} attempts`, error);
// Save to DB, send to Slack, push to a queue, etc.
},
// Event filtering — only process these event types
eventFilter: ['checkout.completed', 'subscription.paid', 'refund.created'],
// Dry-run mode — logs what would be sent, doesn't call DataFast
dryRun: false,
// Transaction hydration — fetch full order from CREEM API before mapping
hydrateTransaction: false,
});Client-side cookie helper
In the browser, read the DataFast visitor ID cookie and include it when creating a checkout:
// Import the browser-specific bundle (no Node.js dependencies)
import {
getDataFastVisitorId,
buildCheckoutAttributionMetadata,
autoAttributeCheckoutLinks,
} from 'creem-datafast-sdk/browser';
// Read the cookie
const visitorId = getDataFastVisitorId();
// => "3cff4252-fa96-4gec-8b1b-bs695e763b65" or null
// Or build metadata that includes it automatically
const metadata = buildCheckoutAttributionMetadata({ campaign: 'spring' });
// => { campaign: 'spring', datafast_visitor_id: '3cff4252-...' }Send the visitorId to your server when creating a checkout.
Auto-attribute checkout links
Automatically append the DataFast visitor ID to all CREEM checkout links on the page:
import { autoAttributeCheckoutLinks } from 'creem-datafast-sdk/browser';
// Call after DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const count = autoAttributeCheckoutLinks();
console.log(`Attributed ${count} checkout links`);
});
// Or with a custom selector
autoAttributeCheckoutLinks('a[href*="checkout.creem.io"], a.checkout-btn');This finds all <a> tags matching the selector and appends metadata[datafast_visitor_id]=<visitor_id> as a query parameter.
Zero-config script tag
For static sites, add the data-auto-init attribute to auto-attribute links on page load:
<script src="https://unpkg.com/creem-datafast-sdk/dist/browser.js" data-auto-init></script>Checkout example
const checkout = await cd.createCheckout({
input: {
productId: 'prod_xxx',
customer: { email: '[email protected]' },
successUrl: 'https://yoursite.com/success',
metadata: { campaign: 'spring' },
},
datafastVisitorId: visitorId, // from client-side cookie
});
// checkout.checkoutUrl => redirect the customer hereThe visitor ID is stored in metadata.datafast_visitor_id. Your existing metadata keys are preserved.
Checkout URL builder (static sites)
For static sites or JAMstack apps that can't make server-side calls, build a checkout URL with attribution baked in:
import { buildCheckoutUrl } from 'creem-datafast-sdk';
const url = buildCheckoutUrl({
productId: 'prod_xxx',
datafastVisitorId: 'vis_abc123',
queryParams: { coupon: 'SPRING20' }, // optional extra params
});
// => "https://checkout.creem.io?product_id=prod_xxx&metadata[datafast_visitor_id]=vis_abc123&coupon=SPRING20"
// Redirect or use in an <a> tag
window.location.href = url;Merge strategies
If datafast_visitor_id already exists in metadata:
| Strategy | Behavior |
|----------|----------|
| "preserve" (default) | Keep the existing value |
| "overwrite" | Replace with the new value |
| "error" | Throw MetadataCollisionError |
const checkout = await cd.createCheckout({
input: { productId: 'prod_xxx', metadata: { datafast_visitor_id: 'old' } },
datafastVisitorId: 'new',
mergeStrategy: 'overwrite', // "new" wins
});Express example
import express from 'express';
import { createCreemDataFast } from 'creem-datafast-sdk';
const app = express();
const cd = createCreemDataFast({ /* config */ });
// Important: use express.raw() on the webhook route, NOT express.json()
app.post(
'/webhook/creem',
express.raw({ type: 'application/json' }),
cd.expressWebhookHandler({
onProcessed(result) {
console.log('Payment tracked:', result.transactionId);
},
onError(error) {
console.error('Webhook failed:', error);
},
})
);Next.js example
// app/api/webhook/creem/route.ts
import { createCreemDataFast } from 'creem-datafast-sdk';
const cd = createCreemDataFast({ /* config */ });
export const POST = cd.nextWebhookHandler({
onProcessed(result) {
console.log('Payment tracked:', result.transactionId);
},
});Works with both Node.js and Edge runtime thanks to the Web Crypto API.
Webhook signature verification
CREEM signs webhooks with HMAC SHA256 using the creem-signature header. This package:
- Reads the raw request body (not re-serialized JSON)
- Computes HMAC SHA256 using the Web Crypto API (
crypto.subtle) — compatible with Edge runtimes - Compares using constant-time comparison to prevent timing attacks
- Rejects with
InvalidWebhookSignatureErroron mismatch
Critical: For Express, use express.raw({ type: 'application/json' }) on the webhook route so the raw body is preserved. Using express.json() will re-serialize the body and break signature verification.
You can also use the verifier standalone:
import { verifyCreemWebhookSignature } from 'creem-datafast-sdk';
// Note: this is async (returns a Promise)
await verifyCreemWebhookSignature(rawBody, signatureHeader, webhookSecret);
// throws InvalidWebhookSignatureError if invalidEvent support matrix
| CREEM Event | DataFast Field Mapping |
|---|---|
| checkout.completed | amount, currency, transaction_id (order ID), email, name, customer_id, datafast_visitor_id (from metadata), renewal: false |
| subscription.paid | Same fields, renewal: true, timestamp from period start |
| refund.created | Same fields, negative amount, refunded: true, timestamp from refund |
Unsupported CREEM events are silently acknowledged (HTTP 200) so CREEM does not retry them.
Retry and dead letter handling
By default, DataFast API calls are retried up to 3 times with exponential backoff and jitter on transient failures (5xx, 408, 429, network errors). Non-retryable errors (4xx) fail immediately.
const cd = createCreemDataFast({
// ...
// Custom retry config
retry: {
maxAttempts: 5, // up to 5 attempts
initialDelayMs: 500, // start at 500ms
maxDelayMs: 30000, // cap at 30s
retryableStatusCodes: [429, 500, 502, 503, 504],
},
// Or disable retries entirely
// retry: false,
// Called when all retries fail — use for alerting or manual replay queues
onDeadLetter: async (ctx) => {
await db.deadLetters.insert({
eventId: ctx.eventId,
eventType: ctx.eventType,
transactionId: ctx.transactionId,
payload: ctx.payment,
error: ctx.error.message,
attempts: ctx.attempts,
failedAt: new Date(),
});
await slack.alert(`Dead letter: ${ctx.eventType} ${ctx.eventId}`);
},
});Webhook replay
Re-process a previously failed webhook. This bypasses idempotency checks so the event is processed again even if it was already recorded:
// Replay from stored raw body and headers
const result = await cd.replayWebhook({
rawBody: storedRawBody,
headers: storedHeaders,
});
console.log(result.ok, result.transactionId);Useful for ops recovery when events end up in the dead letter queue.
Dry-run mode
Test your webhook pipeline without sending data to DataFast:
const cd = createCreemDataFast({
// ...
dryRun: true,
});In dry-run mode, the full pipeline runs (signature verification, event parsing, payment mapping) but the DataFast API call is skipped. The mapped payment payload is logged instead. The handler returns { skipped: true, skipReason: 'dry_run' }.
Webhook event filtering
Only process the events you care about:
const cd = createCreemDataFast({
// ...
eventFilter: ['checkout.completed'], // ignore subscription.paid and refund.created
});Events not in the filter are silently acknowledged (HTTP 200, skipReason: 'filtered'). If eventFilter is not set, all supported events are processed.
Transaction hydration
By default, payment data is extracted from the webhook payload. Enable hydration to fetch the full order details from the CREEM API before mapping — this can provide richer data (e.g., additional metadata, updated amounts):
const cd = createCreemDataFast({
// ...
hydrateTransaction: true,
});This adds a CREEM API call to the webhook processing path, which increases latency. If the API call fails, the handler falls back to the webhook payload data and logs a warning.
Health check
Verify all integration connections in one call:
const health = await cd.healthCheck();
console.log(health);
// {
// healthy: true,
// checks: {
// creemApiKey: { ok: true, message: 'CREEM API key is configured' },
// webhookSecret: { ok: true, message: 'Webhook secret is configured' },
// datafastApi: { ok: true, message: 'DataFast API reachable (HTTP 200)', latencyMs: 142 },
// },
// timestamp: '2025-06-15T10:30:00.000Z',
// }Use this in a /health endpoint for deploy verification or monitoring dashboards.
Idempotency
CREEM retries webhook deliveries. To prevent duplicate DataFast payments, use an idempotency store:
In-memory store (with TTL)
import { createCreemDataFast, InMemoryIdempotencyStore } from 'creem-datafast-sdk';
const store = new InMemoryIdempotencyStore({
ttlMs: 7 * 24 * 60 * 60 * 1000, // 7 days (default)
cleanupIntervalMs: 60 * 60 * 1000, // cleanup every hour (default)
});
const cd = createCreemDataFast({
// ...
idempotencyStore: store,
});
// Call on shutdown to stop the cleanup timer
process.on('SIGTERM', () => store.dispose());Entries are lazily evicted on access and periodically cleaned up. The cleanup timer is unref'd so it won't prevent process exit.
Upstash Redis store (production)
import { Redis } from '@upstash/redis';
import { createCreemDataFast, UpstashIdempotencyStore } from 'creem-datafast-sdk';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
const cd = createCreemDataFast({
// ...
idempotencyStore: new UpstashIdempotencyStore({
redis,
keyPrefix: 'creem-df:idempotency:', // default
ttlSeconds: 604800, // 7 days (default)
}),
});Works with @upstash/redis or any client implementing get(key) and set(key, value, { ex }).
Custom store
Implement the IdempotencyStore interface with any backend:
import type { IdempotencyStore } from 'creem-datafast-sdk';
class PostgresIdempotencyStore implements IdempotencyStore {
async has(key: string): Promise<boolean> {
const row = await db.query('SELECT 1 FROM webhook_events WHERE id = $1', [key]);
return row.length > 0;
}
async set(key: string, value: { processedAt: string; eventType: string }): Promise<void> {
await db.query(
'INSERT INTO webhook_events (id, processed_at, event_type) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING',
[key, value.processedAt, value.eventType]
);
}
}Dedupe keys are based on the webhook event ID.
Batch payment API
Backfill historical CREEM transactions into DataFast:
import { DataFastClient } from 'creem-datafast-sdk';
const client = new DataFastClient({
apiKey: process.env.DATAFAST_API_KEY!,
baseUrl: 'https://datafa.st',
timeoutMs: 30_000,
fetch: globalThis.fetch,
});
const { results } = await client.sendPayments([
{ amount: 49.00, currency: 'USD', transaction_id: 'ord_001', datafast_visitor_id: 'vis_aaa' },
{ amount: 99.00, currency: 'USD', transaction_id: 'ord_002', datafast_visitor_id: 'vis_bbb' },
{ amount: 29.00, currency: 'EUR', transaction_id: 'ord_003' },
]);
for (const r of results) {
if (r.ok) {
console.log('Sent:', r.response?.status);
} else {
console.error('Failed:', r.error?.message);
}
}Payments are sent sequentially with retry. Each result indicates success or failure independently.
DataFast payments query
Look up payments by visitor ID to debug attribution gaps:
import { DataFastClient } from 'creem-datafast-sdk';
const client = new DataFastClient({
apiKey: process.env.DATAFAST_API_KEY!,
baseUrl: 'https://datafa.st',
timeoutMs: 10_000,
fetch: globalThis.fetch,
});
const { body } = await client.getPayments('vis_abc123');
console.log(body); // DataFast API response with matching paymentsTroubleshooting
Signature verification fails
- Ensure you're using
express.raw()notexpress.json()on the webhook route - Verify your webhook secret matches what's configured in CREEM
- Check that no middleware is modifying the request body before the webhook handler
- Note:
verifyCreemWebhookSignature()is now async — make sure youawaitit
Missing visitor ID warnings
- Ensure the DataFast tracking script is loaded on your site
- Check that the
datafast_visitor_idcookie exists before creating the checkout - The cookie is set by DataFast's client-side script, not by this package
Duplicate payments in DataFast
- Enable the idempotency store
- Use a durable store (Redis/Upstash) in production — the in-memory store resets on restart
DataFast API failures
- Check the
onDeadLettercallback for events that failed after all retries - Use
cd.replayWebhook()to re-process events from the dead letter queue - Run
cd.healthCheck()to verify DataFast API connectivity
Edge runtime errors
- This package uses the Web Crypto API — no
node:cryptodependency - Ensure your runtime supports
crypto.subtle(Node.js 18+, Vercel Edge, Cloudflare Workers, Deno)
Security notes
- Webhook signatures are verified using constant-time comparison (prevents timing attacks)
- Uses the Web Crypto API — compatible with Edge runtimes without
node:crypto - Secrets are never logged, even in debug mode
- Raw bodies are used for verification, never re-serialized JSON
- The package validates all required config at initialization time
- The DataFast API key is sent as a Bearer token over HTTPS
Error types
| Error | When |
|---|---|
| ConfigError | Missing required config fields |
| InvalidWebhookSignatureError | Bad or missing webhook signature |
| UnsupportedWebhookEventError | Event type not in supported set |
| MissingVisitorIdError | No visitor ID and requireVisitorId: true |
| MetadataCollisionError | Visitor ID exists and merge strategy is "error" |
| DataFastRequestError | DataFast API returns non-2xx after all retries (includes .status and .responseBody) |
API reference
createCreemDataFast(config) → CreemDataFastInstance
| Method | Description |
|---|---|
| createCheckout(options) | Create a CREEM checkout with DataFast attribution |
| handleWebhook(input) | Process a raw webhook event |
| replayWebhook(input) | Re-process a webhook, bypassing idempotency |
| expressWebhookHandler(options?) | Express middleware for CREEM webhooks |
| nextWebhookHandler(options?) | Next.js App Router handler for CREEM webhooks |
| buildCheckoutUrl(options) | Build a CREEM checkout URL with attribution |
| healthCheck() | Verify all integration connections |
| creem | The underlying CREEM SDK client |
DataFastClient
| Method | Description |
|---|---|
| sendPayment(payment) | Send a single payment event (with retry) |
| sendPayments(payments[]) | Send multiple payments sequentially |
| getPayments(visitorId) | Query payments by visitor ID |
Browser exports (creem-datafast-sdk/browser)
| Function | Description |
|---|---|
| getDataFastVisitorId() | Read the visitor ID cookie |
| buildCheckoutAttributionMetadata(existing?) | Build metadata with visitor ID |
| autoAttributeCheckoutLinks(selector?) | Append visitor ID to checkout links |
Development
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Watch tests
npm run test:watch
# Typecheck
npm run typecheck
# Run Express demo
cd examples/express-demo && npm install && npm run devLicense
MIT
