Webhooks
Payments Central sends HTTP POST notifications to your endpoint when payment events occur, so you can react in real time without polling.
How webhooks work
When an event occurs (e.g. a transaction is captured), Payments Central sends an HTTP POST request to your webhook URL with a JSON body describing the event. Your endpoint must respond with 200 OK within 10 seconds.
Event types
| Event | Description |
|---|---|
transaction.created | A new transaction was created |
transaction.captured | A transaction was successfully captured |
transaction.failed | A transaction was declined or failed |
transaction.refunded | A full or partial refund was issued |
transaction.voided | A transaction was voided before settlement |
ledger.entry.posted | A journal entry was posted to the ledger |
Event payload
Every webhook POST body follows this structure:
{
"event": "transaction.captured",
"id": "evt_01HXYZ999",
"created_at": "2026-05-12T10:00:05Z",
"data": {
"id": "txn_01HXYZ123456",
"merchant_id": "mer_01ABCDEF",
"status": "captured",
"amount": 4999,
"currency": "USD",
"gateway": "stripe",
"merchant_ref": "order-8821",
"created_at": "2026-05-12T10:00:00Z",
"updated_at": "2026-05-12T10:00:05Z"
}
}
Registering a webhook endpoint
- Go to Dashboard → Settings → Webhooks
- Click Add endpoint
- Enter your HTTPS URL (e.g.
https://yourapp.com/webhooks/payments-central) - Select the event types you want to subscribe to
- Copy the signing secret — you'll use it to verify deliveries
Verifying webhook signatures
Every webhook delivery includes a X-PC-Signature header — an HMAC-SHA256 of the raw request body signed with your webhook secret. Always verify this before processing the event.
Node.js verification
import crypto from 'crypto';
function verifyWebhook(rawBody: Buffer, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express handler
app.post('/webhooks/payments-central', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-pc-signature'] as string;
if (!sig || !verifyWebhook(req.body, sig, process.env.PC_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
switch (event.event) {
case 'transaction.captured':
await fulfillOrder(event.data.merchant_ref);
break;
case 'transaction.failed':
await notifyCustomerOfFailure(event.data.merchant_ref);
break;
}
res.sendStatus(200);
});
Python verification
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/payments-central', methods=['POST'])
def handle_webhook():
sig = request.headers.get('X-PC-Signature', '')
if not verify_webhook(request.get_data(), sig, os.environ['PC_WEBHOOK_SECRET']):
abort(401)
event = request.get_json()
if event['event'] == 'transaction.captured':
fulfill_order(event['data']['merchant_ref'])
return '', 200
Retry behaviour
If your endpoint returns a non-2xx status or times out, Payments Central retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 8 hours |
After 5 failed attempts the delivery is abandoned and marked as failed in the dashboard.
Idempotent event handling
Use the event id field (e.g. evt_01HXYZ999) to deduplicate redeliveries. Store processed event IDs and skip duplicates:
const alreadyProcessed = await db.exists('processed_events', event.id);
if (!alreadyProcessed) {
await processEvent(event);
await db.insert('processed_events', { id: event.id });
}
Testing webhooks locally
Use a tunnel tool to expose your local server during development:
# Using ngrok
ngrok http 3000
# Your tunnel URL:
# https://abc123.ngrok.io/webhooks/payments-central
# Register this URL in the dashboard under a test webhook endpoint