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.
requestobject | nullThe API request that created the event: { id, idempotency_key }. request.idempotency_key is the Idempotency-Key you sent on that request (see Idempotency), or null if you didn't send one.

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. lineItems lists only what the customer bought; optional add-ons they didn't add aren't included.

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

When a delivery exhausts all retries, XPay emails your account's owners, admins, and developers. The email names the endpoint URL and the event type that couldn't be delivered, so you know which integration to look at. These alerts cover Live mode deliveries only. Failures come by email rather than a webhook on purpose, since a webhook to a broken endpoint would just fail too.

For the retry schedule behind that alert, and how to replay a delivery once you've fixed the handler, see Replaying & retries.

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