DocsAPI ReferenceCheckout Sessions
Checkout Sessions API
A Stripe-style HTTP API for accepting payments from your own apps and websites. Create a session on your server, let the customer pay on the hosted page or embedded in your frontend, then verify before fulfilling.
Basics
| Base URL | https://saleonix.com/api/v1 |
| Authentication | Bearer key in the Authorization header (or X-Api-Key). Keys in query strings are rejected. |
| Currency | EUR only — there is no currency parameter; sending one returns currency_unsupported. |
| Units | All amounts are integer cents: 4318 = €43.18. Floats and numeric strings are rejected. |
| Format | JSON bodies with Content-Type: application/json (required on POST; missing or wrong type returns 415 unsupported_media_type). |
Integration style
| Style | Flow | Best for |
|---|---|---|
| Hosted checkout | Your server creates a session with your secret key and redirects the customer to the returned url. Saleonix renders the payment page (simulated outcome in TEST, real 3DS in LIVE). | The only supported path today — works with the React SDK's redirectToCheckout too. |
Embedded checkout (confirming a session directly from your frontend with your publishable key) is not currently available— 3-D Secure requires the card to be entered on Saleonix's own page in both TEST and LIVE. See "Confirm a session" below.
Create a checkout session
/api/v1/checkout/sessionsSecret keyA session charges in exactly one of three modes:
- Price mode — pass
price_id(+ optionalquantity); the amount is read from your catalog on the server. - Line items mode (WooCommerce, Shopify, and other cart-plugin integrations) — pass
line_items, an array of up to 100{ price_id, quantity }entries; each is resolved from your catalog exactly like price mode, andamount_totalis the sum. Use this when a cart contains several distinct registered products — Saleonix is merchant of record and needs to know what was actually sold, not just the total. - Amount mode (off by default) — pass
amount(+ requireddescription); your server states the total directly, with no catalog link. This requires the Amount-mode checkout permission, which Saleonix grants per account on request; without it, requests containingamountare rejected with400 amount_mode_unsupported. Useprice_idorline_itemsinstead.
Passing more than one (or none) of price_id / amount / line_items is rejected. In all threemodes the customer's browser never carries an amount — only your server decides what is charged.
Amount mode is off unless it was enabled for you
amount returns 400 amount_mode_unsupported. It is a per-account permission Saleonix grants on request — and it is enforced in TEST as well as LIVE, so what you can build against in test mode is exactly what will work in live. If you need it, contact support; otherwise use price_id or line_items.What is required
No single parameter is required on its own. What the endpoint requires is that the body carries exactly one mode selector — either price_id or line_items (or amount, which is disabled). Sending none of them, or more than one, returns 400 parameter_invalid. Choose by what the customer is buying:
| What you are selling | Use | Notes |
|---|---|---|
| One product | price_id | Optionally with quantity for several units of that same product. |
| Several different products (a cart) | line_items | The only way to sell more than one product in one session. Each entry carries its own price_id and quantity; do not send a top-level quantity. |
| An opaque total | amount | Off by default — returns 400 amount_mode_unsupported unless the Amount-mode checkout permission was granted to your account. |
Inside line_items, each entry's price_id is required and its quantity is optional (defaults to 1). Every parameter outside the mode selector is optional. The smallest valid request is therefore a single field:
Minimal request body
{
"price_id": "clpr1ce..."
}The Content-Type: application/json header is required on this endpoint; the Idempotency-Key header is optional but strongly recommended (see below). There is no currency parameter — sending one returns currency_unsupported.
| Field | Type | Required | Description |
|---|---|---|---|
| price_id | string | Conditional | Required only when you are not sending line_items — one product per session. A price in your store (find it on the product page in the dashboard). A price_ prefix is tolerated. Must be active, one-time and EUR — recurring prices return price_mode_unsupported, unknown or another store's ids return 404 resource_missing. Mutually exclusive with line_items and amount. |
| quantity | integer | No | Integer 1–100, default 1. Price mode only — sending it alongside line_items or amount is rejected with parameter_invalid (set quantity per entry inside line_items instead). |
| line_items | array | Conditional | Required when the session sells more than one product — this is the only way to put several products in one session. 1–100 entries, each { price_id, quantity? }. price_id is required per entry and resolved exactly like the top-level one; quantity is optional (integer 1–100, default 1). All prices must be EUR (the only supported currency) and belong to your store. Mutually exclusive with price_id and amount. |
| amount | integer | Permission | Requires the Amount-mode checkout permission, which is off by default. Without it, any body containing amount is rejected with 400 amount_mode_unsupported in test and live alike — use price_id or line_items. When granted: total in cents, minimum 50 (€0.50), maximum €10,000 by default. |
| description | string | Conditional | Required with amount, and valid only there — so it is usable only on accounts that hold the Amount-mode checkout permission. Sending it in price or line items mode is rejected with parameter_invalid. 1–255 chars, plain text; shown to the customer on the hosted checkout page. |
| success_url | string | No | Absolute URL, max 2000 chars; the customer is redirected here after payment with ?session_id=cs_... appended. https is required in live mode (http is allowed in test mode for local development); anything else returns invalid_success_url. |
| cancel_url | string | No | Stored for client-side use. Validated exactly like success_url (absolute URL, max 2000 chars, https in live mode); anything else returns invalid_cancel_url. |
| customer_email | string | No | A valid email address, max 254 chars. Prefills the customer record and is echoed back on reads. |
| metadata | object | No | Up to 20 keys (1–40 chars each). Values may be strings, numbers or booleans (coerced to strings, ≤500 chars each). Echoed back on reads. See Working with metadata below. |
| expires_at | integer | No | Epoch seconds. Clamped to 30 min – 24 h from now; default 24 h. A value outside that window is clamped, not rejected. |
Working with metadata
metadata is a free-form key/value bag for your own bookkeeping — your internal order id, cart id, customer reference, campaign, whatever lets you match a Saleonix session back to a record in your system. Saleonix never interprets it; it is stored with the session and echoed back verbatim on every read, on the hosted checkout result and in webhook payloads.
Creating metadata
{
"price_id": "clpr1ce...",
"metadata": {
"order_id": "1066",
"cart_id": "c_9f2a",
"customer_ref": "CUST-4471",
"source": "woocommerce",
"items": 3,
"gift": true
}
}Rules, all enforced at create time:
- It must be a flat JSON object — arrays, strings and nested objects are rejected with
metadata_invalid. - At most 20 keys. Each key must be 1–40 characters.
- Values may be strings, numbers or booleans only. Numbers and booleans are coerced to strings, so
{ "qty": 3 }reads back as{ "qty": "3" }— compare as strings on your side.null, arrays and objects as values are rejected. - Each value is at most 500 characters after coercion.
- Omitting
metadata(or sendingnull) is fine — reads then return an empty object,{}, nevernull. - Metadata is part of the idempotency request hash: replaying an
Idempotency-Keywith different metadata returns409 idempotency_key_reused.
Never put secrets in metadata
Price mode
Create session — price mode
curl -X POST https://saleonix.com/api/v1/checkout/sessions \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345" \
-d '{
"price_id": "clpr1ce...",
"quantity": 1,
"success_url": "https://example.com/thanks",
"customer_email": "buyer@example.com",
"metadata": { "internal_ref": "12345" }
}'Line items mode (carts / WooCommerce / Shopify)
Create session — line items mode
# A cart of 2 distinct registered products (WooCommerce/Shopify-style)
curl -X POST https://saleonix.com/api/v1/checkout/sessions \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wc-order-1066" \
-d '{
"line_items": [
{ "price_id": "clpr1ce1...", "quantity": 2 },
{ "price_id": "clpr1ce2...", "quantity": 1 }
],
"success_url": "https://shop.example/thanks",
"customer_email": "buyer@example.com",
"metadata": { "order_id": "1066" }
}'Amount mode (requires the Amount-mode checkout permission)
Create session — amount mode
# A cart of 3 items + shipping − coupon = €43.18 → amount: 4318 (cents)
curl -X POST https://saleonix.com/api/v1/checkout/sessions \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wc-order-1066" \
-d '{
"amount": 4318,
"description": "Order #1066 — example-shop.com (3 items incl. shipping)",
"success_url": "https://shop.example/thanks",
"customer_email": "buyer@example.com",
"metadata": { "order_id": "1066" }
}'Response 201
checkout.session
{
"id": "cs_test_aBcD...",
"object": "checkout.session",
"url": "https://saleonix.com/checkout?sid=...&exp=...&sig=...",
"client_secret": "cs_test_aBcD..._secret_XyZ...",
"status": "open",
"payment_status": "unpaid",
"livemode": false,
"currency": "eur",
"amount_subtotal": 10000,
"amount_tax": 1800,
"amount_total": 11800,
"quantity": 1,
"price_id": "clpr1ce...",
"product_id": "clxyz...",
"description": null,
"order_id": "clabc...",
"customer_email": "buyer@example.com",
"success_url": "https://example.com/thanks",
"cancel_url": null,
"metadata": { "internal_ref": "12345" },
"created": 1780000000,
"expires_at": 1780086400
}url— the hosted checkout page (a signed link, valid until the session expires).client_secret— returned only here. Store it server-side; pass it to your frontend only for embedded checkout. Treat it like a password for this one payment.
Idempotency
Idempotency-Key header with a unique value per order (max 191 characters). Retries with the same key replay the original session (including its client_secret) with HTTP 200 instead of creating and charging twice. Reusing a key with a different body returns 409 idempotency_key_reused.Retrieve a session
/api/v1/checkout/sessions/{id}Secret keySame shape as create, with client_secret: null and url: null once the session is no longer open (paid or expired). Fulfill the order when status is "complete" and payment_status is "paid". If you created the session in amount mode, also check that amount_total equals the total you expect before fulfilling.
Retrieve & verify
curl https://saleonix.com/api/v1/checkout/sessions/cs_test_aBcD... \
-H "Authorization: Bearer sk_test_xxx"| status | meaning |
|---|---|
open | awaiting payment |
complete | payment succeeded |
expired | deadline passed; create a new session |
| payment_status | meaning |
|---|---|
unpaid | no successful attempt yet |
paid | settled |
failed | an attempt was declined — the session cannot be retried; create a new one |
Confirm a session (embedded checkout — currently unavailable)
/api/v1/checkout/sessions/{id}/confirmPublishable key + client_secretAlways rejected, in TEST and LIVE alike
400 three_d_secure_required. This is deliberate: TEST rejects the same way LIVE does, so an integration built and tested against TEST behaves identically the moment you flip to LIVE instead of discovering the break in production.Use the hosted checkout flow instead, for both TEST and LIVE: redirect the customer to the session's urlfrom create. The card is entered on Saleonix's hosted page, which simulates the outcome in TEST (via the documented test cards below) and runs a real 3DS authentication against the gateway in LIVE. Then verify with GET /checkout/sessions/{id} before fulfilling, same as any hosted-checkout flow.
Test cards (entered on the hosted page in TEST mode — not sent to this endpoint)
| Card | Result |
|---|---|
4242 4242 4242 4242 | Approved |
4343 4343 4343 4343 | Declined |
Building checkout with the SDK?
redirectToCheckout, plus safe-redirect scheme checking. Use it if you can.Errors
All errors share one envelope:
Error shape
{ "error": { "type": "card_error", "code": "card_declined", "message": "..." } }| HTTP | type | Typical codes |
|---|---|---|
| 400 | invalid_request_error | parameter_invalid, invalid_json, session_expired, invalid_success_url, invalid_cancel_url, unsupported_test_card, price_inactive, price_mode_unsupported, currency_unsupported, amount_mode_unsupported, metadata_invalid, idempotency_key_invalid, three_d_secure_required (confirm endpoint, LIVE sessions only) |
| 401 | authentication_error | missing_api_key, invalid_api_key, wrong_key_type, invalid_client_secret, environment_mismatch |
| 402 | card_error | card_declined, invalid_card_number, invalid_expiry, expired_card, invalid_cvc |
| 403 | invalid_request_error | capability_disabled — the Payments API channel is turned off for this account (live mode only). Contact support to have it re-enabled. |
| 404 | invalid_request_error | resource_missing |
| 409 | invalid_request_error | session_already_complete, session_payment_failed, order_already_processed, idempotency_key_reused |
| 415 | invalid_request_error | unsupported_media_type |
| 429 | rate_limit_error | rate_limited, too_many_attempts (honor Retry-After) |
| 500 | api_error | internal_error, payment_outcome_unknown, settlement_failed |
Security model
- No browser-facing request ever carries an amount: price mode derives amounts from the catalog; amount mode accepts them only on the secret-key endpoint. The confirm endpoint ignores injected amount fields.
- Currency is never client-settable — EUR is pinned server-side.
- Keys are verified by SHA-256 hash; raw keys are never stored or logged.
client_secretis hashed at rest and returned exactly once.- Concurrent confirms cannot double-charge: the order is claimed atomically before the gateway is called.
- Card data is forwarded to the payment gateway only — never persisted or logged.
- TEST and LIVE are fully isolated: keys, sessions, and money never cross.
- Per-IP, per-key and per-session rate limits; all auth failures are audited.
