Webhooks
Receive real-time payment notifications via HMAC-signed HTTP callbacks.
Setup
Set your webhook URL
In the partner portal settings (or via the admin panel), enter your HTTPS endpoint URL. JoPay sends POST requests to this URL whenever a payment event occurs.
Save your webhook secret
When webhooks are enabled, JoPay generates a 256-bit webhook secret (64 hex characters). Copy and store this secret securely — you will need it to verify signatures.
Enable webhooks
Toggle webhook_enabled to true. JoPay will start dispatching events immediately for any new payment activity.
2xx status code within 8 seconds. Responses outside this window are treated as failures and trigger retries.Payload Format
Every webhook delivery is a JSON POST request with the following body:
{
"event": "payment.proof_verified",
"delivery_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"partner_id": "partner-uuid",
"request_id": "request-uuid",
"merchant_id": "merchant-uuid",
"proof_status": "verified",
"proof_source": "trails_on_chain",
"tx_hash": "0xabc123...",
"payer_address": "0xdef456...",
"timestamp": "2026-04-05T14:30:00.000Z"
}Field Reference
| Field | Type | Description |
|---|---|---|
event | string | Event type (see below). |
delivery_id | string | Unique ID for this delivery attempt. Use for idempotency. |
partner_id | string | Your partner UUID. |
request_id | string | The payment request UUID. |
merchant_id | string | The merchant UUID. |
proof_status | string | Current proof status: none, attached, or verified. |
proof_source | string | null | How proof was obtained (e.g. trails_on_chain). |
tx_hash | string | null | On-chain transaction hash, if available. |
payer_address | string | null | Wallet address of the payer, if known. |
timestamp | string | ISO 8601 timestamp of when the event was created. |
Signature Verification
Every webhook request includes an x-jopay-signature header with the format v1=SIGNATURE,t=TIMESTAMP. The signature is an HMAC-SHA256 hash of TIMESTAMP.BODY using your webhook secret as the key.
How to Verify
- Parse the
x-jopay-signatureheader to extract thev1signature andttimestamp. - Concatenate
t+"."+ raw request body. - Compute HMAC-SHA256 of that string using your webhook secret.
- Compare the computed hex digest with the
v1value using a timing-safe comparison. - Optionally reject requests where the timestamp is too far from the current time (e.g. more than 5 minutes) to prevent replay attacks.
Node.js Verification Example
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// Parse "v1=<sig>,t=<ts>"
const parts: Record<string, string> = {};
for (const segment of signatureHeader.split(",")) {
const [key, value] = segment.split("=", 2);
if (key && value) parts[key.trim()] = value.trim();
}
const receivedSig = parts["v1"];
const timestamp = parts["t"];
if (!receivedSig || !timestamp) return false;
// Compute expected signature
const payload = timestamp + "." + rawBody;
const expectedSig = createHmac("sha256", secret)
.update(payload)
.digest("hex");
// Timing-safe comparison
if (receivedSig.length !== expectedSig.length) return false;
return timingSafeEqual(
Buffer.from(receivedSig, "hex"),
Buffer.from(expectedSig, "hex"),
);
}Request Headers
| Header | Description |
|---|---|
content-type | application/json |
x-jopay-signature | HMAC signature and timestamp: v1=SIG,t=TS |
x-jopay-event | The event type (e.g. payment.proof_verified) |
x-jopay-delivery | The unique delivery UUID |
user-agent | JoPay-Webhook/1.0 |
Retry Schedule
If your endpoint returns a non-2xx response (or times out), JoPay retries with exponential backoff. The delays between attempts are:
| Attempt | Delay |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 6 hours |
| 6 | 24 hours |
After 6 failed attempts, the delivery is marked as abandoned and no further retries are made.
Event Types
| Event | When It Fires |
|---|---|
payment.proof_attached | A payer has submitted proof (transaction hash) for a payment request. The proof has not yet been verified on-chain. |
payment.proof_verified | The payment proof has been verified on-chain (via Trails). The USDC transfer is verified. |
Idempotency
Every delivery includes a unique delivery_id. If your endpoint receives the same delivery_id more than once (due to retries or network issues), treat subsequent deliveries as duplicates. Store the delivery_id and skip re-handling if you have already seen it.