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.

Your endpoint must respond with a 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

FieldTypeDescription
eventstringEvent type (see below).
delivery_idstringUnique ID for this delivery attempt. Use for idempotency.
partner_idstringYour partner UUID.
request_idstringThe payment request UUID.
merchant_idstringThe merchant UUID.
proof_statusstringCurrent proof status: none, attached, or verified.
proof_sourcestring | nullHow proof was obtained (e.g. trails_on_chain).
tx_hashstring | nullOn-chain transaction hash, if available.
payer_addressstring | nullWallet address of the payer, if known.
timestampstringISO 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

  1. Parse the x-jopay-signature header to extract the v1 signature and t timestamp.
  2. Concatenate t + "." + raw request body.
  3. Compute HMAC-SHA256 of that string using your webhook secret.
  4. Compare the computed hex digest with the v1 value using a timing-safe comparison.
  5. 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

HeaderDescription
content-typeapplication/json
x-jopay-signatureHMAC signature and timestamp: v1=SIG,t=TS
x-jopay-eventThe event type (e.g. payment.proof_verified)
x-jopay-deliveryThe unique delivery UUID
user-agentJoPay-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:

AttemptDelay
11 minute
25 minutes
330 minutes
42 hours
56 hours
624 hours

After 6 failed attempts, the delivery is marked as abandoned and no further retries are made.

Client errors (HTTP 4xx, except 429) are not retried. If your endpoint returns a 4xx response, the delivery is immediately abandoned. Fix your endpoint and rely on the next event to test.

Event Types

EventWhen It Fires
payment.proof_attachedA payer has submitted proof (transaction hash) for a payment request. The proof has not yet been verified on-chain.
payment.proof_verifiedThe 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.

A simple approach: maintain a set (or database table) of handled delivery IDs with a TTL of 48 hours. Check before acting on any webhook.