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-Secretheader? - Does Payroc sign webhook payloads? (No: shared secret header)
- How do I handle
processingAccount.status.changedorterminalOrder.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 toPAYROC_WEBHOOK_SECRET, in constant time. In Python usehmac.compare_digeston bytes (encode both sides; passingstrraises on non-ASCII). - Return
401when the header is missing or wrong,400for 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
200on success. Otherwise Payroc retries five times, then emails the subscription's support address. - Dispatch on
type; dedupe onid.
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.changed | A processing account's status changes |
processingAccount.riskStatus.changed | A processing account's risk status changes |
processingAccount.signature.signed | A processing account's agreement is signed |
terminalOrder.status.changed | A 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
- references/overview.md - What Payroc webhooks are, events and payload shape
- references/setup.md - Create an event subscription with the API
- references/verification.md - Secret header check, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests