Webhooks
Webhooks tell your system when an asynchronous Payscribe operation changes state. Treat each delivery as a secure, at-least-once event: verify it, record it once, acknowledge quickly, and process slow work asynchronously.
Delivery flow
| Step | What happens | Your responsibility |
|---|---|---|
| 1. Deliver | Payscribe sends an HTTPS POST. | Receive the raw request body. |
| 2. Verify | The delivery is signed. | Check timestamp and HMAC signature. |
| 3. Deduplicate | A delivery can be retried. | Store X-Payscribe-Event-Id with a unique constraint. |
| 4. Process | The event requires business work. | Return 200 quickly; queue reconciliation and fulfilment. |
Configure an endpoint
- In the dashboard, open Settings → Webhooks.
- Register a publicly reachable HTTPS URL that accepts
POSTrequests. - Select the events your integration needs.
- Store the webhook secret in your server environment, separately from the API key.
Return 200 OK within five seconds. Use a temporary HTTPS tunnel only for sandbox development; production needs a stable production endpoint.
Verify every delivery
| Header | Purpose |
|---|---|
X-Payscribe-Signature | v1= HMAC-SHA256 signature |
X-Payscribe-Event-Id | Unique delivery ID for deduplication |
X-Payscribe-Timestamp | Unix timestamp; reject stale events |
The signed value is timestamp + "." + event_id + "." + raw_body.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Register before express.json(); req.body must remain a Buffer.
app.post('/webhooks/payscribe', express.raw({type: 'application/json'}), async (req, res) => {
const timestamp = req.headers['x-payscribe-timestamp'];
const eventId = req.headers['x-payscribe-event-id'];
const signature = req.headers['x-payscribe-signature'];
if (!signature || !timestamp || !eventId) return res.sendStatus(401);
const expected = `v1=${crypto.createHmac('sha256', process.env.PAYSCRIBE_WEBHOOK_SECRET)
.update(`${timestamp}.${eventId}.${req.body.toString('utf8')}`).digest('hex')}`;
const received = Buffer.from(signature);
const calculated = Buffer.from(expected);
if (received.length !== calculated.length || !crypto.timingSafeEqual(calculated, received)) return res.sendStatus(401);
// Insert eventId with a unique DB constraint, queue work, then acknowledge.
return res.sendStatus(200);
});
Test and recover
In sandbox, trigger an event with Simulate transfer. Verify the signature, record the event ID, and reconcile final state with the API or dashboard. Test duplicate deliveries and invalid signatures before production.
Never assume delivery order across event types or fulfil from an unverified payload. For payloads, retry behaviour, and the event catalogue, use the Webhooks & Events reference.
Was this page helpful?