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-SIGNATUREheader? - Why does my Payt webhook signature verification fail?
- How do I handle
invoice_created,invoice_paid,invoice_payment_created,case_createdorbundled_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_: dispatch on event.event_name; context holds the invoice, case, debtor or other object. Payt documents 53 event types; the common ones:
| Event | Triggered when |
|---|---|
invoice_created | An invoice is imported for the first time |
invoice_paid | The invoice's open amount reaches zero |
invoice_closed | The invoice is both sent and paid |
invoice_payment_created | The amount the debtor must pay changes (payment, costs, interest) |
case_created | A debt collection case is created |
case_paid | A debt collection case is fully paid |
bundled_reminder_sent | A bundled reminder is sent |
payment_plan_created | A payment plan is created |
credit_limit_exceeded | A debtor's open amounts exceed the credit limit |
psp_ | A 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
- references/overview.md - Payload shape, all event types, delivery and retries
- references/setup.md - Activating webhooks and generating the secret
- references/verification.md - Signature check in Node, Python and Next.js, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests