Gareth Wilson Gareth Wilson

Guide to Payroc Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationAPI only: POST https://api.payroc.com/v1/event-subscriptions
MethodPOST
FormatCloudEvents 1.0, JSON
Event fieldtype
AuthenticationYour secret, sent as plain text in Payroc-Secret. No signature or timestamp
SecretChosen by you, 16 to 64 characters. API responses mask it
SuccessHTTP 200
RetriesFive, then an email to the subscription's support address
TimeoutNot documented
Source IPsNot published

Events and payloads

Payroc currently documents four events, all for boarding:

typeFires when
processingAccount.status.changedPayroc changes the status of a processing account
processingAccount.riskStatus.changedPayroc changes the risk status of a processing account
processingAccount.signature.signedThe Merchant Processing Agreement for a processing account is signed
terminalOrder.status.changedPayroc 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-Secret header on every notification, answers Payroc with a 200 straight 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 200 as 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 id to 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.