Checkout Sessions API
Create, retrieve, and update hosted checkout sessions — endpoints, request/response shapes, and payment verification.
Create hosted checkout experiences for your customers. A checkout session generates a unique payment URL that you redirect customers to. Decal handles payment collection, order tracking, and post-payment fulfillment.
- Base URL:
https://api.usedecal.com - Current API version:
v0 - Authentication: See Authentication
- Session schema: See Checkout types for the full CheckoutSession shape.
For a complete end-to-end walkthrough — creating sessions, fulfilling orders via webhook, handling abandoned carts — start with the Web Integration Guide or POS Integration Guide.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v0/checkout/sessions | Create a checkout session |
GET | /v0/checkout/sessions/:sessionId | Retrieve a session |
PATCH | /v0/checkout/sessions/:sessionId | Update a session |
Create a checkout session
POST /v0/checkout/sessionsCreates a new checkout session with an order and returns a payment URL to redirect your customer to.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
items | OrderItem[] | yes | Line items for the order (min 1). |
externalId | string | no | Your internal order identifier. Unique per organization and mode. See externalId and idempotency. |
taxes | OrderTax[] | no | Taxes applied to the order. |
discounts | OrderDiscount[] | no | Discounts applied to the order. |
expiresInMinutes | integer | no | Session lifetime, 15 -- 1440. Defaults to 60. |
customer | OrderCustomer | no | Attach or create a customer. |
shippingAddress | ShippingAddress | no | Pre-populate the shipping address for the underlying order. |
metadata | Metadata | no | Key/value data attached to the underlying order. |
successUrl | string | no | Redirect URL after successful payment. Supports {SESSION_ID} and {ORDER_ID} placeholders. |
callbackUrl | string | no | Webhook URL for payment events. Supports {SESSION_ID} and {ORDER_ID} placeholders. See Webhooks. |
requireFromCustomer | CheckoutRequireFromCustomer | no | Inputs the customer must provide on the hosted checkout page before payment (phone, shippingAddress, location). |
paymentDestination | string | no | Override the payment destination for this session's payment. See Payment Routing. |
Response
Returns a CheckoutSession object with status 201 Created.
curl -X POST https://api.usedecal.com/v0/checkout/sessions \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"name": "Classic Burger",
"quantity": 2,
"unitPrice": 1299
},
{
"name": "Fries",
"quantity": 1,
"unitPrice": 499
}
],
"taxes": [{ "type": "additive", "amount": 230 }],
"discounts": [{ "type": "percentage", "amount": 10, "name": "Weekend Special" }],
"successUrl": "https://yoursite.com/order/confirmed?session={SESSION_ID}",
"customer": {
"email": "jane@example.com"
}
}'const response = await fetch("https://api.usedecal.com/v0/checkout/sessions", {
method: "POST",
headers: {
Authorization: "Bearer sk_live_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [
{ name: "Classic Burger", quantity: 2, unitPrice: 1299 },
{ name: "Fries", quantity: 1, unitPrice: 499 },
],
taxes: [{ type: "additive", amount: 230 }],
discounts: [{ type: "percentage", amount: 10, name: "Weekend Special" }],
successUrl: "https://yoursite.com/order/confirmed?session={SESSION_ID}",
customer: {
email: "jane@example.com",
},
}),
});
const { checkoutSession } = await response.json();
// Redirect your customer to the checkout page
console.log(checkoutSession.url);
// => "https://pay.usedecal.com/s/clx8k2m9abc1234"Example response
{
"checkoutSession": {
"id": "cs_live_clx8k2m9abc1234",
"url": "https://pay.usedecal.com/s/clx8k2m9abc1234",
"status": "pending",
"active": true,
"customerId": "cust_live_abc123def456",
"failedAttempts": 0,
"successUrl": "https://yoursite.com/order/confirmed?session=cs_live_clx8k2m9abc1234",
"callbackUrl": null,
"expiresAt": "2026-04-13T12:00:00.000Z",
"createdAt": "2026-04-13T11:00:00.000Z",
"updatedAt": "2026-04-13T11:00:00.000Z",
"order": {
"id": "ord_live_xyz789",
"provider": "native",
"currency": "USD",
"status": "open",
"paymentStatus": "unpaid",
"amounts": {
"subtotal": 3097,
"tax": 230,
"discount": 310,
"tip": 0,
"total": 3017,
"paid": 0
},
"items": [
{
"id": "itm_1",
"name": "Classic Burger",
"quantity": 2,
"unitPrice": 1299,
"totalPrice": 2598,
"itemType": "product"
},
{
"id": "itm_2",
"name": "Fries",
"quantity": 1,
"unitPrice": 499,
"totalPrice": 499,
"itemType": "product"
}
],
"taxes": [
{
"id": "tax_cuid_abc",
"name": "Tax",
"type": "additive",
"rate": null,
"amount": 230,
"scope": "order"
}
],
"discounts": [
{
"id": "disc_cuid_abc",
"name": "Weekend Special",
"type": "percentage",
"amount": 10,
"scope": "order"
}
],
"createdAt": "2026-04-13T11:00:00.000Z",
"updatedAt": "2026-04-13T11:00:00.000Z"
}
}
}statusis the canonical session state — see CheckoutSessionStatus for all values and their meanings.- The
activeboolean is deprecated — preferstatus. See the full CheckoutSession shape.
Retrieve a checkout session
GET /v0/checkout/sessions/:sessionIdFetch a session by its ID to check its status, view the order, or confirm payment.
curl https://api.usedecal.com/v0/checkout/sessions/cs_live_clx8k2m9abc1234 \
-H "Authorization: Bearer sk_live_your_api_key_here"const sessionId = "cs_live_clx8k2m9abc1234";
const response = await fetch(`https://api.usedecal.com/v0/checkout/sessions/${sessionId}`, {
headers: {
Authorization: "Bearer sk_live_your_api_key_here",
},
});
const { checkoutSession } = await response.json();
if (checkoutSession.order.paymentStatus === "paid") {
console.log("Payment received!");
}Update a checkout session
PATCH /v0/checkout/sessions/:sessionIdExtend a session's expiry or attach metadata. Both fields are optional.
| Field | Type | Description |
|---|---|---|
extendExpiry | integer | Minutes to add to the current expiry time. |
sessionData | object | Arbitrary key-value data. Merged with existing values. |
curl -X PATCH https://api.usedecal.com/v0/checkout/sessions/cs_live_clx8k2m9abc1234 \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"extendExpiry": 30
}'const sessionId = "cs_live_clx8k2m9abc1234";
const response = await fetch(`https://api.usedecal.com/v0/checkout/sessions/${sessionId}`, {
method: "PATCH",
headers: {
Authorization: "Bearer sk_live_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
extendExpiry: 30,
}),
});
const { checkoutSession } = await response.json();
console.log(checkoutSession.expiresAt);
// Expiry extended by 30 minutes from its previous valueCancel the order behind a session
To void a checkout session, cancel its underlying order. This is useful when a customer abandons checkout, a POS operator needs to void a transaction, or an order was created by mistake.
POST /v0/orders/:orderId/cancelOnly orders with status: "open" and amounts.paid: 0 can be cancelled. The endpoint is idempotent
— cancelling an already-cancelled order returns 200. See
Cancel an order for full details.
URL template variables
Both successUrl and callbackUrl support the following placeholders, replaced with the formatted
typed IDs at session creation time:
| Placeholder | Replaced with |
|---|---|
{SESSION_ID} | The formatted checkout session ID (e.g. cs_live_abc1234). |
{ORDER_ID} | The formatted order ID (e.g. ord_live_xyz789). |
You can use either, both, or neither — placeholders are substituted globally, so repeating the same placeholder in a single URL is supported. Unrecognized placeholders are left untouched.
{
"successUrl": "https://yoursite.com/order/confirmed?session={SESSION_ID}&order={ORDER_ID}",
"callbackUrl": "https://yoursite.com/webhooks/decal?order={ORDER_ID}"
}After creation, both URLs contain the actual IDs:
https://yoursite.com/order/confirmed?session=cs_live_clx8k2m9abc1234&order=ord_live_xyz789
https://yoursite.com/webhooks/decal?order=ord_live_xyz789This lets your handlers look up the session or order without needing to store the mapping yourself.
externalId and idempotency
Pass an externalId to link the Decal order created for this session to your internal order
identifier (e.g. your cart ID, order number, or POS ticket ID). The value is stored on the order and
returned on every subsequent response.
externalId values are unique per organization and mode — creating a second session with the
same externalId in the same mode returns 409 Conflict with code: "DUPLICATE_EXTERNAL_ID". This
gives you safe idempotency: retrying a failed session-creation call with the same externalId
cannot create two orders.
- Max length: 255 characters. Empty strings are rejected.
- Scope: Unique per
(organization, mode). The sameexternalIdcan exist in both test and live mode simultaneously. - Returned as: the top-level
externalIdfield on the order object.
{
"items": [{ "name": "Classic Burger", "quantity": 1, "unitPrice": 1299 }],
"externalId": "cart_abc123"
}Payment destination override
Pass paymentDestination on session creation to override where this session's payment is routed
instead of the default orchestration flow. See
Payment Routing for the full guide.
Error codes
See Errors for the common error response shape.
| Code | Status | Description |
|---|---|---|
INVALID_SESSION_ID | 400 | Session ID format is invalid. |
ORGANIZATION_NOT_FOUND | 400 | API key is not associated with an organization. |
SESSION_NOT_FOUND | 404 | Session does not exist or belongs to another org. |
SESSION_CREATION_FAILED | 400 | Order or session could not be created. |
SESSION_UPDATE_FAILED | 400 | Session could not be updated. |
CREATE_FAILED | 400 | Generic creation error (see message for details). |
DUPLICATE_EXTERNAL_ID | 409 | An order with this externalId already exists for this org and mode. |
INVALID_PAYMENT_DESTINATION | 400 | paymentDestination value is not a valid destination ID or address. |
PAYMENT_DESTINATION_NOT_FOUND | 400 | No matching payment destination found for this organization. |
PAYMENT_DESTINATION_NOT_ACTIVE | 400 | Destination exists but is inactive, deleted, or not yet verified. |