Files
english/.opencode/skills/payment-integration/references/creem/webhooks.md
2026-04-12 01:06:31 +07:00

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.