Utopia PaymentsDocs

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" }
  }'
Response
{
  "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.

GET /payments/pay_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d
{
  "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; null until the payment settles.
  • net_amount: what you keep, total_amount − refunded_amount − fee_amount; null until 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_BALANCE while 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, so net_amount falls below zero after a full refund.
  • Send an Idempotency-Key, so a retried request does not refund twice.

On this page