docs
IntegrateWebhooks

Event reference

Every webhook event XPay can deliver, when it fires, and the object it carries.

Events are how XPay tells your server something happened: a payment succeeded, a refund landed, a customer was created. Every event delivered to your endpoint shares the same JSON envelope; what changes is the value of type and the resource carried inside data.object.

This page is the canonical list of events you can subscribe to. For the click path to subscribe, see Setting up an endpoint. For the verifier code, see Verifying signatures.

The event envelope

Every webhook delivery has the same top-level shape:

{
  "id": "evt_test_AbC123...",
  "object": "event",
  "api_version": "3.1.0",
  "created": "2026-05-01T12:00:00.000Z",
  "type": "checkout.session.completed",
  "livemode": false,
  "data": {
    "object": { "id": "cs_test_...", "object": "checkout.session" }
  }
}
FieldTypeMeaning
idstringEvent ID, prefixed evt_test_* or evt_live_*. Stable across retries. Use as your idempotency key.
objectstringAlways "event".
api_versionstringAPI version used to render data.object.
createdISO 8601 stringWhen the event was first recorded.
typestringWhat kind of event (see the tables below).
livemodebooleantrue for live-mode events, false for test-mode.
data.objectobjectThe resource the event is about. Identical shape to GET /<resource>/:id.

The data.object is identical to what the corresponding GET endpoint returns. If you can read a Checkout Session from GET /checkout/sessions/:id, your checkout.session.completed handler can read the same fields by the same names off data.object.

Checkout Session events

Fire on the lifecycle of a Checkout SessionAPI. These are the events most integrations key off.

EventFires whendata.object
checkout.session.completedA customer's payment on the session succeeds. The session's status is now complete.Checkout SessionAPI

Subscribe to checkout.session.completed to fulfill the order. The payload carries the resolved paymentIntent, customer, and lineItems, so a single event is enough for most fulfillment logic.

Charge events

Fire on the lifecycle of a ChargeAPI. A Charge is one attempt to move money on the customer's payment method. A successful payment produces one Charge; a customer who retried after a decline produces several.

EventFires whendata.object
charge.succeededA Charge captures successfully. Fires before checkout.session.completed for hosted-checkout payments.ChargeAPI
charge.failedA Charge attempt fails. Carries failureCode and failureMessage describing why.ChargeAPI
charge.refundedA successful Refund (full or partial) is applied to the Charge. The refunded amount is in amountRefunded. The Charge's refunds.data array carries every Refund applied so far.ChargeAPI

Use charge.* events when your data model tracks money at the charge level (e.g. multiple captures, retried payments). For most integrations, listening to checkout.session.completed and refund.* is enough.

Refund events

Fire on the lifecycle of a RefundAPI.

EventFires whendata.object
refund.createdA new Refund record is created.RefundAPI
refund.failedA Refund attempt fails. The Refund's status is failed.RefundAPI

Both API-issued and dashboard-issued refunds emit these. See Refunds for the API path.

charge.refunded (above) carries the parent Charge with the new Refund nested inside refunds.data, so you can key fulfillment off either side depending on which entity your data model tracks.

Customer events

Fire on the lifecycle of a CustomerAPI.

EventFires whendata.object
customer.createdA Customer record is created (via the API or the dashboard).CustomerAPI
customer.updatedA Customer record's properties change.CustomerAPI
customer.deletedA Customer record is deleted.CustomerAPI

For the difference between Registered and Guest customers, and how guest matching works, see Customer lifecycle.

Webhook monitoring

One event fires when a webhook itself fails. Useful for paging yourself when production deliveries break.

EventFires when
webhook_endpoint.delivery_failedA webhook delivery exhausts all retries. The first transition to failed for a given delivery emits one event.

Subscribe on a separate, healthy endpoint (a Slack relay, a status-page hook, an internal monitoring URL). The data.object carries:

  • id: the failed delivery's ID.
  • url: the endpoint URL that failed.
  • eventType: the event that couldn't be delivered.
  • webhookEndpointId: the failing endpoint's ID.
  • attemptCount: how many attempts were tried.
  • lastAttemptStatus: the last HTTP status returned, or 0 for network failures.
  • lastAttemptError: the error message from the last attempt.

Pair with Replaying & retries if you want to manually replay events once the underlying handler is fixed.

Order on a successful payment

A single payment fires more than one event. They arrive close together but not necessarily in order; treat each event as independent and dedup on event.id.

OrderEventWhy
1charge.succeededThe money moved.
2checkout.session.completedThe session is now complete. Fulfillment trigger.

For a refund:

OrderEventWhy
1refund.createdA Refund record was created.
2charge.refundedThe parent Charge's amountRefunded was updated.

You don't need to listen to all of them. Pick the events your data model needs and ignore the rest.

Reading data.object

The data.object field is the same shape you'd get from the matching GET endpoint. Don't reach for the API to re-fetch a resource right after receiving its event; the payload already has everything that endpoint would return.

For the canonical shape of each resource, see the API Reference Object pages:

For how those resources fit together (and which IDs to keep on your order record), see Object model.

Idempotency reminder

Every event can be delivered more than once: XPay retries on non-2xx, you may manually replay from the Workbench, and a successful response that didn't make it back to XPay produces a duplicate. Use event.id as the dedup key in your handler.

For the full pattern, see Verifying signatures → Idempotency.

Where to next

On this page