2.9 KiB
2.9 KiB
Creem.io Webhooks
Webhook Setup
Configure webhook endpoint in Creem dashboard. Receive events at your endpoint URL.
Event Types
Checkout Events
checkout.completed- Payment successful, access grantedcheckout.abandoned- Cart abandoned (triggers recovery emails if enabled)
Subscription Events
subscription.created- New subscription startedsubscription.updated- Changes to quantity, product, statussubscription.paused- Subscription pausedsubscription.resumed- Subscription resumedsubscription.cancelled- Cancellation scheduled/completedsubscription.renewed- Successful renewal charge
Payment Events
payment.succeeded- Charge successfulpayment.failed- Charge failedrefund.created- Refund processedchargeback.created- Dispute opened
License Events
license.activated- Device activated against licenselicense.deactivated- Device deactivated
Webhook Payload Structure
{
"id": "evt_xxx",
"type": "checkout.completed",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"object": {
"id": "cs_xxx",
"customer_id": "cus_xxx",
"product_id": "prod_xxx",
"amount": 2900,
"currency": "usd",
"metadata": { "order_id": "123" }
}
}
}
Signature Verification
import crypto from 'crypto';
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express handler
app.post('/webhooks/creem', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-creem-signature'];
const payload = req.body.toString();
if (!verifyWebhook(payload, signature, process.env.CREEM_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
switch (event.type) {
case 'checkout.completed':
await handleCheckoutCompleted(event.data.object);
break;
case 'subscription.cancelled':
await handleSubscriptionCancelled(event.data.object);
break;
}
res.status(200).send('OK');
});
Idempotency
Store processed event IDs to prevent duplicate processing:
async function handleWebhook(event) {
// Check if already processed
const existing = await db.webhookEvents.findOne({ eventId: event.id });
if (existing) return { status: 'already_processed' };
// Process event
await processEvent(event);
// Mark as processed
await db.webhookEvents.create({
eventId: event.id,
type: event.type,
processedAt: new Date()
});
}
Retry Behavior
Creem retries failed webhooks (non-2xx responses). Implement idempotency to handle retries safely.
Testing Webhooks
Use test mode API keys (sk_test_) - events sent to same webhook endpoint with test data.