Object model
How XPay's resources fit together. What you create, what you read, and the two IDs to store on your order record.
A successful payment on XPay is a small graph of related objects. You create the top one (a Checkout Session) and read the rest as nested fields. This page is the one-screen mental model of how those objects fit together, which IDs you'll see, and what to keep on your order record.
The objects
Checkout Session cs_*
├── customer cus_*
└── paymentIntent pi_*
├── charges[] ch_*
│ └── balanceTransaction txn_* (the receivable)
└── refunds[] re_*
└── balanceTransaction txn_* (the reversal)| Object | What it represents |
|---|---|
| Checkout SessionAPI | A configured checkout. Line items, customer fields, and the integration surface (hosted page, drop-in, elements). One per customer attempt. |
| CustomerAPI | The shopper's record. Contact, billing, and shipping details. Captured during checkout, or linked by customerId if you already have one. |
| Payment IntentAPI | The transaction. What money is meant to move, the method used, and the state it's in. The financial counterpart to the Checkout Session. |
| ChargeAPI | A money-move attempt. One try to authorize and capture money on the customer's payment method. A Payment Intent has more than one if the customer retried. |
| RefundAPI | A money reversal. Sends money back to the customer, full or partial, against a captured Charge. |
| Balance TransactionAPI | A ledger row. Every Charge and every Refund produces one. The signed value is what hits your balance. |
What you create vs what you read
You create Checkout Sessions and Refunds. Payment Intents, Charges, and Balance Transactions are read-only resources. You encounter them as nested fields in responses and as IDs in webhook payloads, but the API does not expose direct creation. Every payment flow starts at a Checkout Session.
The two creation surfaces:
POST /checkout/sessionsproduces a Checkout SessionAPI. Behind the scenes the customer's payment runs on a Payment Intent, which produces Charges, which produce Balance Transactions. You see all of them as nested fields on the session, but you don'tPOSTany of them yourself.POST /refundsproduces a RefundAPI against api_*(the Payment Intent for the payment) or ach_*(a specific Charge). Provide exactly one. The Refund produces its own Balance Transaction (the reversal).
Everything else is read-only. You retrieve a Checkout Session, you read its paymentIntent, you read its charges, you read their balance transactions. You never create them on your own.
Payment Links are a no-code feature created in the dashboard. When a customer opens a /p/plink_... URL, XPay creates the Checkout Session for that customer automatically. You still receive checkout.session.completed and the same nested object graph as any other integration.
Which IDs to store
Two IDs do different jobs. Keep both on your order record.
cs_*is your checkout reference. It identifies the customer's checkout context: which line items they bought, what fields they filled in, which integration surface they used. Use it for support, debugging, and re-rendering the customer's order in your own UI.pi_*is your transaction handle. It identifies the money: what was charged, what was refunded, what settled into your balance. Use it to issue refunds and to reconcile against payouts. One per successful payment.
Both arrive on the checkout.session.completed payload, so capture them at the same time and store both. Each one is the right tool for its own job, and neither replaces the other.
cus_* (if present) is the third id worth keeping when you want to re-find a returning customer across orders. Everything else (ch_*, txn_*) is reachable on demand from GET /checkout/sessions/:id or GET /refunds/:id and rarely needs to live on your order record.
How the objects connect
Checkout Session is the parent
The session has at most one Payment Intent (created when the customer submits the form) and at most one Customer (existing or created during checkout). Read the session, you have the rest.
Payment Intent holds the lifecycle
Every payment attempt on the session goes through one Payment Intent. Retries reuse it; they don't spawn a new one.
Charges and Refunds are the money events
A Charge is "money came in." A Refund is "money went back." Each produces a Balance Transaction, which is the row that settles into your balance and your payouts.
Balance Transactions are the ledger
The signed value of a Balance Transaction (positive for charges, negative for refunds) is the truth about your money. Charges and Refunds are how you describe the event; Balance Transactions are how the books balance.
When you reach into the deeper objects
For most integrations the answer is "rarely." The deeper objects exist so the data is consistent and traceable, not because you need to interact with them directly.
| You need to... | What to do |
|---|---|
| Fulfill an order after payment | Read cs_* on checkout.session.completed. That's the whole job for most integrations. |
| Issue a refund from code | POST /refunds with the pi_*. See the Refunds integration page. |
| Look at the exact charge that ran | Read paymentIntent.charges[] on the session response. |
| Reconcile against your balance and payouts | Read paymentIntent.charges[].balanceTransaction. Each carries the settled amount, fees, and which payout (if any) it landed in. |
| Look up a returning customer | Read cs.customer (or fetch CustomerAPI by cus_*). |
The events you receive
The one event every integration listens for is checkout.session.completed. The data.object is a full Checkout SessionAPI, identical to GET /checkout/sessions/:id, with the full nested graph attached. That single event is enough to fulfill the order and capture every ID you might want to store.
Other events (checkout.session.expired, refund.created, charge.refunded) are useful when you want to react to specific lifecycle changes outside the success path. The full list is on the Event reference.
Where to next
Checkout Session reference
The fields on the session you create, the lifecycle it moves through, and what you can configure.
Refunds
How to issue a refund against a pi_* (or a specific ch_*). Single API call.
Setting up a webhook
Receive checkout.session.completed and verify the signature.
Choose your integration
Pick the surface that builds the Checkout Session: Payment Links, Hosted, Drop-in, or Elements.