Guide to Payroc Webhooks: Features and Best Practices
Payroc webhooks tell your platform when something changes on the merchant accounts you board with Payroc. When a processing account's status or risk status changes, when a merchant signs their processing agreement, or when a terminal order moves along, Payroc sends a notification to your endpoint, so your onboarding flow can move forward without polling the boarding API.
This guide covers how Payroc event subscriptions are created, the CloudEvents format and the events available, how to authenticate notifications with the Payroc-Secret header, what happens when delivery fails, and the practices that keep a Payroc receiver reliable.
What are Payroc webhooks?
Payroc sends webhooks for event subscriptions. You create a subscription through the API with the event types you want, an endpoint URI, a secret and a support email address. When one of those events happens, Payroc sends a POST to the URI with a body in the CloudEvents format.
Payroc doesn't sign notifications. The secret you set on the subscription is "a secret that we return in the header of webhook requests to verify that it is a genuine request": it arrives as plain text in the Payroc-Secret header, and your endpoint compares it with the value you stored.
Payroc's Hosted Payments Page has its own callback mechanism, which works differently and isn't covered here.
Payroc webhook features
| Feature | Details |
|---|---|
| Configuration | API only: POST https://api.payroc.com/v1/event-subscriptions |
| Method | POST |
| Format | CloudEvents 1.0, JSON |
| Event field | type |
| Authentication | Your secret, sent as plain text in Payroc-Secret. No signature or timestamp |
| Secret | Chosen by you, 16 to 64 characters. API responses mask it |
| Success | HTTP 200 |
| Retries | Five, then an email to the subscription's support address |
| Timeout | Not documented |
| Source IPs | Not published |
Events and payloads
Payroc currently documents four events, all for boarding:
type | Fires when |
|---|---|
processingAccount.status.changed | Payroc changes the status of a processing account |
processingAccount.riskStatus.changed | Payroc changes the risk status of a processing account |
processingAccount.signature.signed | The Merchant Processing Agreement for a processing account is signed |
terminalOrder.status.changed | Payroc changes the status of a terminal order |
Every notification uses the same CloudEvents attributes, with the event's details in data. Here is Payroc's example:
{
"specversion": "1.0",
"type": "processingAccount.status.changed",
"version": "1.0.0",
"source": "payroc",
"id": "123e4567-e89b-12d3-a456-426614174000",
"time": "2024-07-02T15:30:00.000Z",
"datacontenttype": "application/json",
"data": {
"processingAccountId": "38765",
"status": "entered"
}
}
version is the version of the event type in Payroc's specification, not of CloudEvents itself (that's specversion). The events list gives 1.0.0 for every type, while the subscription guide's example shows 1.0, so don't compare it as an exact string. id is unique per event and makes a good deduplication key.
Setting up Payroc webhooks
Generate a secret between 16 and 64 characters long, for example with openssl rand -hex 24, which prints 48 characters. Then create the subscription with your API bearer token:
curl -X POST https://api.payroc.com/v1/event-subscriptions \
-H "Authorization: Bearer $PAYROC_ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"eventTypes": [
"processingAccount.status.changed",
"processingAccount.riskStatus.changed",
"processingAccount.signature.signed",
"terminalOrder.status.changed"
],
"notifications": [
{
"type": "webhook",
"uri": "https://example.com/webhooks/payroc",
"secret": "<your secret>",
"supportEmailAddress": "payments-team@example.com"
}
]
}'
The response returns the subscription id, which you use to retrieve, update or delete it, and shows the secret masked, so keep your own copy. The URI must be publicly reachable, and the support email address should be a monitored inbox, because that is where Payroc reports failed deliveries.
With Hookdeck. Hookdeck's Payroc source covers event subscriptions only, not Hosted Payments Page callbacks. Create a source with the Payroc type, paste the subscription's secret into Secret, and use the source URL as the notification uri. Hookdeck compares it with the Payroc-Secret header on each request. The source accepts POST only, which is what Payroc sends.
Securing Payroc webhooks
Read Payroc-Secret, compare it with your stored secret in constant time, and only then parse the body.
const crypto = require('crypto');
const express = require('express');
const SECRET = process.env.PAYROC_WEBHOOK_SECRET;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/payroc', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get('payroc-secret');
if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Deduplicate on event.id, route on event.type, queue the work, then answer with exactly 200.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
SECRET = os.environ.get("PAYROC_WEBHOOK_SECRET", "")
def verify_payroc_request(headers) -> bool:
if not SECRET:
return False # fail closed when unconfigured
received = headers.get("Payroc-Secret") # Starlette and Flask headers are case-insensitive
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))
A static secret authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Use HTTPS, a secret at the long end of the allowed range, and update the subscription and your receiver together if it might have leaked.
Make Payroc webhooks production-ready. Hookdeck Event Gateway checks the
Payroc-Secretheader on every notification, answers Payroc with a200straight away, and delivers each event to your service with retries you control.
Payroc webhook limitations and pain points
Five retries, then an email
The Problem: During an outage on your side, notifications that run out of retries end up as emails to your support address.
Why It Happens: Payroc needs a 200. Without one, it retries five times and then emails the support address. Payroc doesn't document the interval between retries, and the email concerns that one notification: Payroc sends it "only when a single instance of an event notification fails to deliver".
Workarounds:
- Use a monitored support address, and treat those emails as a signal to reconcile against the boarding API.
- Answer with
200as soon as the secret checks out, and process afterwards.
How Hookdeck Can Help: Hookdeck answers Payroc with a 200 once the notification is stored, then delivers it to your service with retries on your own schedule. Issues alert you when deliveries fail, and you can replay any stored event.
Only 200 counts
The Problem: You return 202 Accepted or 204 No Content, and Payroc retries anyway.
Why It Happens: Payroc says "You must return a 200 status code" and retries "if we do not receive a 200 response". It doesn't mention other 2xx codes.
Workarounds:
- Return exactly
200.
How Hookdeck Can Help: Hookdeck answers Payroc with a 200, whatever your service returns.
API-only configuration
The Problem: There's no dashboard page to check which URL and events a subscription uses.
Why It Happens: Event subscriptions are created and changed through the API, and responses mask the secret.
Workarounds:
- Keep the subscription definition in code or configuration, alongside the secret in your secrets manager.
- Retrieve subscriptions by
idto audit them.
How Hookdeck Can Help: If the subscription points at a Hookdeck source, the URI Payroc holds stays the same while you move or change the services behind it, so most changes don't need an API call to Payroc.
A shared secret with no signature
The Problem: Anyone who learns the secret can send notifications that pass your check.
Why It Happens: Payroc-Secret carries the same value on every request and isn't tied to the body or a time.
Workarounds:
- Treat notifications as prompts and fetch the processing account or terminal order from the API before acting on anything important.
- Rotate the secret by updating the subscription, accepting both values in your receiver during the change.
How Hookdeck Can Help: Hookdeck's Payroc source compares Payroc-Secret with the secret you paste. Hookdeck answers Payroc with a 200 either way, then records a request without the right secret as failed verification, so it never reaches your destination.
Best practices
Route on type, deduplicate on id
Switch on the CloudEvents type, and store id before processing so a retried notification is skipped. See our guide to webhook idempotency.
Fetch current state
data carries only a few fields, such as processingAccountId and status. Use them to fetch the full record from the Payroc API, which also protects you against a replayed request.
Subscribe to every event you handle
Each subscription lists its event types. Add new types to the subscription as you build support for them, and acknowledge unexpected ones with 200.
Acknowledge fast
Return 200 immediately and do the work afterwards. See why to process webhooks asynchronously.
Conclusion
Payroc event subscriptions send CloudEvents JSON for processing account and terminal order changes, authenticated by the secret you choose in the Payroc-Secret header rather than a signature. Compare the header in constant time, route on type, deduplicate on id, and return exactly 200, because five failed attempts end in an email instead of another delivery.
Hookdeck Event Gateway sits in front of your receiver, checks the secret, answers Payroc at once, and keeps every notification until your service has processed it.
Get started with Hookdeck for free and handle Payroc webhooks reliably in minutes.