Agent skill

Payroc Webhooks Skill

Receive and authenticate Payroc event subscription webhooks. Use when setting up a Payroc webhook endpoint, checking the Payroc-Secret header, debugging 401s, or handling CloudEvents such as processingAccount.status.changed, processingAccount.riskStatus.changed, processingAccount.signature.signed and terminalOrder.status.changed.

Install this skill

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


Payroc (payment processor) sends a CloudEvents 1.0 JSON POST to your endpoint when something changes on a processing account or terminal order. Covers event subscriptions only, not Hosted Payments Page callbacks.

Requests are not signed. Each one carries the subscription's secret as plain text in a Payroc-Secret header, and your endpoint authenticates the sender by comparing that header with the secret you chose when you created the subscription.

Security note: a static shared credential authenticates the sender, not the body, and gives no replay protection (there is no signature and no timestamp). Serve the endpoint over HTTPS, dedupe on the event id, and rotate the credential if it leaks.

When to Use This Skill

  • How do I receive Payroc webhooks / event subscriptions?
  • How do I verify the Payroc-Secret header?
  • Does Payroc sign webhook payloads? (No: shared secret header)
  • How do I handle processingAccount.status.changed or terminalOrder.status.changed?
  • Why does my Payroc webhook endpoint return 401, or why does Payroc keep retrying?

Verification (core)

const crypto = require('crypto');

function verifyPayrocRequest(headers, secret = process.env.PAYROC_WEBHOOK_SECRET) {
  if (!secret) return false;                    // never accept when unconfigured
  const received = headers['payroc-secret'];    // whole header value, plain text
  if (!received) return false;
  const a = Buffer.from(received);
  const b = Buffer.from(secret);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
  • Header: Payroc-Secret: <secret> (case-insensitive lookup). Compare the whole value to PAYROC_WEBHOOK_SECRET, in constant time. In Python use hmac.compare_digest on bytes (encode both sides; passing str raises on non-ASCII).
  • Return 401 when the header is missing or wrong, 400 for a body that isn't a JSON object.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, check the header, then parse.
  • Return exactly 200 on success. Otherwise Payroc retries five times, then emails the subscription's support address.
  • Dispatch on type; dedupe on id.

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

Event Types

The body is a CloudEvent: specversion (1.0), type, version, source (payroc), id, time, datacontenttype and data.

Event (type)Sent When
processingAccount.status.changedA processing account's status changes
processingAccount.riskStatus.changedA processing account's risk status changes
processingAccount.signature.signedA processing account's agreement is signed
terminalOrder.status.changedA terminal order's status changes

Environment Variables

PAYROC_WEBHOOK_SECRET=your-subscription-secret   # notifications[].secret, 16 to 64 characters

Local Development

Payroc can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

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

Use the printed URL as the notification uri of your subscription.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →