Integration guides
Accept a payment with hosted checkout
Your server creates a checkout session. The buyer pays on a page that Merxian hosts. Your webhook endpoint receives the result.
On this page
Before you start#
You need these items:
- A sandbox API key with the scopes
checkout:writeandcheckout:read. See Get an API key. - At least one price in your catalog. Each line of a checkout session names a price ID, such as
price_4Wm9TqLz2Xb7KdN8r. See Products and prices. - A webhook endpoint that receives
payment.*,checkout.*, andtransaction.*events. See Configure an endpoint.
How the flow works#
- Your server creates a checkout session. Merxian returns the session with its
url. - Your server sends the buyer to the
url. - The buyer pays on the hosted page. Merxian calculates the tax, when it applies, and collects the payment. Then it sends the buyer to your
successUrl. - Merxian sends the result to your webhook endpoint.
- Your server fulfils when the result is a success.
The return to successUrl and the webhook event are independent. Either one can arrive first.
1. Create the checkout session#
Give exactly one of these two fields:
transactionDraft: a currency and catalog prices with quantities. Merxian creates a draft transaction for the session.transactionId: adraftorreadytransaction that you created before. Use this when you build the transaction yourself. See Build a transaction.
Set successUrl and cancelUrl. Both must use https. Set clientReferenceId to your own reference, so you can match the session to your records.
curl -X POST https://api.merxian.com/v1/checkout-sessions \
-H "Authorization: Bearer $MERXIAN_API_KEY" \
-H "Idempotency-Key: 9b3f6c1e-2a47-4d8b-b5e0-7c1d2e3f4a5b" \
-H "Content-Type: application/json" \
-d '{
"transactionDraft": {
"currency": "EUR",
"lineItems": [{ "priceId": "price_4Wm9TqLz2Xb7KdN8r", "quantity": 1 }]
},
"successUrl": "https://shop.example.com/checkout/success",
"cancelUrl": "https://shop.example.com/checkout/cancel",
"clientReferenceId": "A-1042",
"customerEmail": "ada@example.com",
"customerCountry": "DE"
}'// A plain HTTP example with fetch in Node.js 18 or later. It is not an SDK.
const response = await fetch('https://api.merxian.com/v1/checkout-sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MERXIAN_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
transactionDraft: {
currency: 'EUR',
lineItems: [{ priceId: 'price_4Wm9TqLz2Xb7KdN8r', quantity: 1 }],
},
successUrl: 'https://shop.example.com/checkout/success',
cancelUrl: 'https://shop.example.com/checkout/cancel',
clientReferenceId: 'A-1042',
customerEmail: 'ada@example.com',
customerCountry: 'DE',
}),
})
const session = await response.json()The response has status: "open", the transactionId, and the url. The summary in transaction shows the amounts in minor units. For example, total { "amountMinor": 14280, "currency": "EUR" } is €142.80.
The session expires 24 hours after creation by default. To change this, set expiresAt in epoch milliseconds, from 30 minutes to 7 days in the future.
2. Send the buyer to the hosted page#
Redirect the buyer to url, for example with an HTTP 303 response from your server.
If a retry of the create request replays the first result, the url of the replayed response does not include the token. In that case, expire the session and create a new one with a new Idempotency-Key. See Idempotency.
3. Handle the return#
After a successful payment, Merxian sends the buyer to successUrl. When the buyer cancels, Merxian sends the buyer to cancelUrl.
A return to successUrl is not proof of payment. Anyone can open that URL. Show a page that says the payment is in progress or complete, based on the state that your server knows. Do not fulfil in the request handler of successUrl.
4. Fulfil on the webhook event#
Fulfil when your webhook endpoint receives one of these events:
| Event | Use it when |
|---|---|
payment.succeeded |
One payment pays the transaction, which is the case for hosted checkout. |
transaction.completed |
You want one signal that the full amount of the transaction is collected. |
checkout.completed |
You want to update the buyer interface. Do not use it alone to release goods. |
Match the event to your records with data.object.transaction_id, or with client_reference_id on the checkout event. Before you act, verify the signature and check that you did not process the event id before. See Verify signatures and Process events.
5. Read the state as a fallback#
If an event does not arrive, read the resources from the API:
- GET
/v1/checkout-sessions/{sessionId}returnsstatusandpaymentStatusof the session. - GET
/v1/transactions/{transactionId}returnsstatusandpaymentStatusof the transaction.
A session with status: "complete" and a transaction with status: "completed" are paid.
Handle failures#
| Event | What it means | What to do |
|---|---|---|
payment.failed |
The payment was refused. The session returns to open. |
Nothing is required. The buyer can try again on the same page until the session expires. |
checkout.expired |
The session ended without a payment. status is expired, or cancelled when the buyer cancelled. |
Release reservations. Create a new session if the buyer still wants to pay. |
payment.canceled |
The open payment of an ended session was canceled. | Stop waiting for that payment. |
When a session created its own draft transaction and ends without a payment in progress, Merxian also cancels that transaction. You receive transaction.canceled.
Expire a session early#
To stop a buyer from paying, for example when stock runs out, call POST/v1/checkout-sessions/{sessionId}/expire. It closes an open or payment_pending session and cancels a payment that the session started. A complete or cancelled session returns 409.
curl -X POST https://api.merxian.com/v1/checkout-sessions/cs_6HvB2nQx9LmT4kWpR/expire \
-H "Authorization: Bearer $MERXIAN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"