Agent skill

Payt Webhooks Skill

Receive and verify Payt webhooks. Use when setting up a Payt webhook endpoint, debugging X-PAYT-SIGNATURE verification, or handling accounts-receivable events like invoice_created, invoice_paid, invoice_payment_created, case_created and bundled_reminder_sent.

Install this skill

npx skills add hookdeck/webhook-skills --skill payt-webhooks


Payt (paytsoftware.com, Dutch accounts-receivable and credit-management software) sends a webhook when invoices, debtors, debt-collection cases, payment plans or PSP transactions change. Every delivery is a POST with a JSON body and an X-PAYT-SIGNATURE header: the hex HMAC-SHA256 of the raw body.

When to Use This Skill

  • How do I receive Payt webhooks?
  • How do I verify the X-PAYT-SIGNATURE header?
  • Why does my Payt webhook signature verification fail?
  • How do I handle invoice_created, invoice_paid, invoice_payment_created, case_created or bundled_reminder_sent?
  • Why do Payt events arrive out of order or more than once?

Verification (core)

The signature is hex(HMAC-SHA256(secret, raw request body)): the whole header value, no prefix. Only the raw body bytes are signed (no timestamp or id), and the key is the secret's UTF-8 bytes, used as-is. Check the header's format (exactly 64 hex characters) before comparing; hex is case-insensitive, so lowercase it, then compare in constant time.

const crypto = require('crypto');
const FORMAT = /^[0-9a-fA-F]{64}$/;

function verifyPaytSignature(rawBody, signature, secret) {   // rawBody: Buffer
  if (!secret || typeof signature !== 'string' || !FORMAT.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signature.toLowerCase()), b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib, hmac, re
from typing import Optional
FORMAT = re.compile(r"[0-9a-fA-F]{64}")

def verify_payt_signature(raw_body: bytes, signature: Optional[str], secret: Optional[str]) -> bool:
    if not secret or not signature or not FORMAT.fullmatch(signature):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature.lower().encode("ascii"), expected.encode("ascii"))

Read the body raw (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()) and parse JSON only after the check. Return 401 for a missing, malformed or wrong signature. Payt signs no timestamp, so there is no replay window to check.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The body is { "event": { "event_name", "context", "event_time" } }: dispatch on event.event_name; context holds the invoice, case, debtor or other object. Payt documents 53 event types; the common ones:

EventTriggered when
invoice_createdAn invoice is imported for the first time
invoice_paidThe invoice's open amount reaches zero
invoice_closedThe invoice is both sent and paid
invoice_payment_createdThe amount the debtor must pay changes (payment, costs, interest)
case_createdA debt collection case is created
case_paidA debt collection case is fully paid
bundled_reminder_sentA bundled reminder is sent
payment_plan_createdA payment plan is created
credit_limit_exceededA debtor's open amounts exceed the credit limit
psp_transaction_successfulA PSP transaction is registered

The other 43 are listed in references/overview.md; log the ones you don't handle.

Delivery

Per Payt's docs, respond 200 within 10 seconds. Failures are retried up to 10 times at increasing intervals, and frequent failures deactivate the webhook (Payt emails you). Order is not guaranteed (invoice_closed can arrive before invoice_created) and events can repeat, so make processing idempotent. For an object you don't know yet, Payt suggests answering 404 so the event is retried later.

Environment Variables

PAYT_WEBHOOK_SECRET=your_payt_webhook_secret   # shown only once, on the webhook settings page

Local Development

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

No account required. Use the printed URL as the webhook URL in Payt.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →