One-time payments
Create a hosted checkout, send the customer to it, and fulfil the order from the webhook.
1. Create a checkout session
Send the products the customer is buying. Each item in product_cart is a
product you created, in the dashboard or with POST /products, and a quantity.
All items must share a currency.
Amounts are integer minor units: 34900 is AED 349.00. See
Currencies and amounts.
curl https://utopia-payments.com/api/v1/checkout_sessions \
-H "Authorization: Bearer $UTOPIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{
"product_cart": [{ "product_id": "pdt_3f2b8c1e9a4d4e7b8c2a1d5e6f7a8b9c", "quantity": 1 }],
"customer": { "email": "customer@example.com" },
"return_url": "https://your-store.com/thank-you",
"metadata": { "order_id": "1001" }
}'{
"object": "checkout_session",
"session_id": "cks_9b1e44a0c3d24f6e8a7b5c2d1e0f9a8b",
"kind": "payment",
"status": "open",
"checkout_url": "https://utopia-payments.com/checkout/cks_9b1e44a0c3d24f6e8a7b5c2d1e0f9a8b",
"total_amount": 34900,
"currency": "AED",
"product_cart": [
{ "product_id": "pdt_3f2b8c1e9a4d4e7b8c2a1d5e6f7a8b9c", "name": "Pro course", "quantity": 1, "unit_amount": 34900 }
],
"customer": {
"name": null,
"email": "customer@example.com",
"phone_number": null
},
"return_url": "https://your-store.com/thank-you",
"payment_id": null,
"subscription_id": null,
"metadata": { "order_id": "1001" },
"livemode": true,
"created_at": "2026-09-14T10:00:00.000Z",
"expires_at": "2026-09-15T10:00:00.000Z",
"completed_at": null
}Sessions last 24 hours. A session's customer holds only what you sent, with
the rest null; the customer record and its customer_id are created when the
payment succeeds, and the payment carries them. Retrieving the session later
fills in payment_id once a payment for it has succeeded, and completed_at
when the session was paid.
Carts without saved products
Carts that don't map to saved products, such as a store's order total, can price items inline:
await utopia.checkoutSessions.create({
product_cart: [{ name: 'Order #1001', unit_amount: 12550, currency: 'AED', quantity: 2 }],
return_url: 'https://your-store.com/orders/1001',
});2. The customer pays
The hosted checkout collects the customer's details and card in the secure
payment window. Afterwards the customer lands on your return_url with
payment_id, status and session_id appended.
Treat those query parameters as a hint for what to show. Fulfil orders from the
payment.succeeded webhook, or confirm with GET /payments/:id before
shipping anything.
3. Fulfil from the webhook
payment.succeeded carries the payment, including the metadata you sent, so
you can find the order it belongs to. See Webhooks.
{
"object": "payment",
"payment_id": "pay_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
"status": "succeeded",
"total_amount": 34900,
"refunded_amount": 0,
"fee_amount": 3012,
"net_amount": 31888,
"currency": "AED",
"description": "Pro course",
"customer": {
"customer_id": "cus_2c4e6a8b0d1f4a3c5e7b9d1f3a5c7e9b",
"name": "Layla Hassan",
"email": "customer@example.com",
"phone_number": "+971501234567"
},
"checkout_session_id": "cks_9b1e44a0c3d24f6e8a7b5c2d1e0f9a8b",
"subscription_id": null,
"billing_reason": "payment",
"metadata": { "order_id": "1001" },
"livemode": true,
"created_at": "2026-09-14T09:58:12.000Z",
"paid_at": "2026-09-14T10:00:00.000Z"
}Amounts are integers in the currency's minor unit (fils for AED).
fee_amount: Utopia's processing fee, fixed when the payment succeeds and kept after a refund;nulluntil the payment settles.net_amount: what you keep,total_amount−refunded_amount−fee_amount;nulluntil the payment settles, and below zero after a full refund because the fee is kept.
See Payment lifecycle for every checkout and payment status, and Customers for matching by email.
Refunds
Refund a succeeded payment from the dashboard (the payment's details,
Refund) or with the API. Leave amount out to give back all that is
left of the payment, or send part of it in the payment's minor unit:
curl https://utopia-payments.com/api/v1/payments/pay_…/refunds \
-X POST \
-H "Authorization: Bearer $UTOPIA_API_KEY" \
-H "Idempotency-Key: refund-order-1042" \
-H "Content-Type: application/json" \
-d '{"amount": 500}'The answer is the payment once the refund is recorded: status is
partially_refunded or refunded, and refunded_amount has grown. A
refund.succeeded event goes out with data.refund_amount, as it does for
any refund. The money goes back to the card the customer paid with, usually
within a few working days, depending on the customer's bank.
- A live refund is drawn on your balance in the payment's currency, and is
refused with
409 INSUFFICIENT_BALANCEwhile the balance does not cover it. - More than what is left of the payment is refused with
400 AMOUNT_TOO_LARGE. - A payment made today may be refundable only in full until it settles
overnight (
409 NOT_SETTLED); from the next day it can be refunded in part too. A payment charged in the customer's own currency is refunded only in full (409 FULL_REFUND_ONLY). - Utopia's processing fee is not returned: it stays in
fee_amount, sonet_amountfalls below zero after a full refund. - Send an
Idempotency-Key, so a retried request does not refund twice.