creem-datafast
v0.2.0
Published
Wraps the official Creem SDK to capture DataFast visitor attribution at checkout and forward verified payment and refund webhooks — with built-in Next.js and Express adapters.
Downloads
189
Maintainers
Readme
creem-datafast
Connect Creem payments to DataFast analytics without writing any glue code. One factory, automatic cookie capture, webhook forwarding.
Table of Contents
Zero glue code — one factory call wires up checkout attribution and webhook forwarding
Framework adapters — Next.js App Router and Express 5 out of the box, or bring your own
Production-ready — idempotent webhooks, retries with backoff, Web Crypto signature verification
Official Upstash adapter — ready-made distributed idempotency for serverless and multi-instance deployments
Refund support — forwards
refund.createdasrefunded: truepayment eventsCurrency-aware — correctly converts zero-decimal (JPY) and three-decimal (KWD) currencies
How It Works
sequenceDiagram
participant B as Browser
participant S as Your Server
participant C as Creem
participant D as DataFast
B->>S: POST /api/checkout (cookies)
Note right of S: reads datafast_visitor_id<br/>injects into metadata
S->>C: createCheckout()
C-->>B: redirect to checkoutUrl
B->>C: completes payment
C->>S: webhook (checkout.completed)
Note right of S: verifies signature<br/>deduplicates event<br/>maps payload
S->>D: POST /api/v1/payments- Your backend calls
createCheckout()with the incomingRequestor cookie header. - The package injects
datafast_visitor_idanddatafast_session_idinto Creem metadata without dropping the rest of your metadata. - Creem redirects the customer to
checkoutUrl. - Creem sends
checkout.completed,subscription.paid, andrefund.createdwebhooks back to your server. handleWebhook()verifiescreem-signature, deduplicates the event id, maps the payload, and forwards the payment or refund to DataFast.
Supported events: checkout.completed, subscription.paid, refund.created. Any other Creem event is ignored and returns 200 OK. Initial subscription checkout.completed deliveries are acknowledged but ignored so the first subscription payment is attributed only once through subscription.paid.
Judge in 2 minutes
Fastest local proof: real checkout creation against Creem test mode, plus a signed webhook fixture replay through the full pipeline (signature verification, idempotency, mapping, DataFast forwarding).
pnpm install
cp example-next/.env.example example-next/.env.localFill example-next/.env.local with real test values for CREEM_API_KEY, CREEM_WEBHOOK_SECRET, DATAFAST_API_KEY, DATAFAST_WEBSITE_ID, and CREEM_PRODUCT_ID. APP_BASE_URL can stay at http://localhost:3000 for a local run.
pnpm build
pnpm --filter example-next devThen:
- Open
http://localhost:3000. - Click
Launch checkout via server cookie captureto see the hosted checkout flow the package creates. - In another terminal, replay a signed webhook fixture through the full pipeline (Creem doesn't deliver to localhost without a tunnel — see the example READMEs for the ngrok setup if you want live delivery):
export CREEM_WEBHOOK_SECRET=your_real_webhook_secret
curl -i http://localhost:3000/api/webhook/creem \
-H "content-type: application/json" \
-H "creem-signature: $(node --input-type=module -e 'import { createHmac } from \"node:crypto\"; import { readFileSync } from \"node:fs\"; const rawBody = readFileSync(\"tests/fixtures/checkout-completed.json\", \"utf8\"); process.stdout.write(createHmac(\"sha256\", process.env.CREEM_WEBHOOK_SECRET).update(rawBody).digest(\"hex\"));')" \
--data-binary @tests/fixtures/checkout-completed.jsonYou should see:
HTTP/1.1 200 OKfrom the webhook route[example-next] forwarding payload to DataFast ...[example-next] webhook processed ...
If you prefer Express, swap the env file and dev command:
cp example-express/.env.example example-express/.env.local
pnpm --filter example-express devUse the same fixture and curl command against http://localhost:3000/api/webhook/creem. The key success signal there is [example-express] forwarding payload to DataFast ....
For the longer setup, tunnel, and verification flow, see example-next/README.md, example-express/README.md, and docs/development.md.
Integrate with AI Agents
Paste this prompt into Claude Code, Cursor, Codex, or any AI coding agent:
Use curl to download, read and follow: https://raw.githubusercontent.com/santigamo/creem-datafast/main/SKILL.mdInstallation
pnpm add creem-datafastInternally the package wraps the official creem Core SDK, so you do not need to install creem separately in a normal consumer app.
Quickstart
Next.js
Install the package, create a shared client, then use the included route handler adapter.
// lib/creem-datafast.ts
import { createCreemDataFast } from "creem-datafast";
export const creemDataFast = createCreemDataFast({
creemApiKey: process.env.CREEM_API_KEY!,
creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
datafastApiKey: process.env.DATAFAST_API_KEY!,
testMode: true
});// app/api/checkout/route.ts
import { NextResponse } from "next/server";
import { creemDataFast } from "@/lib/creem-datafast";
export const runtime = "nodejs";
export async function POST(request: Request) {
const { checkoutUrl } = await creemDataFast.createCheckout(
{
productId: process.env.CREEM_PRODUCT_ID!,
successUrl: `${process.env.APP_BASE_URL!}/success`
},
{ request }
);
return NextResponse.redirect(checkoutUrl, { status: 303 });
}// app/api/webhook/creem/route.ts
import { createNextWebhookHandler } from "creem-datafast/next";
import { creemDataFast } from "@/lib/creem-datafast";
export const runtime = "nodejs";
export const POST = createNextWebhookHandler(creemDataFast);Express
Use the framework-agnostic core in your app layer and keep the webhook route on raw body middleware.
import express from "express";
import { createCreemDataFast } from "creem-datafast";
import { createExpressWebhookHandler } from "creem-datafast/express";
const app = express();
const creemDataFast = createCreemDataFast({
creemApiKey: process.env.CREEM_API_KEY!,
creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
datafastApiKey: process.env.DATAFAST_API_KEY!,
testMode: true
});
app.post("/api/checkout", async (req, res) => {
const { checkoutUrl } = await creemDataFast.createCheckout(
{
productId: process.env.CREEM_PRODUCT_ID!,
successUrl: `${process.env.APP_BASE_URL!}/success`
},
{
request: { headers: req.headers, url: req.url }
}
);
res.redirect(303, checkoutUrl);
});
app.post(
"/api/webhook/creem",
express.raw({ type: "application/json" }),
createExpressWebhookHandler(creemDataFast)
);Framework-Agnostic
Use handleWebhook() directly when your framework is not Next.js or Express. You just need the raw request body as a string and the request headers.
import { createCreemDataFast, InvalidCreemSignatureError } from "creem-datafast";
const creemDataFast = createCreemDataFast({
creemApiKey: process.env.CREEM_API_KEY!,
creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
datafastApiKey: process.env.DATAFAST_API_KEY!,
testMode: true
});
// Works with any Node.js framework, and the core flow is smoke-validated on Cloudflare Workers with injected Creem/DataFast boundaries.
async function handleCreemWebhook(rawBody: string, headers: Record<string, string>) {
try {
const result = await creemDataFast.handleWebhook({ rawBody, headers });
if (result.ignored) {
return { status: 200, body: "Ignored" };
}
return { status: 200, body: "OK" };
} catch (error) {
if (error instanceof InvalidCreemSignatureError) {
return { status: 400, body: "Invalid signature" };
}
return { status: 500, body: "Internal error" };
}
}Client-Side Helper
Use the browser helper when your checkout request originates from the browser and cookies are not automatically forwarded to your backend (e.g. cross-origin fetch calls). In same-origin setups the server-side cookie capture handles this automatically.
import { appendDataFastTracking, getDataFastTracking } from "creem-datafast/client";
const tracking = getDataFastTracking();
const checkoutEndpoint = appendDataFastTracking("/api/checkout", tracking);
// Then use checkoutEndpoint as your fetch URL:
const response = await fetch(checkoutEndpoint, { method: "POST" });Tracking precedence during checkout creation is:
params.trackingparams.metadata.datafast_*request.urlquery params- cookies, using
request.headers.cookiefirst andcookieHeaderonly to fill missing tracking fields
Advanced
Custom webhook response logic (Next.js)
If you need custom response logic in Next.js, use handleWebhookRequest() instead of createNextWebhookHandler(). It reads the raw body for you and forwards the webhook through the same core path. Since handleWebhookRequest() is a low-level helper, you are responsible for catching InvalidCreemSignatureError (-> 400) and unexpected errors (-> 500).
import { handleWebhookRequest } from "creem-datafast/next";
import { InvalidCreemSignatureError } from "creem-datafast";
import { creemDataFast } from "@/lib/creem-datafast";
export const runtime = "nodejs";
export async function POST(request: Request) {
try {
const result = await handleWebhookRequest(creemDataFast, request);
if (result.ignored) {
return new Response("Ignored", { status: 200 });
}
return new Response("OK", { status: 200 });
} catch (error) {
if (error instanceof InvalidCreemSignatureError) {
return new Response("Invalid signature", { status: 400 });
}
return new Response("Internal error", { status: 500 });
}
}Idempotency
handleWebhook() uses an in-process MemoryIdempotencyStore by default. This is convenient for local development and single-instance deployments, but it is not safe for multi-instance production environments because deduplication does not survive process restarts or span multiple instances.
Recommended production setup:
pnpm add @upstash/redisimport { Redis } from "@upstash/redis";
import { createCreemDataFast } from "creem-datafast";
import { createUpstashIdempotencyStore } from "creem-datafast/idempotency/upstash";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!
});
export const creemDataFast = createCreemDataFast({
creemApiKey: process.env.CREEM_API_KEY!,
creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
datafastApiKey: process.env.DATAFAST_API_KEY!,
idempotencyStore: createUpstashIdempotencyStore(redis)
});See docs/production-idempotency.md for the IdempotencyStore contract, TTL guidance, and how to implement a custom store.
Dead-letter callback
When forwarding to DataFast fails after all retries, onDeadLetter fires before the error is re-thrown. Use it to persist failed events for later inspection or replay.
const creemDataFast = createCreemDataFast({
creemApiKey: process.env.CREEM_API_KEY!,
creemWebhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
datafastApiKey: process.env.DATAFAST_API_KEY!,
onDeadLetter: async ({ eventId, eventType, payload, error }) => {
console.error("Webhook forwarding failed", { eventId, eventType, error });
await db.deadLetters.insert({ eventId, eventType, payload, failedAt: new Date() });
}
});The idempotency claim is released after onDeadLetter runs, so Creem can redeliver the event.
Health check
healthCheck() runs non-destructive probes to verify your configuration before going live.
const health = await creemDataFast.healthCheck();
if (!health.ok) {
console.error("Integration not ready:", health.checks);
process.exit(1);
}The Creem check validates that the SDK client is configured (no cheap remote ping is available through the SDK surface). The DataFast check sends an authenticated probe to verify reachability and API key acceptance.
Configuration
Constructor options for createCreemDataFast():
creemApiKey: Creem Core SDK API key.creemWebhookSecret: secret used to validatecreem-signature.datafastApiKey: bearer token for DataFast payments.testMode: set totrueto targethttps://test-api.creem.io. Defaults tofalse.timeoutMs: per-request timeout for DataFast forwarding. Defaults to8000.retry.retries: additional retry attempts after the initial DataFast request, so1means up to2total attempts. Defaults to1.retry.baseDelayMs: base backoff delay in milliseconds. Defaults to250.retry.maxDelayMs: maximum backoff delay in milliseconds. Defaults to2000.strictTracking: throwMissingTrackingErrorwhen nodatafast_visitor_idis found at checkout. Defaults tofalse.idempotencyStore: customIdempotencyStorefor distributed deduplication.logger: inject a custom logger implementing{ debug, info, warn, error }.creemClient: inject a pre-configured Creem SDK instance instead of usingcreemApiKey.onDeadLetter: callback invoked when DataFast forwarding fails after retries, before the webhook error is re-thrown.captureSessionId: also capturedatafast_session_idfrom cookies and query parameters. Defaults totrue.hydrateTransactionOnSubscriptionPaid: fetch the full Creem transaction forsubscription.paidwebhooks to get exact amount and timestamp. Falls back to product pricing on failure. Defaults totrue.idempotencyInFlightTtlSeconds: seconds before an in-flight webhook claim expires, allowing redelivery. Defaults to300.idempotencyProcessedTtlSeconds: seconds before a completed webhook record expires. Defaults to86400.webhookDryRun: map and log webhook payloads without POSTing them to DataFast. Intended for local and staging verification. Defaults tofalse.fetch: inject a customfetchimplementation for DataFast requests.
API Reference
import {
createCreemDataFast,
CreemDataFastError,
DataFastRequestError,
InvalidCreemSignatureError,
MissingTrackingError,
MemoryIdempotencyStore
} from "creem-datafast";
import { createNextWebhookHandler, handleWebhookRequest } from "creem-datafast/next";
import { createExpressWebhookHandler } from "creem-datafast/express";
import { appendDataFastTracking, getDataFastTracking } from "creem-datafast/client";
import { createUpstashIdempotencyStore } from "creem-datafast/idempotency/upstash";Root API:
createCreemDataFast(options)— returns aCreemDataFastClient.client.createCheckout(params, context?)— creates a Creem checkout with injected DataFast tracking.client.handleWebhook({ rawBody, headers })— verifies, deduplicates, maps, and forwards a webhook.client.verifyWebhookSignature(rawBody, headers)— returnstrueorfalsefor signature validity; throwsInvalidCreemSignatureErrorwhencreem-signatureis missing.client.healthCheck()— runs non-destructive connectivity checks for the configured Creem/DataFast integration.
Error classes:
CreemDataFastError— base class for all package errors.InvalidCreemSignatureError— webhook signature is missing or invalid.MissingTrackingError— thrown bycreateCheckout()whenstrictTrackingis enabled and nodatafast_visitor_idis found.DataFastRequestError— DataFast API request failed. Exposes.retryable,.status, and.requestId.
Troubleshooting
- Invalid webhook signature: make sure the handler reads the raw request body, not parsed JSON.
- Missing
creem-signatureheader:verifyWebhookSignature()andhandleWebhook()throwInvalidCreemSignatureErrorbecause the request is malformed. - Missing visitor tracking: the checkout still works by default; enable
strictTrackingif you want the request to fail instead. - Double-counted revenue from DataFast: if you use the DataFast tracking script alongside server-side webhook forwarding, the same payment can be recorded twice — once by the script detecting URL parameters on the success page, and once by the webhook. Add
data-disable-payments="true"to the DataFast script tag when usingcreem-datafastfor server-side attribution. - Wrong amount format: Creem amounts are interpreted as minor units and converted into decimal major units before sending to DataFast.
- Refund semantics:
refund.createdforwards the refunded amount as a new DataFast payment withrefunded: trueand uses the Creem refund id astransaction_id. - Duplicate forwards: the built-in
MemoryIdempotencyStoreisdev / single-instance only. For multi-instance deployments, pass a durable atomicidempotencyStoresuch ascreateUpstashIdempotencyStore(redis). Seedocs/production-idempotency.md. - Slow or flaky DataFast responses: forwarding uses an
8000mstimeout by default and retries only network errors, timeouts, and408/429/5xxresponses. - Webhook dry-runs:
webhookDryRun: trueskips the DataFast POST and releases the idempotency claim so the same event can be replayed repeatedly while testing. Inspect the normalized payload through your injected logger. - Health checks:
healthCheck()can verify that DataFast is reachable and the API key is accepted, but Creem currently exposes no cheap remote ping through the injected SDK surface, so the Creem result is limited to configuration validation.
Compatibility
- Node 18+ runtime. ESM-only (
import, notrequire()). - Framework-agnostic core is smoke-validated on Cloudflare Workers and Bun.
- Next.js Route Handlers on the Node runtime.
- Express webhook routes using
express.raw({ type: "application/json" }).
Development
See docs/development.md for package checks, CI pipeline, runnable examples, and manual local verification.
