# KOMOJU Webhooks

## When to Use This Skill

* How do I receive KOMOJU webhooks?
* How do I verify the `X-Komoju-Signature` header on a KOMOJU webhook?
* How do I handle `payment.captured`, `payment.refunded` or `subscription.created` events?
* Why is my KOMOJU webhook signature verification failing?
* How do I deduplicate KOMOJU webhook deliveries?

## Verification (core)

Set a secret token on the KOMOJU webhook. Without one, deliveries are unsigned and fail verification.

With a secret token set, KOMOJU sends `X-Komoju-Signature: <hex>` with **no prefix**. The value is the
lowercase hex **HMAC-SHA256 of the raw request body**, keyed with the secret token (its UTF-8 bytes,
used as-is). Nothing else is signed: no timestamp, no id. It is not the Standard Webhooks spec.
Validate the header's shape (exactly 64 hex characters), then compare in constant time. Verify over
the **raw bytes before parsing**.

Node (compares validated ASCII hex, so `Buffer.from(x, 'hex')` is never called on untrusted input):

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

function verifyKomojuSignature(rawBody, signatureHeader, secret) {
  if (typeof signatureHeader !== 'string' || !/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
  const expected = Buffer.from(crypto.createHmac('sha256', secret).update(rawBody).digest('hex'));
  const received = Buffer.from(signatureHeader.toLowerCase());
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

```

Python (compare bytes so a non-ASCII header yields 401, not a 500):

```python
import hashlib, hmac, re

def verify_komoju_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    if not signature_header or not re.fullmatch(r"[0-9a-fA-F]{64}", signature_header, re.ASCII):
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest().encode("ascii")
    return hmac.compare_digest(signature_header.lower().encode("ascii"), expected)

```

Missing, empty, malformed or mismatched signature: respond **401**. Express handlers must read the
body with `express.raw({ type: '*/*' })` so any content type verifies the exact bytes.

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

## Events

Every delivery is an HTTPS `POST` with a JSON body shaped
`{ "id", "type", "resource": "event", "data": { ... }, "created_at" }`. Dispatch on the body's `type`
field, which is also sent in the `X-Komoju-Event` header. `data` is the resource (its `resource` key
names it, e.g. `payment` or `webhook`).

| Event | Meaning |
| --- | --- |
| `ping` | Test event (`data` is the webhook) |
| `payment.authorized` | A payment was authorized |
| `payment.captured` | A payment was captured |
| `payment.updated` | A payment was changed |
| `payment.expired` | A payment was expired |
| `payment.cancelled` | A payment was cancelled |
| `payment.refund.created` | A refund was created (may be partial) |
| `payment.refunded` | A payment was fully refunded |
| `payment.failed` | A payment attempt failed (it may be re-attempted) |
| `customer.created` / `customer.updated` / `customer.deleted` | Customer changes |
| `subscription.created` / `subscription.captured` / `subscription.failed` / `subscription.suspended` / `subscription.deleted` | Subscription lifecycle |
| `settlement.created` | A settlement was created |

## Delivery and Idempotency

A non-success status or a connection failure is retried 25 times with growing intervals, the final
attempt about 25 days later. You can resend a delivery from the dashboard (from a payment's page).
Dedupe on the event `id`, and acknowledge quickly and process asynchronously.

## Environment Variables

```bash
KOMOJU_WEBHOOK_SECRET=your_secret_token   # the secret token set on the KOMOJU webhook

```

## Setup with Hookdeck

Create the Hookdeck source with the **KOMOJU** type and paste the webhook's secret token into the
source's webhook verification settings. Set a secret token on the KOMOJU webhook. Without one,
deliveries are unsigned and fail verification. Hookdeck verifies `X-Komoju-Signature` (HMAC-SHA256,
hex) on KOMOJU's `POST` delivery.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 komoju --path /webhooks/komoju

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/komoju-webhooks/references/overview.md) - KOMOJU webhook concepts, events and payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/komoju-webhooks/references/setup.md) - Create a webhook in the dashboard and set its secret token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/komoju-webhooks/references/verification.md) - Signature verification details and gotchas