Open a checkout session
/v1/sessionsAnnounces an order and returns the hosted checkout link to send the customer to. Called from your backend, never from the storefront.
Authentication — X-Client-Id and
X-Api-Key.
The session starts pending and lapses after session_ttl_minutes unless it
advances. A lapsed session cannot be transacted on, and it cannot be revived —
open a fresh one.
The merchant's terms — the down payment and the reservation fee — are snapshotted onto the session as it opens, so an open quote never moves.
Not idempotent unless you make it so
Sending the same order_reference twice opens two sessions. Send an
Idempotency-Key when you want a retry to be safe.
Idempotency
Pass an Idempotency-Key header — a UUID, one per checkout attempt — and a
replay of the same request returns the original session instead of opening a
second one. This is what makes a network timeout safe to retry: you get back the
session you already created, even if you never saw the first response.
| Same key, same body | Returns the original session. Nothing new is created. |
| Same key, different body | 409. |
| No key | A new session every time, as above. |
Keys are scoped to the merchant and mode that presented them, and expire after 24 hours — after that the same value is treated as new. Reuse one key across retries of a single attempt, and mint a new one for each new checkout.
A request that fails validation does not burn the key: fix the body and retry with the same one.
Request
| Header | Required | Notes |
|---|---|---|
Idempotency-Key |
No | Makes a retry of this request safe. Max 255 characters; longer is a 400. |
| Field | Type | Required | Notes |
|---|---|---|---|
order_reference |
string | Yes | Your own identifier for the order. Max 100 characters. |
total_amount |
integer | Yes | The order total in integer minor units (cents) — send 49999 for $499.99. Must sit inside the merchant's terms. |
return_url |
string | Yes | Where the customer lands after completing checkout. HTTPS only, max 2048 characters. |
cancel_url |
string | Yes | Where the customer lands if they back out. HTTPS only. |
customer_email |
string or null | No | |
customer_state |
string or null | No | The two-letter US state the order ships to, which the jurisdiction rules are applied against. Omit it and the merchant's own state is used instead, which is the more conservative reading. |
metadata |
object or null | No | Your own string key/value pairs, echoed back on the session and on the contract it becomes. At most 50 keys; keys max 40 characters, values max 500. Values must be strings, and are trimmed — omit a key rather than sending an empty value. |
curl -X POST "$SHOTPAY_URL/v1/sessions" \
-H "X-Client-Id: $SHOTPAY_CLIENT_ID" \
-H "X-Api-Key: $SHOTPAY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"order_reference": "ORD-1001",
"total_amount": 49999,
"customer_email": "customer@example.com",
"customer_state": "TX",
"return_url": "https://store.example/checkout/complete",
"cancel_url": "https://store.example/cart",
"metadata": { "order_id": "1001", "warehouse": "SEA-2" }
}'
$session = Http::withHeaders([
'X-Client-Id' => config('shotpay.client_id'),
'X-Api-Key' => config('shotpay.api_key'),
// One per checkout attempt, stored on the order so a retry reuses it.
'Idempotency-Key' => $order->shotpay_idempotency_key,
])->post(config('shotpay.url').'/v1/sessions', [
'order_reference' => $order->number,
'total_amount' => $order->total_cents,
'customer_email' => $order->email,
'customer_state' => $order->shipping_state,
'return_url' => route('checkout.complete'),
'cancel_url' => route('cart'),
'metadata' => ['order_id' => (string) $order->id, 'warehouse' => $order->warehouse_code],
])->json('data');
Response
201 Created — a Checkout Session.
{
"data": {
"id": "cs_9Xk2mQpR7vLzT4hB1nWsY6dF3jCgA8eU",
"status": "pending",
"order_reference": "ORD-1001",
"total_amount": 49999,
"down_payment_amount": 12500,
"down_payment_percentage": 25,
"checkout_url": "https://secure.shotpay.com/checkout/cs_9Xk2mQpR7vLzT4hB1nWsY6dF3jCgA8eU",
"return_url": "https://store.example/checkout/complete",
"cancel_url": "https://store.example/cart",
"expires_at": "2026-08-12T15:30:00+00:00",
"created_at": "2026-08-12T14:30:00+00:00",
"metadata": { "order_id": "1001", "warehouse": "SEA-2" }
}
}
Redirect the customer to checkout_url. Nothing is owed until they accept the
plan there — see Checkout Flow.
Errors
| Status | When |
|---|---|
400 |
The Idempotency-Key is longer than 255 characters. |
401 |
The key is missing, unknown, revoked, or paired with a client id from the other mode. |
403 |
With a reason: layaway is unavailable for this merchant or this customer's state. Without one: the merchant is not cleared to transact. |
409 |
This Idempotency-Key was already used with a different body. |
422 |
The request is wrong — most often a total outside the merchant's range. |
429 |
Rate limited. Back off for Retry-After seconds. |
See Error Handling for the reason
values and the response bodies.