Documentation Index
Fetch the complete documentation index at: https://docs.creem.io/llms.txt
Use this file to discover all available pages before exploring further.
CREEM Webhooks Reference
Comprehensive guide to implementing webhook handlers for CREEM events.
Overview
Webhooks push real-time notifications about payments, subscriptions, and other events to your application. They are essential for:
- Granting access after payment
- Revoking access on cancellation
- Syncing subscription status
- Handling refunds and disputes
Setup
- Create a webhook endpoint in your application
- Register the URL in the CREEM Dashboard (Developers > Webhooks)
- Copy the webhook secret for signature verification
- Test with the test environment before going live
Network and WAF Configuration
CREEM does not provide static source IP addresses for outbound webhooks in
either production or Test Mode. If a firewall or WAF protects the webhook
endpoint, do not rely on source-IP allowlists as the authentication mechanism.
Keep the endpoint reachable over HTTPS and verify every request with the
creem-signature header.
Bot protection and WAF products can challenge webhook deliveries because
webhooks are automated server-to-server requests. If this happens, add a
route-level exception or skip rule for the webhook endpoint. On Cloudflare
specifically, Bot Fight Mode cannot be skipped with custom rules; disable it or
use Super Bot Fight Mode or Bot Management with a skip rule.
Signature Verification
CRITICAL: Always verify signatures to prevent fraud.
The signature is sent in the creem-signature header as a HMAC-SHA256 hex digest.
import crypto from 'crypto';
function verifySignature(rawBody: string, signature: string, secret: string): boolean {
const computed = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(computed, 'hex'),
Buffer.from(signature, 'hex')
);
}
Retry Policy
If your endpoint doesn’t respond with HTTP 200, CREEM retries with exponential backoff:
- Initial attempt
- 30 seconds later
- 1 minute later
- 5 minutes later
- 1 hour later
After all retries fail, the event is marked as failed. You can manually resend from the dashboard.
Event Structure
All webhook events follow this structure:
{
"id": "evt_unique_event_id",
"eventType": "event.type",
"created_at": 1728734325927,
"object": {
// Event-specific payload
}
}
Event Types
checkout.completed
Fired when a customer successfully completes a checkout. This is your primary trigger for granting access.
{
"id": "evt_5WHHcZPv7VS0YUsberIuOz",
"eventType": "checkout.completed",
"created_at": 1728734325927,
"object": {
"id": "ch_4l0N34kxo16AhRKUHFUuXr",
"object": "checkout",
"request_id": "my-request-id",
"status": "completed",
"mode": "test",
"order": {
"id": "ord_4aDwWXjMLpes4Kj4XqNnUA",
"customer": "cust_1OcIK1GEuVvXZwD19tjq2z",
"product": "prod_d1AY2Sadk9YAvLI0pj97f",
"amount": 1000,
"currency": "EUR",
"status": "paid",
"type": "recurring",
"created_at": "2024-10-12T11:58:33.097Z",
"updated_at": "2024-10-12T11:58:33.097Z"
},
"product": {
"id": "prod_d1AY2Sadk9YAvLI0pj97f",
"name": "Monthly",
"description": "Monthly plan",
"price": 1000,
"currency": "EUR",
"billing_type": "recurring",
"billing_period": "every-month",
"status": "active",
"tax_mode": "exclusive",
"tax_category": "saas"
},
"customer": {
"id": "cust_1OcIK1GEuVvXZwD19tjq2z",
"object": "customer",
"email": "customer@example.com",
"name": "John Doe",
"country": "NL"
},
"subscription": {
"id": "sub_6pC2lNB6joCRQIZ1aMrTpi",
"object": "subscription",
"product": "prod_d1AY2Sadk9YAvLI0pj97f",
"customer": "cust_1OcIK1GEuVvXZwD19tjq2z",
"status": "active",
"collection_method": "charge_automatically",
"metadata": {
"custom_data": "my custom data",
"internal_customer_id": "internal_123"
}
},
"custom_fields": [],
"metadata": {
"custom_data": "my custom data",
"internal_customer_id": "internal_123"
}
}
}
Handler Example:
async function handleCheckoutCompleted(checkout: CheckoutObject) {
const { customer, subscription, product, metadata, order } = checkout;
// 1. Find or create user
let user = await db.users.findByEmail(customer.email);
if (!user) {
user = await db.users.create({
email: customer.email,
name: customer.name,
creemCustomerId: customer.id
});
}
// 2. Grant access based on product
await db.subscriptions.create({
userId: user.id,
creemSubscriptionId: subscription?.id,
productId: product.id,
status: 'active',
metadata: metadata
});
// 3. Send welcome email
await sendWelcomeEmail(user.email, product.name);
}
subscription.active
Fired when a new subscription is created and first payment collected. Use subscription.paid for granting access instead - this is mainly for synchronization.
{
"id": "evt_6EptlmjazyGhEPiNQ5f4lz",
"eventType": "subscription.active",
"created_at": 1728734325927,
"object": {
"id": "sub_21lfZb67szyvMiXnm6SVi0",
"object": "subscription",
"status": "active",
"collection_method": "charge_automatically",
"product": {
"id": "prod_AnVJ11ujp7x953ARpJvAF",
"name": "Pro Plan",
"price": 10000,
"currency": "EUR",
"billing_type": "recurring",
"billing_period": "every-month"
},
"customer": {
"id": "cust_3biFPNt4Cz5YRDSdIqs7kc",
"email": "customer@example.com",
"name": "John Doe",
"country": "SE"
},
"created_at": "2024-09-16T19:40:41.984Z",
"updated_at": "2024-09-16T19:40:42.121Z"
}
}
subscription.paid
Fired when a subscription payment is successfully processed. This includes initial payments and renewals.
{
"id": "evt_21mO1jWmU2QHe7u2oFV7y1",
"eventType": "subscription.paid",
"created_at": 1728734327355,
"object": {
"id": "sub_6pC2lNB6joCRQIZ1aMrTpi",
"object": "subscription",
"status": "active",
"product": {
"id": "prod_d1AY2Sadk9YAvLI0pj97f",
"name": "Monthly",
"price": 1000,
"currency": "EUR",
"billing_type": "recurring",
"billing_period": "every-month"
},
"customer": {
"id": "cust_1OcIK1GEuVvXZwD19tjq2z",
"email": "customer@example.com",
"name": "John Doe",
"country": "NL"
},
"collection_method": "charge_automatically",
"last_transaction_id": "tran_5yMaWzAl3jxuGJMCOrYWwk",
"last_transaction_date": "2024-10-12T11:58:47.109Z",
"next_transaction_date": "2024-11-12T11:58:38.000Z",
"current_period_start_date": "2024-10-12T11:58:38.000Z",
"current_period_end_date": "2024-11-12T11:58:38.000Z",
"canceled_at": null,
"metadata": {
"custom_data": "my custom data"
}
}
}
Handler Example:
async function handleSubscriptionPaid(subscription: SubscriptionObject) {
// Extend access period
await db.subscriptions.update({
where: { creemSubscriptionId: subscription.id },
data: {
status: 'active',
currentPeriodEnd: new Date(subscription.current_period_end_date),
nextPaymentDate: new Date(subscription.next_transaction_date)
}
});
}
subscription.canceled
Fired when a subscription is canceled (by customer or merchant).
{
"id": "evt_2iGTc600qGW6FBzloh2Nr7",
"eventType": "subscription.canceled",
"created_at": 1728734337932,
"object": {
"id": "sub_6pC2lNB6joCRQIZ1aMrTpi",
"object": "subscription",
"status": "canceled",
"product": {
"id": "prod_d1AY2Sadk9YAvLI0pj97f",
"name": "Monthly"
},
"customer": {
"id": "cust_1OcIK1GEuVvXZwD19tjq2z",
"email": "customer@example.com"
},
"current_period_start_date": "2024-10-12T11:58:38.000Z",
"current_period_end_date": "2024-11-12T11:58:38.000Z",
"canceled_at": "2024-10-12T11:58:57.813Z",
"metadata": {}
}
}
Handler Example:
async function handleSubscriptionCanceled(subscription: SubscriptionObject) {
// Revoke access at period end (not immediately)
await db.subscriptions.update({
where: { creemSubscriptionId: subscription.id },
data: {
status: 'canceled',
canceledAt: new Date(subscription.canceled_at),
// Keep access until period ends
accessUntil: new Date(subscription.current_period_end_date)
}
});
// Send cancellation confirmation
await sendCancellationEmail(subscription.customer.email);
}
subscription.expired
Fired when the billing period ends without successful payment. Retries may still happen.
{
"id": "evt_V5CxhipUu10BYonO2Vshb",
"eventType": "subscription.expired",
"created_at": 1734463872058,
"object": {
"id": "sub_7FgHvrOMC28tG5DEemoCli",
"object": "subscription",
"status": "active",
"product": {
"id": "prod_3ELsC3Lt97orn81SOdgQI3",
"name": "Annual Plan",
"price": 1200,
"billing_period": "every-year"
},
"customer": {
"id": "cust_3y4k2CELGsw7n9Eeeiw2hm",
"email": "customer@example.com"
},
"current_period_end_date": "2024-12-16T12:39:47.000Z"
}
}
Note: Status remains “active” during retry period. Only act on subscription.canceled for terminal state.
subscription.trialing
Fired when a subscription enters a trial period.
{
"id": "evt_2ciAM8ABYtj0pVueeJPxUZ",
"eventType": "subscription.trialing",
"created_at": 1739963911073,
"object": {
"id": "sub_dxiauR8zZOwULx5QM70wJ",
"object": "subscription",
"status": "trialing",
"product": {
"id": "prod_3kpf0ZdpcfsSCQ3kDiwg9m",
"name": "Pro Plan with Trial",
"price": 1100
},
"customer": {
"id": "cust_4fpU8kYkQmI1XKBwU2qeME",
"email": "customer@example.com"
},
"current_period_start_date": "2025-02-19T11:18:25.000Z",
"current_period_end_date": "2025-02-26T11:18:25.000Z",
"items": [
{
"id": "sitem_1xbHCmIM61DHGRBCFn0W1L",
"product_id": "prod_3kpf0ZdpcfsSCQ3kDiwg9m",
"units": 1
}
]
}
}
subscription.paused
Fired when a subscription is paused.
{
"id": "evt_5veN2cn5N9Grz8u7w3yJuL",
"eventType": "subscription.paused",
"created_at": 1754041946898,
"object": {
"id": "sub_3ZT1iYMeDBpiUpRTqq4veE",
"object": "subscription",
"status": "paused",
"product": {
"id": "prod_sYwbyE1tPbsqbLu6S0bsR",
"name": "Monthly Plan",
"price": 2000
},
"customer": {
"id": "cust_4fpU8kYkQmI1XKBwU2qeME",
"email": "customer@example.com"
},
"current_period_end_date": "2025-09-01T09:51:47.000Z"
}
}
subscription.update
Fired when a subscription is modified (seats changed, upgraded, etc.).
{
"id": "evt_5pJMUuvqaqvttFVUvtpY32",
"eventType": "subscription.update",
"created_at": 1737890536421,
"object": {
"id": "sub_2qAuJgWmXhXHAuef9k4Kur",
"object": "subscription",
"status": "active",
"product": {
"id": "prod_1dP15yoyogQe2seEt1Evf3",
"name": "Monthly Sub",
"price": 1000
},
"customer": {
"id": "cust_2fQZKKUZqtNhH2oDWevQkW",
"email": "customer@example.com"
},
"items": [
{
"id": "sitem_3QWlqRbAat2eBRakAxFtt9",
"product_id": "prod_5jnudVkLGZWF4AqMFBs5t5",
"units": 1
}
],
"current_period_end_date": "2025-02-26T11:20:36.000Z"
}
}
refund.created
Fired when a refund is processed.
{
"id": "evt_61eTsJHUgInFw2BQKhTiPV",
"eventType": "refund.created",
"created_at": 1728734351631,
"object": {
"id": "ref_3DB9NQFvk18TJwSqd0N6bd",
"object": "refund",
"status": "succeeded",
"refund_amount": 1210,
"refund_currency": "EUR",
"reason": "requested_by_customer",
"transaction": {
"id": "tran_5yMaWzAl3jxuGJMCOrYWwk",
"amount": 1000,
"amount_paid": 1210,
"status": "refunded"
},
"subscription": {
"id": "sub_6pC2lNB6joCRQIZ1aMrTpi",
"status": "canceled"
},
"customer": {
"id": "cust_1OcIK1GEuVvXZwD19tjq2z",
"email": "customer@example.com"
},
"created_at": 1728734351525
}
}
Handler Example:
async function handleRefund(refund: RefundObject) {
// Check if this requires access revocation
if (refund.subscription?.status === 'canceled') {
await db.subscriptions.update({
where: { creemSubscriptionId: refund.subscription.id },
data: {
status: 'refunded',
accessUntil: new Date() // Immediate revocation
}
});
}
// Log refund for accounting
await db.refunds.create({
transactionId: refund.transaction.id,
amount: refund.refund_amount,
currency: refund.refund_currency,
reason: refund.reason
});
}
dispute.created
Fired when a chargeback/dispute is opened.
{
"id": "evt_6mfLDL7P0NYwYQqCrICvDH",
"eventType": "dispute.created",
"created_at": 1750941264812,
"object": {
"id": "disp_6vSsOdTANP5PhOzuDlUuXE",
"object": "dispute",
"amount": 1331,
"currency": "EUR",
"transaction": {
"id": "tran_4Dk8CxWFdceRUQgMFhCCXX",
"status": "chargeback"
},
"subscription": {
"id": "sub_5sD6zM482uwOaEoyEUDDJs",
"status": "active"
},
"customer": {
"id": "cust_OJPZd2GMxgo1MGPNXXBSN",
"email": "customer@example.com"
},
"created_at": 1750941264728
}
}
Complete Webhook Handler
Here’s a complete TypeScript webhook handler with all event types:
import crypto from 'crypto';
interface WebhookEvent {
id: string;
eventType: string;
created_at: number;
object: any;
}
export async function handleCreemWebhook(req: Request): Promise<Response> {
// 1. Get signature and raw body
const signature = req.headers.get('creem-signature');
const rawBody = await req.text();
if (!signature) {
return new Response('Missing signature', { status: 401 });
}
// 2. Verify signature
const secret = process.env.CREEM_WEBHOOK_SECRET!;
const computed = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(computed, 'hex'), Buffer.from(signature, 'hex'))) {
return new Response('Invalid signature', { status: 401 });
}
// 3. Parse event
const event: WebhookEvent = JSON.parse(rawBody);
try {
// 4. Handle event
switch (event.eventType) {
case 'checkout.completed':
await handleCheckoutCompleted(event.object);
break;
case 'subscription.active':
await handleSubscriptionActive(event.object);
break;
case 'subscription.paid':
await handleSubscriptionPaid(event.object);
break;
case 'subscription.canceled':
await handleSubscriptionCanceled(event.object);
break;
case 'subscription.expired':
await handleSubscriptionExpired(event.object);
break;
case 'subscription.trialing':
await handleSubscriptionTrialing(event.object);
break;
case 'subscription.paused':
await handleSubscriptionPaused(event.object);
break;
case 'subscription.update':
await handleSubscriptionUpdate(event.object);
break;
case 'refund.created':
await handleRefundCreated(event.object);
break;
case 'dispute.created':
await handleDisputeCreated(event.object);
break;
default:
console.log(`Unhandled event type: ${event.eventType}`);
}
return new Response('OK', { status: 200 });
} catch (error) {
console.error('Webhook handler error:', error);
// Return 500 to trigger retry
return new Response('Internal error', { status: 500 });
}
}
Next.js Adapter
If using the @creem_io/nextjs package:
// app/api/webhook/creem/route.ts
import { Webhook } from '@creem_io/nextjs';
export const POST = Webhook({
webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
onCheckoutCompleted: async ({ customer, product, subscription, metadata }) => {
console.log(`${customer.email} purchased ${product.name}`);
// Grant access
},
onGrantAccess: async ({ customer, metadata }) => {
const userId = metadata?.referenceId as string;
await grantAccess(userId, customer.email);
},
onRevokeAccess: async ({ customer, metadata }) => {
const userId = metadata?.referenceId as string;
await revokeAccess(userId, customer.email);
},
});
Best Practices
- Always verify signatures - Never process unverified webhooks
- Return 200 quickly - Process asynchronously if needed
- Be idempotent - Handle duplicate deliveries gracefully
- Log events - Keep records for debugging
- Handle all relevant events - Don’t miss critical state changes
- Test in sandbox - Verify handlers before production
- Monitor failures - Set up alerts for webhook failures