payment-gateway.app Docs
API Reference

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:

  1. Inbound webhooks - from payment providers (Stripe, Mollie, GoCardless, PayPal) to the Main Backend. These update transaction state automatically.
  2. 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.

ParameterValuesDescription
typestripe, mollie, gocardless, paypalProvider type
providerIdMongoDB ObjectIdThe 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 EventEffect on Transaction
payment_intent.succeeded-> completed
payment_intent.payment_failed-> failed
charge.refundedRecords an immutable provider-confirmed refund
charge.dispute.createdOpens chargeback record
charge.dispute.closedRecords 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 StateEffect on Transaction
paid-> succeeded
failed-> failed
canceled-> cancelled
expired-> cancelled
Refund updatesRecords an immutable provider-confirmed refund
Chargeback updatesRecords verified dispute state and final outcomes

GoCardless Events Handled

GoCardless EventEffect on Transaction
payments.confirmed-> succeeded
payments.failed-> failed
payments.cancelled-> cancelled
mandates.cancelledMarks mandate as cancelled
mandates.failedMarks mandate as failed
refunds.createdRecords 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 FamilyEffect on Transaction
Checkout/order completion events-> succeeded
Payment capture denied/failed events-> failed
Payment capture refunded eventsRecords an immutable provider-confirmed refund
Dispute and chargeback eventsRecords verified dispute state and final outcomes

Dispute outcomes and customer holds

Provider dispute callbacks use verified provider evidence:

  • open and under_review keep the transaction in active dispute state.
  • lost and accepted are final merchant-loss outcomes and can create a Customer Risk Hold for future checkout policy.
  • won reverses 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

  1. Copy Webhook Signing Secret from Sites -> Edit Site (or create a site and note the secret).
  2. When creating each checkout session, set ipnUrl to your listener URL (http is allowed for local development; use HTTPS in production).
  3. 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.

VersionWho uses it on the compatible releaseAutomatic delivery budget
v1Existing/unversioned sites and new sites explicitly selecting v1Three total attempts, 1-second then 2-second delays, two-minute cycle.
v2New sites by default and existing sites explicitly selecting v2At 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
}
FieldDescription
deliveryIdPresent on the versioned gateway; historical v1 may omit it.
idTransaction ID (MongoDB ObjectId hex string).
sessionPublicIdOptional signed checkout-attempt correlation emitted when available. It is never a delivery or deduplication identity.
externalReferenceOptional merchant reference from the checkout (omitted when empty).
statusInteger 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
}
FieldRequirementDescription
schemaVersionRequired; 2Identifies the signed v2 envelope.
deliveryIdRequiredOpaque identifier for one logical delivery. It is stable across retries.
eventVersionRequired; positive integerMonotonically increasing version for the transaction's notification stream.
occurredAtRequired; RFC 3339 timestampWhen the event was created by the gateway.
sessionPublicIdOptional checkout correlationPublic 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, statusSame meaning as v1Payment 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

HeaderDescription
X-Signature-TimestampUnix timestamp (seconds, UTC) when the gateway signed the request.
X-Signature-HMAC-SHA256Hex-encoded HMAC-SHA256 of {timestamp}.{rawBody} using the Site Webhook Signing Secret (whsec_...).
X-IPN-VersionThe versioned gateway sends 1 or 2. Required for v2 and set to 2; historical signed v1 can omit it.
X-IPN-Delivery-IDThe 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: 2 and reject a missing or mismatched X-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:

  1. Verify the timestamp/HMAC over the exact bytes and validate the version and matching header/body delivery ID.
  2. Atomically claim deliveryId and apply the intended effect. If the ID was already claimed, return the same successful 2xx response without repeating the effect.
  3. Store the highest accepted eventVersion per transaction. Ignore an event older than that version so a delayed pending or failed notification cannot regress a later completed, refunded, or disputed state.
  4. 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

On this page