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
- In Dashboard → Settings → Webhook, choose Sandbox or Live and enter a public HTTPS URL on port 443.
- Copy the signing secret into your server’s secret manager. It is shown once; if you lose it, rotate it.
- 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:
Wegopay-Signature: t=1760184073,v1=5f2b…(64 hex characters)
Wegopay-Event-Id: 01JA2Z4M8Q6V3N5R7T9W1X3Y5B- Read the raw request body as bytes. Do not parse and re-encode the JSON first.
- Compute HMAC-SHA256 with the signing secret, exactly as issued, over
<t>.followed by the raw body. - Compare it with
v1in constant time. Reject timestamps more than five minutes from your clock. - Check that
Wegopay-Event-Idequals the body’seventId, and thatenvironmentmatches the endpoint.
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
eventIdunder a unique constraint and queue the work in the same transaction. Return any2xxonce 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
observationVersionto 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
| Type | Sent when |
|---|---|
payment.paid | The payment is verified as paid. |
payment.failed | A final failure is recorded. |
payment.refunded | The payment is fully refunded. |
payment.partially_refunded | Part of the payment is refunded. |
payment.disputed | A dispute opens or its stage changes. |
payment.dispute_won | A dispute is won. |
payment.dispute_lost | A dispute is lost. |
Sandbox simulations send sandbox.simulation.* events instead. Handle them separately and never fulfill orders from them.
Payload
{
"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