Webhooks overview
When a transaction settles, the gateway POSTs a JSON payload to the
notify_url you supplied on /payments/collect (or /payments/send).
This is how you find out about asynchronous outcomes without polling.
Receiving a webhook
Section titled “Receiving a webhook”POST /pdirect/webhook HTTP/1.1Host: merchant.example.comContent-Type: application/jsonUser-Agent: Pdirect-Gateway/1.0
{ "transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d", "customer_reference": "cust_abc123", "status": "approved", "amount": "12.50", "currency": "usd", "completed_at": "2026-05-05T10:15:08Z"}Respond with 2xx (typically 200 OK) within 10 seconds to
acknowledge. Anything else — 3xx, 4xx, 5xx, timeout — is
treated as a delivery failure.
Allow-listing
Section titled “Allow-listing”Your notify_url host must be on a per-merchant allow-list
configured in the dashboard. Empty list rejects all (fail-closed)
— until you populate it, the gateway will refuse to send webhooks
for any of your transactions and /payments/collect will return
error_code: "1110" for the field.
The allow-list rejects:
http://URLs in production environments (onlyhttps://allowed)- Hosts not on the explicit allow-list
- Schemes other than
http/https(javascript:,data:, etc.)
Sandbox accepts http:// for local-development convenience.
Retry policy
Section titled “Retry policy”If your endpoint doesn’t respond 2xx within 10 seconds, the
gateway retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | (immediate) |
| 2 | 5 seconds |
| 3 | 15 seconds |
| 4 | 60 seconds |
After the fourth attempt, the gateway logs the failure and gives
up. There is no dead-letter queue or notification of delivery
failure today — your endpoint must be reliable, or you must poll
GET /payments/status/{transaction_id} for definitive state.
Signature verification (not yet enforced)
Section titled “Signature verification (not yet enforced)”Idempotent processing on your end
Section titled “Idempotent processing on your end”The gateway sends one webhook per state transition, but networks fail and retries can deliver the same event more than once. Make your handler idempotent:
import express from "express";import crypto from "node:crypto";
const app = express();app.use(express.json());
const seenEvents = new Map<string, true>(); // production: use Redis
app.post("/pdirect/webhook", async (req, res) => { const id = req.body.transaction_id; const status = req.body.status; const eventKey = `${id}:${status}`;
if (seenEvents.has(eventKey)) { res.status(200).send("ok"); // already processed return; } seenEvents.set(eventKey, true);
// do work — credit customer, update order, etc. await updateOrderStatus(id, status, req.body);
res.status(200).send("ok");});For production, use Redis with a TTL of at least 24 hours and key
on (transaction_id, status, completed_at) — the gateway emits
distinct events per state transition, so you may legitimately get
two for the same transaction (processing then approved).
What if my server is down?
Section titled “What if my server is down?”Plan for this. The retry budget is 4 attempts over ~80 seconds.
If you’re down longer, you’ll lose those events and must reconcile
via polling GET /payments/status/{transaction_id} for every
transaction whose state you can’t determine.
For high-stakes flows, a queue between the webhook receiver and your business logic is a good idea: the receiver acks fast, the queue processes when the worker pool is healthy.
Examples
Section titled “Examples”Express (Node)
Section titled “Express (Node)”import express from "express";
const app = express();app.use(express.json({ limit: "1mb" }));
app.post("/pdirect/webhook", async (req, res) => { // TODO: signature verification, when shipped await processEvent(req.body); res.status(200).send("ok");});FastAPI (Python)
Section titled “FastAPI (Python)”from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/pdirect/webhook")async def webhook(req: Request): body = await req.json() # TODO: signature verification, when shipped await process_event(body) return {"ok": True}Flask (Python)
Section titled “Flask (Python)”from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/pdirect/webhook")def webhook(): body = request.get_json() # TODO: signature verification, when shipped process_event(body) return jsonify({"ok": True}), 200See also
Section titled “See also”- Event types — what events you can expect
- Signature verification — currently blocked
- Your first request — webhook in context