Webhooks & IPN
Inbound provider webhooks and outbound IPN configuration for real-time payment event notifications.
Webhooks & IPN
The Payment Gateway handles webhooks at two levels:
- Inbound webhooks - from payment providers (Stripe, Mollie, GoCardless, PayPal) to the Main Backend. These update transaction state automatically.
- Outbound IPN (Instant Payment Notifications) - from the Main Backend to your server when a transaction's status changes.
Provider callbacks and IPN delivery are not served by the Admin Backend. Configure provider webhook URLs on your Main Backend host ({$MAIN_BACKEND_DOMAIN} - documentation uses webhook.yourcompany.com as an example; distinct from api.* / {$ADMIN_BACKEND_DOMAIN}). That service exposes /hooks/... at the server root and checkout runtime under /api/v1/....
Inbound Provider Webhooks
Endpoint
On your Main Backend base host (no /api/v1 prefix):
POST https://webhook.yourcompany.com/hooks/{type}/{providerId}Hosted equivalent: POST https://webhook.payment-gateway.app/hooks/{type}/{providerId} - Hostnames & DNS conventions.
| Parameter | Values | Description |
|---|---|---|
type | stripe, mollie, gocardless, paypal | Provider type |
providerId | MongoDB ObjectId | The provider record ID (from Admin Panel) |
Configure this URL in your provider dashboard as the webhook destination. The gateway verifies provider authenticity with the provider-specific method below and rejects callbacks that do not match the configured provider, organization, or stored payment identifier.
Stripe Events Handled
| Stripe Event | Effect on Transaction |
|---|---|
payment_intent.succeeded | -> completed |
payment_intent.payment_failed | -> failed |
charge.refunded | Records an immutable provider-confirmed refund |
charge.dispute.created | Opens chargeback record |
charge.dispute.closed | Records the verified final dispute outcome |
Mollie Events Handled
Mollie webhook bodies identify a payment, refund, or chargeback to verify. The gateway fetches the canonical records with the configured API key and checks the provider, organization, account, and stored payment ID before applying a state change. Duplicate deliveries are guarded by durable event claims and the external refund or chargeback ID.
For next-generation webhooks, create a Mollie webhook for each mode you use. Use Snapshot payloads and select the six payment events (payment.authorized, payment.canceled, payment.expired, payment.failed, payment.paid, payment.pending), the six refund events (refund.queued, refund.pending, refund.processing, refund.refunded, refund.failed, refund.canceled), and chargeback.received and chargeback.reversed. Mollie may need to enable the chargeback events for your account. Copy that webhook's signing secret into the Mollie provider's Webhook Signing Secret field. The webhook mode, API key, and signing secret must match. Classic payment-ID callbacks and direct chargeback-ID callbacks can be unsigned because the gateway re-fetches the referenced records from Mollie; next-generation event_* callbacks require a valid X-Mollie-Signature HMAC-SHA256 signature. See Mollie's create-webhook reference, refund webhook event list, and next-generation webhook signing guide.
| Mollie Payment State | Effect on Transaction |
|---|---|
paid | -> succeeded |
failed | -> failed |
canceled | -> cancelled |
expired | -> cancelled |
| Refund updates | Records an immutable provider-confirmed refund |
| Chargeback updates | Records verified dispute state and final outcomes |
GoCardless Events Handled
| GoCardless Event | Effect on Transaction |
|---|---|
payments.confirmed | -> succeeded |
payments.failed | -> failed |
payments.cancelled | -> cancelled |
mandates.cancelled | Marks mandate as cancelled |
mandates.failed | Marks mandate as failed |
refunds.created | Records refund |
PayPal Events Handled
PayPal callbacks are verified with the PayPal transmission headers and the configured PayPal Webhook ID before the gateway applies the event to a transaction. Duplicate deliveries are guarded with processed-event claims before capture, refund, dispute, or status side effects.
| PayPal Event Family | Effect on Transaction |
|---|---|
| Checkout/order completion events | -> succeeded |
| Payment capture denied/failed events | -> failed |
| Payment capture refunded events | Records an immutable provider-confirmed refund |
| Dispute and chargeback events | Records verified dispute state and final outcomes |
Dispute outcomes and customer holds
Provider dispute callbacks use verified provider evidence:
openandunder_reviewkeep the transaction in active dispute state.lostandacceptedare final merchant-loss outcomes and can create a Customer Risk Hold for future checkout policy.wonreverses that dispute's payment effect. It does not undo a separate refund.
Receipt-backed return tax and document obligations remain separate from webhook acceptance. A successful callback is not proof that a credit note or tax record was materialized.
Signature and Authenticity Verification
Stripe: The gateway validates the Stripe-Signature header using your configured Stripe Webhook Secret (whsec_...).
Mollie: The gateway checks the X-Mollie-Signature HMAC against the configured Webhook Signing Secret for next-generation event_* callbacks. A missing or invalid signature is rejected. Classic payment-ID and direct chargeback-ID callbacks can arrive unsigned and are authenticated by fetching canonical payment and adjustment records from Mollie with the configured API key. If a signature is supplied, it must verify; the gateway never accepts a supplied signature without a configured secret. It checks tenant, provider, account, payment, and adjustment ownership before recording a refund or chargeback.
GoCardless: The gateway validates the Webhook-Signature header using your GoCardless Webhook Secret.
PayPal: The gateway validates PayPal transmission headers against the configured PayPal Webhook ID through PayPal's verification API.
If authenticity verification fails, ownership checks fail, or the payload is invalid, the gateway returns 400 and logs the attempt. Do not disable webhook verification in production.
Verified refund and dispute callbacks use immutable operation identities and reject conflicting payload reuse. Delayed callbacks cannot regress a later operation state. See Refunds and Returns for initiation, direct readback, and tax/document boundaries.
Webhook authenticity and idempotency are part of the payment-data evidence boundary. See Payment Data Boundary for the customer-facing evidence status and residual responsibilities.
Outbound IPN (to Your Application)
When you create a checkout (Admin Checkouts -> Create or POST /api/v1/checkouts/{siteId}/create on the Admin API host), you can set ipnUrl to the HTTPS endpoint on your infrastructure that should receive IPN posts for transactions created from that checkout. The URL is stored on the transaction (see Transactions -> [transaction] -> IPN Information in the admin UI for the aggregate signed-v1 delivery result and manual retry).
Signing uses the Webhook Signing Secret from the Site that owns the checkout: Sites -> Add New Site or Sites -> Edit Site -> Webhook Signing Secret (often whsec_...). Regenerate it with Regenerate if it is compromised; then update every integration that verifies IPN signatures.
Configuring IPN
- Copy Webhook Signing Secret from Sites -> Edit Site (or create a site and note the secret).
- When creating each checkout session, set
ipnUrlto your listener URL (httpis allowed for local development; use HTTPS in production). - Verify incoming posts using the headers and algorithm below (examples also appear under Payment Session API -> Webhook / IPN in the admin UI).
IPN contract versions
These versioned-delivery controls require the gateway release that exposes IPN format in Sites. Existing or unversioned sites remain on signed v1; new sites default to v2. A new site can explicitly select v1 for an older receiver. Upgrade to a compatible published receiver before choosing v2. Signed v1 remains supported through 30 June 2027, without an automatic date-based shutdown. See Migrating to IPN v2.
The gateway sends one format per logical notification. Its body, destination, selected version and identity are fixed when accepted, including during automatic retries and manual replay. A site rollback to v1 applies to fresh checkouts; checkouts already using v2 keep v2 for later events. Do not negotiate a version from the HTTP response or fall back to v1 after a v2 failure.
Verify the exact received body with the Site Webhook Signing Secret before reading any fields. The signature scheme is unchanged for both versions.
| Version | Who uses it on the compatible release | Automatic delivery budget |
|---|---|---|
| v1 | Existing/unversioned sites and new sites explicitly selecting v1 | Three total attempts, 1-second then 2-second delays, two-minute cycle. |
| v2 | New sites by default and existing sites explicitly selecting v2 | At most 20 attempts within 48 hours; the attempt limit often exhausts around 14 hours. |
IPN v1 payload (legacy JSON body)
Each POST body is a small JSON object:
{
"deliveryId": "01HV...",
"id": "67c8e2f7d6ef0dc8a3fa2011",
"sessionPublicId": "sess_pub_checkout_attempt_123",
"externalReference": "order_12345",
"status": 2
}| Field | Description |
|---|---|
deliveryId | Present on the versioned gateway; historical v1 may omit it. |
id | Transaction ID (MongoDB ObjectId hex string). |
sessionPublicId | Optional signed checkout-attempt correlation emitted when available. It is never a delivery or deduplication identity. |
externalReference | Optional merchant reference from the checkout (omitted when empty). |
status | Integer transaction status code (same numeric codes used internally by the gateway). |
Interpret status using your server-side mapping or the Transactions documentation - outbound IPN does not use string event names like transaction.succeeded.
The versioned gateway sends X-IPN-Version: 1 and X-IPN-Delivery-ID matching the signed body deliveryId even for v1; it omits schemaVersion, eventVersion and occurredAt. Historical v1 senders can omit these version/delivery headers and body delivery ID. Maintained receivers accept both signed v1 shapes.
V1 senders also include sessionPublicId when the transaction retains the checkout-attempt public identity. Receivers may use it for checkout correlation, but must not use it as a delivery identifier or duplicate key.
IPN v2 payload (JSON body)
Version 2 retains the payment fields and adds an immutable delivery identity and event ordering metadata:
{
"schemaVersion": 2,
"deliveryId": "01HV...",
"eventVersion": 42,
"occurredAt": "2026-07-26T18:00:00Z",
"id": "67c8e2f7d6ef0dc8a3fa2011",
"sessionPublicId": "sess_pub_checkout_attempt_123",
"externalReference": "order_12345",
"status": 2
}| Field | Requirement | Description |
|---|---|---|
schemaVersion | Required; 2 | Identifies the signed v2 envelope. |
deliveryId | Required | Opaque identifier for one logical delivery. It is stable across retries. |
eventVersion | Required; positive integer | Monotonically increasing version for the transaction's notification stream. |
occurredAt | Required; RFC 3339 timestamp | When the event was created by the gateway. |
sessionPublicId | Optional checkout correlation | Public identity of the checkout attempt that created the transaction. It is separate from deliveryId and must not be used to deduplicate IPN delivery. |
id, externalReference, status | Same meaning as v1 | Payment fields described above. |
sessionPublicId identifies a checkout attempt. deliveryId is the opaque identity of one logical IPN delivery and remains stable when that delivery is retried. A checkout attempt can produce more than one notification, so receivers must use deliveryId, not sessionPublicId, for v2 delivery deduplication.
IPN request headers
| Header | Description |
|---|---|
X-Signature-Timestamp | Unix timestamp (seconds, UTC) when the gateway signed the request. |
X-Signature-HMAC-SHA256 | Hex-encoded HMAC-SHA256 of {timestamp}.{rawBody} using the Site Webhook Signing Secret (whsec_...). |
X-IPN-Version | The versioned gateway sends 1 or 2. Required for v2 and set to 2; historical signed v1 can omit it. |
X-IPN-Delivery-ID | The versioned gateway sends it for both formats. Required for v2 and must exactly equal the signed body's deliveryId; historical v1 can omit it. |
rawBody must be the exact JSON bytes as received (no re-formatting) so the signature matches.
Official WooCommerce and aMember Pro plugins implement this verification for you.
IPN signature verification
Verification example (Go):
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func verifyIPN(rawBody []byte, timestamp, signatureHex, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(rawBody))))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHex))
}Verification example (Node.js):
const crypto = require("crypto");
function verifyIPN(rawBody, timestamp, signatureHex, secret) {
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHex),
);
}[!CAUTION] Always verify both the timestamp and the HMAC before trusting the payload. For v2, also require
X-IPN-Version: 2and reject a missing or mismatchedX-IPN-Delivery-ID. Reject stale timestamps if you need replay protection. Failing to verify allows attackers to forge payment confirmations.
IPN retry behavior
The sender treats delivery as successful when your endpoint returns any HTTP 2xx status code. For v2, transient network failures, timeouts, 408, 429, and applicable 5xx responses are durably retried with validated exponential backoff and bounded jitter for a default maximum of 48 hours. Safe Retry-After values are honored within the policy cap. Invalid/blocked targets and non-retryable client errors are terminal.
Delivery is at least once: an acknowledgement can be lost after your receiver has completed work, so the gateway can send the same deliveryId again with a fresh timestamp and signature. v1 keeps its legacy short retry behavior during the migration period because a v1 receiver cannot be assumed to be duplicate-safe.
Delivery history and recovery
The main backend owns the durable IPN queue. Automatic v2 retry stops at the earlier of the configured attempt limit (20 by default) and the 48-hour deadline. V1 retains three total attempts in the same durable queue, with 1-second then 2-second delays and a two-minute automatic cycle. It does not adopt the long v2 retry policy. A transient receiver or network failure schedules another attempt; an exhausted or terminal delivery needs operator attention.
On the Admin API, authorized viewers can inspect:
GET /api/v1/transactions/{id}/ipn-deliveries?state={state}&cursor={cursor}GET /api/v1/transactions/{id}/ipn-deliveries/{deliveryId}GET /api/v1/transactions/{id}/ipn-deliveries/{deliveryId}/attempts?cursor={cursor}GET /api/v1/transactions/{id}/ipn-commands?state={state}&cursor={cursor}
Authorized editors can enqueue recovery using POST to the delivery's /retry path, or POST /api/v1/transactions/{id}/ipn-commands/{commandId}/retry when construction failed before a delivery existed. For a pending or scheduled delivery, this advances the existing cycle without resetting its attempts or deadline. A terminal delivery starts a fresh bounded replay cycle. A 202 response means queued, not delivered. If an attempt is already active, the API returns 200 with state: "delivering" and does not queue another attempt. The older POST /api/v1/transactions/{id}/retry-ipn endpoint remains supported.
Retries preserve the original delivery ID, payload, version and callback URL. Each HTTP attempt receives a fresh signature timestamp. Do not clear the receiver's duplicate/order state, create a different delivery ID, or send both versions to force a retry.
History exposes bounded operational metadata, not callback URLs, payloads or secrets. Detailed attempts expire after 30 days. Compact ordering and effect receipts must remain for later manual replay. The aggregate transaction ipnResponse remains available.
Build an idempotent, ordered v2 receiver
Before applying any business effect, a v2 receiver must:
- Verify the timestamp/HMAC over the exact bytes and validate the version and matching header/body delivery ID.
- Atomically claim
deliveryIdand apply the intended effect. If the ID was already claimed, return the same successful 2xx response without repeating the effect. - Store the highest accepted
eventVersionper transaction. Ignore an event older than that version so a delayedpendingorfailednotification cannot regress a later completed, refunded, or disputed state. - Retain delivery-ID records for at least the gateway's configured retry window plus an operational safety margin. Keep compact ordering and effect receipts beyond attempt-log retention for later manual replay; expiration of detailed logs must not allow a duplicate business effect.
Do not use id plus status as the v2 duplicate key: distinct events can legitimately share those values. Do not return a failure merely because a notification is a duplicate or older event; a successful 2xx acknowledgement stops unnecessary delivery attempts.
Migrating from v1 to v2
Upgrade compatible published receivers and the matching gateway main/Admin/frontend release before permitting v2 traffic. Existing sites stay on v1 until explicitly changed; new sites default to v2. Follow Migrating to IPN v2 for testing, the support period through 30 June 2027, old callback URL retention and drain-before-downgrade instructions. That date does not disable v1; removal requires a separately announced major release after migration and drain.
Optional readiness probe
The probe is an informational check, not a payment or version negotiation. It uses normal signature headers, X-IPN-Version: 2, and a matching X-IPN-Delivery-ID:
{
"schemaVersion": 2,
"eventType": "ipn.test",
"deliveryId": "probe-unique-id",
"occurredAt": "2026-09-01T12:00:00Z"
}Authenticate and validate this exact shape before any order lookup or payment effect. Reject hybrid probe/payment payloads. Return HTTP 200 with:
{
"schemaVersion": 2,
"eventType": "ipn.test",
"deliveryId": "probe-unique-id"
}A generic OK or unrelated 2xx response does not establish readiness. WooCommerce and the website receiver support the probe; native aMember requires normal checkout/payment qualification instead.
Queue it with POST /api/v1/sites/{id}/ipn-readiness and {"transactionId":"<existing checkout transaction>"}. Read the result at GET /api/v1/sites/{id}/ipn-readiness/{commandId}. The transaction must belong to that site and organization. A site without a suitable transaction can use a normal test checkout first. The probe neither creates an endpoint configuration nor changes ipnVersion.
Responding to IPN
For payment notifications, return a 2xx response only after authenticating and durably accepting the payload. If work is asynchronous, persist the event and deduplication claim before acknowledging it. A readiness probe instead requires the exact HTTP 200 JSON echo above.
HTTP/1.1 200 OK