# Creem Webhooks

[Creem](https://creem.io) is a merchant-of-record payments platform for SaaS. Its webhooks push
checkout, subscription, refund, dispute and customer-credit events to your HTTPS endpoint as
JSON `POST` requests. Each request carries a `creem-signature` header: a hex HMAC-SHA256 of the
raw request body, keyed with the endpoint's signing secret.

## When to Use This Skill

* How do I receive Creem webhooks?
* How do I verify the `creem-signature` header?
* How do I handle `checkout.completed`, `subscription.paid` or `refund.created` events?
* Why is my Creem webhook signature verification failing?
* How do I make my Creem webhook handler idempotent?

## Verification (core)

`creem-signature` is the **bare lowercase hex** HMAC-SHA256 of the **raw request body**: no
prefix, no timestamp, no other header is signed. The key is the signing secret used as a UTF-8
string (not hex- or base64-decoded). Check the header is exactly 64 hex characters before
decoding, because `Buffer.from(x, 'hex')` silently drops invalid characters.

```javascript
const crypto = require('crypto');

function verifyCreemSignature(rawBody, signature, secret = process.env.CREEM_WEBHOOK_SECRET) {
  if (!secret || typeof signature !== 'string') return false;
  if (!/^[0-9a-f]{64}$/i.test(signature)) return false;   // also guards the hex decode
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}

// rawBody: Buffer from express.raw({ type: '*/*' }); signature: req.headers['creem-signature']

```

Return `401` when this fails, `400` for a body that isn't a JSON object, and `200` once handled.
In Python use `hmac.compare_digest` on **bytes** (encode both sides) so a non-ASCII header gives
`401`, not `500`. Creem also ships `verifyWebhookSignature` in its TypeScript SDK
(`creem/webhooks`); the examples verify manually so the same check works in Express, Next.js
and FastAPI. Details in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/creem-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/creem-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/creem-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/creem-webhooks/examples/fastapi/).

## Setup

Register the endpoint on the **Developers** tab of the Creem dashboard (or with
`POST /v1/webhooks`, the SDK, CLI or MCP server) and copy the signing secret from the
**Developers > Webhook** page into `CREEM_WEBHOOK_SECRET`. Full steps in
[references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/creem-webhooks/references/setup.md).

## Payload and Event Types

Every payload has a top-level `id` (`evt_...`), `eventType`, `created_at` (milliseconds) and the
resource under `object`; `object.object` names the kind (`checkout`, `subscription`, ...).
**Dispatch on `eventType`**; no header names the event.

| `eventType` | Triggered When |
| --- | --- |
| `checkout.completed` | A checkout finishes |
| `subscription.paid` | A subscription payment succeeds (Creem recommends it for granting access) |
| `subscription.active` | A new subscription is created and paid (use for synchronisation) |
| `subscription.canceled` | The merchant or customer canceled the subscription |
| `subscription.scheduled_cancel` | Cancellation is scheduled for the end of the period |
| `subscription.past_due` / `subscription.unpaid` | A payment failed |
| `subscription.expired` | The period ended without a new payment |
| `subscription.update` / `subscription.trialing` / `subscription.paused` | The subscription changed |
| `refund.created` | A refund is created |
| `dispute.created` | A dispute is opened |
| `credits.granted` / `credits.consumed` / `credits.auto_recharged` | Customer credits change |
| `customer_credits.exhausted` | A customer's credits ran out during prepaid usage settlement |

More detail in [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/creem-webhooks/references/overview.md).

## Delivery and Idempotency

Creem makes up to 5 attempts (initial, then after 30 s, 5 min, 30 min and 6 h) and stops after
24 hours. Delivery is at least once, so deduplicate on the event `id`. Events can also be
resent from the dashboard.

## Environment Variables

```bash
CREEM_WEBHOOK_SECRET=your_creem_webhook_secret   # Developers > Webhook page

```

## Local Development

```bash
npx hookdeck-cli listen 3000 creem --path /webhooks/creem

```

No account required. Register the printed URL as a webhook endpoint in the Creem dashboard.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/creem-webhooks/references/overview.md) - How Creem webhooks work, events, payload, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/creem-webhooks/references/setup.md) - Register the endpoint, get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/creem-webhooks/references/verification.md) - HMAC-SHA256 check, gotchas, debugging
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/creem-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests