Skip to content

After the payment

Webhooks

Wegopay sends a signed HTTPS request to your server when a payment changes. Verify the signature, store the event, then read the payment before acting.

Configure an endpoint

  1. In Dashboard → Settings → Webhook, choose Sandbox or Live and enter a public HTTPS URL on port 443.
  2. Copy the signing secret into your server’s secret manager. It is shown once; if you lose it, rotate it.
  3. Configure it before you create payments. Changing the URL does not reroute events that are already queued.

Sandbox and live have separate endpoints and secrets. An API key is not a signing secret.

Per-key endpoints

In Dashboard → API Keys → Key settings, each key can Inherit the default endpoint (the default), use a Custom endpoint with its own secret, or be Disabled. Events go to the endpoint of the key that created the payment, even after that key is revoked. A failed custom delivery never falls back to the default endpoint.

Verify the signature

Every request carries two headers:

Headers
Wegopay-Signature: t=1760184073,v1=5f2b…(64 hex characters)
Wegopay-Event-Id: 01JA2Z4M8Q6V3N5R7T9W1X3Y5B
  1. Read the raw request body as bytes. Do not parse and re-encode the JSON first.
  2. Compute HMAC-SHA256 with the signing secret, exactly as issued, over <t>. followed by the raw body.
  3. Compare it with v1 in constant time. Reject timestamps more than five minutes from your clock.
  4. Check that Wegopay-Event-Id equals the body’s eventId, and that environment matches the endpoint.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody: the exact request bytes (a Buffer), read before any JSON parser.
// secrets: the current signing secret, plus an old one while it drains.
export function verifyWebhook(rawBody, signatureHeader, secrets) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader ?? '')
  if (!match) throw new Error('Invalid signature')
  const [, timestamp, signature] = match
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error('Stale signature')
  const supplied = Buffer.from(signature, 'hex')
  const valid = secrets.some((secret) => {
    const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest()
    return timingSafeEqual(expected, supplied)
  })
  if (!valid) throw new Error('Invalid signature')
  return JSON.parse(rawBody.toString('utf8'))
}

The complete Node.js handler adds body limits, environment checks and durable acceptance, and has no dependencies.

Process events safely

  • Store, then acknowledge. Insert the eventId under a unique constraint and queue the work in the same transaction. Return any 2xx once it is stored; a duplicate you already stored is a success too.
  • Expect duplicates and any order. Delivery is at least once. Each event is a snapshot, so read the current payment before acting, and never let an older event overwrite newer state. Use observationVersion to recognise older snapshots.
  • Keep environments apart. Use separate secrets, handlers and queues for sandbox and live. A sandbox event must never fulfill a live order.

Event types

TypeSent when
payment.paidThe payment is verified as paid.
payment.failedA final failure is recorded.
payment.refundedThe payment is fully refunded.
payment.partially_refundedPart of the payment is refunded.
payment.disputedA dispute opens or its stage changes.
payment.dispute_wonA dispute is won.
payment.dispute_lostA dispute is lost.

Sandbox simulations send sandbox.simulation.* events instead. Handle them separately and never fulfill orders from them.

Payload

payment.paid
{
  "version": 1,
  "eventId": "01JA2Z4M8Q6V3N5R7T9W1X3Y5B",
  "type": "payment.paid",
  "createdAt": "2026-10-11T12:01:13Z",
  "environment": "sandbox",
  "data": {
    "id": "7d5c2f1e-4b8a-4c3e-9f21-6a0b8e4d1c55",
    "reference": "order-1042",
    "status": "paid",
    "method": "card",
    "amountCents": 1500,
    "refundedCents": 0,
    "netCents": 1500,
    "currency": "USD",
    "cardBrand": "visa",
    "cardLast4": "4242",
    "failureCode": null,
    "failureMessage": null,
    "createdAt": "2026-10-11T12:00:00Z",
    "paidAt": "2026-10-11T12:01:12Z",
    "refundedAt": null,
    "verifiedAt": "2026-10-11T12:01:12Z",
    "statusVersion": 2,
    "observationVersion": 3
  }
}

data is a snapshot of the payment. Nullable fields are present as null when unknown. Newer events can include more fields, so ignore fields you do not recognise. Every field is described in the webhook event reference.

Retries

Wegopay waits up to 10 seconds for a response. A non-2xx response, redirect or network error is retried up to six attempts in total: immediately, then after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. Retry-After is ignored. Contact Wegopay to replay events that exhausted their retries.

Rotate the signing secret

Rotate in the dashboard. Queued events keep the secret they were signed with, so accept both the new and the old secret until Wegopay confirms the old deliveries have drained. Disabling an endpoint does not cancel events already queued.

Never log secrets or payloads

Do not log signing secrets, authorization headers or full event bodies.