# Xendit Webhooks

Xendit is a Southeast Asia payments platform (Indonesia, Philippines, Vietnam, Thailand, Malaysia and more). It notifies your server about payments, payment tokens, refunds, invoices and payouts with webhooks. Xendit does **not** sign the payload: there is no HMAC. It sends your **webhook verification token**, unchanged, in the `x-callback-token` header, and you verify the sender by comparing that header with the token from the dashboard.

## When to Use This Skill

* How do I receive Xendit webhooks?
* How do I verify the `x-callback-token` header on a Xendit webhook?
* Does Xendit sign webhooks with HMAC? (No: shared verification token)
* How do I handle `payment.capture`, `payment.failure` or `payment_token.activation`?
* How do I handle an invoice callback with status `PAID`?
* Why is my Xendit webhook endpoint rejecting requests?

## Verification (core)

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

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyXenditToken(headers, token = process.env.XENDIT_CALLBACK_TOKEN) {
  if (!token) return false;                        // never accept when unconfigured
  const received = headers['x-callback-token'];    // raw token, not a signature
  return Boolean(received) && !Array.isArray(received) && safeEqual(received, token);
}

```

Return `401` when this fails, `400` for a body that isn't a JSON object, and `200` quickly once handled. Nothing is signed, so the raw body isn't needed for verification; read it raw (Express: `express.raw({ type: '*/*' })`) so any content type is accepted, check the token first, then parse. Python uses `hmac.compare_digest` on the same SHA-256 digests; see [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/xendit-webhooks/references/verification.md).

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

## Event Types

Payload shape varies by product. Payments API webhooks carry an `event` field and the resource under `data`; dispatch on `event`. Invoice callbacks have no `event`: the invoice is the body and you check `status`.

| Event | Triggered When |
| --- | --- |
| `payment.capture` | A payment is collected and confirmed by the payment method |
| `payment.authorization` | A payment is reserved on the customer's account |
| `payment.failure` | A payment fails to be collected |
| `payment.expiry` | A successful payment authorization expires |
| `payment_request.expiry` | A payment request expires unpaid |
| `payment_token.activation` | A payment token is authenticated and confirmed |
| `payment_token.failure` | Payment token creation fails authentication |
| `payment_token.expiry` | A payment token reaches its expiry |
| `refund.succeeded` | A refund is confirmed by the payment method |
| `refund.failed` | A refund fails |
| Invoice callback, `status: PAID` | An invoice is paid (no `event` field) |
| Invoice callback, `status: EXPIRED` | An invoice expires unpaid (no `event` field) |

Other products (Payouts, Subscriptions, Disputes, xenPlatform, Payment Sessions) use their own payloads; see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/xendit-webhooks/references/overview.md). Bill Payments callbacks are the exception that is signed (`X-Callback-Signature`, HMAC SHA256), not covered by the token check here.

## Environment Variables

```bash
XENDIT_CALLBACK_TOKEN=your_xendit_webhook_verification_token   # Dashboard > Settings > Webhooks

```

## Local Development

For local webhook testing, run the Hookdeck CLI via `npx` (no account required):

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

```

Paste the printed URL into Xendit Dashboard > Settings > Webhooks.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/xendit-webhooks/references/overview.md) - What Xendit webhooks are, event types, payload shapes, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/xendit-webhooks/references/setup.md) - Find the verification token and register your endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/xendit-webhooks/references/verification.md) - Token check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/xendit-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests