Agent skill

Xendit Webhooks Skill

Receive and verify Xendit webhooks. Use when setting up Xendit webhook handlers, checking the x-callback-token verification token, debugging 401s or failed verification, or handling payment.capture, payment.failure, payment_token.activation, refund.succeeded and invoice PAID callbacks.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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.

For complete handlers with tests, see examples/express/, examples/nextjs/, 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.

EventTriggered When
payment.captureA payment is collected and confirmed by the payment method
payment.authorizationA payment is reserved on the customer's account
payment.failureA payment fails to be collected
payment.expiryA successful payment authorization expires
payment_request.expiryA payment request expires unpaid
payment_token.activationA payment token is authenticated and confirmed
payment_token.failurePayment token creation fails authentication
payment_token.expiryA payment token reaches its expiry
refund.succeededA refund is confirmed by the payment method
refund.failedA refund fails
Invoice callback, status: PAIDAn invoice is paid (no event field)
Invoice callback, status: EXPIREDAn invoice expires unpaid (no event field)

Other products (Payouts, Subscriptions, Disputes, xenPlatform, Payment Sessions) use their own payloads; see 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

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

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

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

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →