Webhook events

Event types, payload schemas, signature verification, and retry schedule.

Overview

JoPay sends webhook notifications to partners when payment proof statuses change. Webhooks are delivered as HTTP POST requests to the URL configured in the partner admin portal. All payloads are JSON and all deliveries are signed.

Event types

EventFires whenProof status
payment.proof_attachedA customer submits a transaction hash as proof of payment.attached
payment.proof_verifiedThe Trails engine confirms the on-chain transfer matches the payment request.verified
Only treat payment.proof_verified as verification of payment. The payment.proof_attached event means a transaction hash was submitted but has not yet been validated on-chain.

Payload schema

Both event types use the same payload structure:

{
  "event": "payment.proof_verified",
  "delivery_id": "del_a1b2c3d4e5f6",
  "partner_id": "ptr_abc123",
  "merchant_id": "mrc_def456",
  "request_id": "req_ghi789",
  "proof_status": "verified",
  "proof_source": "customer",
  "tx_hash": "0x1234abcd...ef56",
  "payer_address": "0x9876fedc...ba01",
  "timestamp": "2026-04-05T14:30:00.000Z"
}

Field reference

FieldTypeDescription
eventstringThe event type: payment.proof_attached or payment.proof_verified.
delivery_idstringUnique identifier for this delivery attempt. Use for idempotency (see below).
partner_idstringThe partner this payment belongs to.
merchant_idstringThe merchant who created the payment request.
request_idstringThe payment request ID.
proof_statusstringCurrent proof status: attached or verified.
proof_sourcestringWho submitted the proof (e.g., customer).
tx_hashstringThe on-chain transaction hash.
payer_addressstringThe wallet address that sent the USDC.
timestampstring (ISO 8601)When the event occurred.

Signature verification

Every webhook delivery includes a signature in the X-JoPay-Signature header. The signature format is:

v1={hmac_signature},t={unix_timestamp}

The HMAC is computed using SHA-256 over the concatenation of the timestamp and the raw request body, separated by a period:

HMAC-SHA256(secret, "{timestamp}.{raw_body}")

Verification steps

  1. Extract the v1 and t values from the header.
  2. Concatenate the timestamp and the raw request body with a period separator.
  3. Compute the HMAC-SHA256 using your webhook secret.
  4. Compare your computed signature to the v1 value using a timing-safe comparison.
  5. Optionally, reject deliveries where the timestamp is more than 5 minutes old to prevent replay attacks.

Node.js verification example

import crypto from "crypto";

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  // Parse "v1={sig},t={timestamp}" format
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => {
      const [key, ...rest] = p.split("=");
      return [key, rest.join("=")];
    })
  );

  const signature = parts.v1;
  const timestamp = parts.t;

  if (!signature || !timestamp) return false;

  // Check timestamp freshness (optional, 5-minute window)
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false;

  // Compute expected signature
  const payload = timestamp + "." + rawBody;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );
}

Retry schedule

If your endpoint returns a non-2xx status code or fails to respond within 10 seconds, JoPay retries the delivery on the following schedule:

AttemptDelay after failureCumulative time
1st retry1 minute1 minute
2nd retry5 minutes6 minutes
3rd retry30 minutes36 minutes
4th retry2 hours2 hours 36 minutes
5th retry6 hours8 hours 36 minutes
6th retry (final)24 hours32 hours 36 minutes

After 6 failed retries, the delivery is marked as permanently failed. It will not be retried automatically. You can view failed deliveries in the partner admin portal and manually trigger a re-delivery.

Idempotency

Due to retries and network conditions, your endpoint may receive the same webhook delivery more than once. Use the delivery_id field to deduplicate:

  1. When you receive a webhook, check if you have already handled a delivery with this delivery_id.
  2. If yes, return a 200 response without taking action again.
  3. If no, handle the event and store the delivery_id for future deduplication.
Store delivery_id values for at least 48 hours to cover the full retry window. A simple database table or Redis set works well for this.

Best practices

  • Return 200 quickly. Handle the webhook asynchronously if you need to do significant work. Return a 200 within 10 seconds to avoid triggering retries.
  • Verify signatures. Always verify the HMAC signature before trusting the payload.
  • Use delivery_id for idempotency. Never assume a webhook will only be delivered once.
  • Log all deliveries. Keep a log of received webhooks for debugging and audit purposes.
  • Use HTTPS. Webhook URLs must use HTTPS. HTTP URLs are rejected.