Guide to Creem Webhooks: Features and Best Practices
Creem is a merchant of record for software companies: it sells your SaaS plans, licences and usage credits, handles tax and compliance as the legal seller, and pays you out. Because Creem owns the checkout and the billing cycle, your product only learns that someone has paid, renewed, fallen behind or asked for a refund through its webhooks. They are what turns a completed checkout into an active account, a failed renewal into a payment reminder, and a dispute into an alert for your team.
This guide covers Creem's checkout, subscription and credit events, which of them should drive access to your product, how to verify creem-signature, and how Creem's retry window works.
What are Creem webhooks?
You register one or more HTTPS endpoints with Creem, each optionally limited to a list of event types. When an event happens, Creem sends a JSON POST whose body has an event id (evt_...), the eventType, a created_at time in milliseconds and the affected object. Test mode and live mode are separate, with their own dashboards, endpoints and secrets.
Every delivery is signed with the endpoint's secret: creem-signature holds a bare hex HMAC-SHA256 of the raw body. Creem doesn't send a timestamp, so the body is all that's signed.
Creem webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard Developers tab, or POST /v1/webhooks (SDK, CLI, MCP) |
| Subscription | events list per endpoint. An empty list means every type |
| Signature | hex HMAC-SHA256 |
| Signature header | creem-signature |
| Signed content | The raw request body |
| Secret | One per endpoint, separate for test and live mode |
| Body | JSON |
| Event routing | Body field eventType |
| Acknowledgement | HTTP 200. No timeout is documented |
| Retries | 5 attempts: initial, then after 30 s, 5 min, 30 min and 6 h. None after 24 hours |
| Manual resend | From the Developers section of the dashboard |
| Source IPs | None. Creem has no static egress IPs |
Events and payloads
eventType | Triggered When | Common Use |
|---|---|---|
checkout.completed | A checkout finishes (carries license_keys when the product issues them) | Fulfil the order |
subscription.active | A new subscription is created and its payment collected | Synchronisation only; Creem advises subscription.paid for access |
subscription.paid | A subscription payment succeeds | Grant or extend access |
subscription.canceled | The merchant or customer cancelled the subscription | Revoke access |
subscription.scheduled_ | Cancellation is scheduled for the end of the billing period | Retention offer |
subscription.past_due | A payment attempt failed; Creem retries it | Payment-recovery prompt |
subscription.unpaid | The subscription moved to unpaid after failed collection | Suspend access per policy |
subscription.expired | current_end_period was reached without a new payment | Revoke access (retries can still happen until canceled) |
subscription.update | The subscription was changed | Sync |
subscription.trialing | The subscription entered a trial | Sync |
subscription.paused | The subscription was paused | Sync |
refund.created | A refund is created | Bookkeeping |
dispute.created | A dispute is opened | Alert your team |
credits.granted | Credits were added to a customer's credit account | Balance sync |
credits.consumed | Credits were debited from a customer's account | Low-balance warnings |
credits.auto_recharged | An empty account was topped up automatically (also fires credits.granted) | Billing sync |
customer_ | Credits ran out while settling a prepaid usage price | Upsell, throttling |
Creem draws a line between subscription.active and subscription.paid. active fires once, when a new subscription is created and its first payment collected, and Creem recommends it only for synchronisation. paid fires for every successful payment, including renewals, and is the event Creem advises for granting or extending access. A failed renewal produces past_due, then possibly unpaid, and expired once the period ends without payment. Creem notes that payment retries can still happen after expired until the subscription is canceled, so don't delete a customer's data on expired.
The credit events are for usage-based products. credits.auto_recharged also fires credits.granted, so count balance changes from one or the other, not both.
Event Payload Structure
idis the event id (evt_...); use it to deduplicate.eventTypenames the event; no header carries it.created_atis a Unix time in milliseconds.objectis the resource; its ownobjectkey names the kind, such ascheckoutorsubscription.- Credit amounts (
amount_minor_units,balance_) are strings, so parse them as integers in minor units.after_ minor_ units
Setting up Creem webhooks
In the dashboard, open the Developers tab and add a webhook with your HTTPS URL. Copy the secret from Developers > Webhook into WEBHOOK_SECRET and treat it as a plain string key.
To create endpoints from code, use POST /v1/webhooks, which the TypeScript SDK, the CLI and the MCP server all wrap. API keys need the webhooks:write scope:
creem webhooks create --url https://example.com/webhooks/creem \
--event checkout.completed --event subscription.paid --json
The API returns the secret when the endpoint is created, and you can fetch it again with creem.webhooks.getSecret(id) or creem webhooks secret <id> --json. Endpoints can be disabled without being deleted, which is handy while you fix a broken receiver.
Creem says web application firewalls and bot-protection products may challenge its automated requests. If deliveries are blocked, add a skip rule for the webhook route.
To receive test-mode deliveries on your machine, forward them with the Hookdeck CLI:
npx hookdeck-cli listen 3000 creem --path /webhooks/creem
Securing Creem webhooks
Compute an HMAC-SHA256 of the raw body with the endpoint secret, encode it as hex and compare it with creem-signature in constant time. Creem's TypeScript snippet is createHmac('sha256', secret).update(payload).digest('hex'). See the verification documentation.
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['creem-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler verifies, skips events it has already processed, and grants or removes access according to Creem's own guidance on which events to trust.
const express = require('express');
function createCreemApp({ alreadyHandled, access, notify }) {
const app = express();
app.post('/webhooks/creem', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
let parsed;
try {
parsed = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
const { id, eventType, object } = parsed;
if (!id || typeof eventType !== 'string') return res.sendStatus(400);
try {
if (await alreadyHandled(id)) return res.sendStatus(200);
switch (eventType) {
case 'checkout.completed':
case 'subscription.paid':
await access.grant(object);
break;
case 'subscription.canceled':
await access.revoke(object);
break;
case 'subscription.past_due':
case 'dispute.created':
await notify(eventType, object);
break;
default:
break; // stored by alreadyHandled, nothing else to do
}
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
No timestamp is signed, so there's no replay window to apply. The event id is the protection against repeats.
Hookdeck's Creem source type verifies creem-signature as a hex HMAC-SHA256 of the raw body with your endpoint secret, in either letter case. Hookdeck answers Creem with 200 regardless, records a failed check as failed verification and does not forward that request. No timestamp check is involved.
Make Creem webhooks production-ready. Hookdeck Event Gateway verifies every Creem delivery, keeps each payment and subscription event beyond Creem's 24-hour retry window, and lets you replay any of them into your billing code.
Creem webhook limitations and pain points
A 24-hour retry window
The Problem: A weekend outage leaves customers who paid without access.
Why It Happens: Creem makes 5 attempts, the last about 6.5 hours after the first, and doesn't retry anything after 24 hours. A longer outage loses the event unless someone resends it.
Workarounds:
- Use the dashboard's resend function for events that failed.
- Reconcile subscriptions against the Creem API after an incident.
How Hookdeck Can Help: Hookdeck accepts each event on Creem's first attempt and stores it, so the 24-hour limit no longer applies to your own downtime. You can retry or replay from Hookdeck whenever your service is back.
Choosing the wrong event for access
The Problem: Customers lose access after renewing, or keep it after their payment fails.
Why It Happens: subscription.active fires only for a new subscription, while renewals arrive as subscription.paid. Payment failure moves through past_due, unpaid and expired, and retries can still succeed after expired.
Workarounds:
- Grant and extend access on
subscription.paid, as Creem advises. - Decide your policy for
past_dueandunpaidexplicitly, and revoke oncanceled.
How Hookdeck Can Help: Hookdeck's event log shows each subscription's events in order, which makes it easy to check your state machine against what Creem actually sent.
No IP allowlist, and WAFs get in the way
The Problem: Deliveries are blocked by your own security layer, or you can't restrict the endpoint by network.
Why It Happens: Creem has no static egress IPs, and it notes that bot protection may challenge its requests.
Workarounds:
- Rely on signature verification and fail closed when the secret is missing.
- Add a WAF skip rule scoped to the webhook path.
How Hookdeck Can Help: With Hookdeck in front, only Hookdeck needs to reach your service, so your firewall rules can be written for Hookdeck rather than for Creem's changing addresses.
At-least-once delivery
The Problem: A renewal is credited twice, or two licence keys are issued.
Why It Happens: Creem delivers at least once, and a slow 200 can lead to a retry of an event you already processed.
Workarounds:
- Store the event
idin the same transaction as the change. - Answer quickly and process afterwards.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated evt_ IDs before they reach your service.
Best practices
Verify against the raw body
Read the body with express.raw({ type: '*/*' }) in Express, await request.arrayBuffer() in Next.js or await request.body() in FastAPI, and reject requests when no secret is configured, because an empty key makes the HMAC trivially forgeable.
Keep test and live apart
Test and live endpoints have different secrets. Use separate environment variables or separate Hookdeck sources so a test event never verifies against production.
Treat credit amounts as strings
amount_minor_units and balance_ arrive as strings. Parse them as integers in minor units, never as floating-point currency.
Filter at the endpoint
Give each endpoint the events its consumer needs. A licensing service may only need checkout.completed and the subscription events, while finance wants refunds and disputes.
Deduplicate and acknowledge fast
Store the id, return 200, then do the work. Our guides to webhook idempotency and asynchronous processing cover both halves.
Conclusion
Creem's webhooks are straightforward to verify: a bare hex HMAC over the body, no timestamp. The care goes into the subscription states, where paid rather than active should drive access, and into the 24-hour retry limit, which makes resends and reconciliation part of running a Creem integration.
Hookdeck Event Gateway removes that time limit for your side of the connection and keeps a replayable record of every billing event.
Get started with Hookdeck for free and handle Creem webhooks reliably in minutes.