121 lines
2.9 KiB
Markdown
121 lines
2.9 KiB
Markdown
# 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 granted
|
|
- `checkout.abandoned` - Cart abandoned (triggers recovery emails if enabled)
|
|
|
|
### Subscription Events
|
|
- `subscription.created` - New subscription started
|
|
- `subscription.updated` - Changes to quantity, product, status
|
|
- `subscription.paused` - Subscription paused
|
|
- `subscription.resumed` - Subscription resumed
|
|
- `subscription.cancelled` - Cancellation scheduled/completed
|
|
- `subscription.renewed` - Successful renewal charge
|
|
|
|
### Payment Events
|
|
- `payment.succeeded` - Charge successful
|
|
- `payment.failed` - Charge failed
|
|
- `refund.created` - Refund processed
|
|
- `chargeback.created` - Dispute opened
|
|
|
|
### License Events
|
|
- `license.activated` - Device activated against license
|
|
- `license.deactivated` - Device deactivated
|
|
|
|
## Webhook Payload Structure
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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.
|