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

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 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

{
  "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.