Checkout Sessions

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.

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

MethodPathDescription
POST/v0/checkout/sessionsCreate a checkout session
GET/v0/checkout/sessions/:sessionIdRetrieve a session
PATCH/v0/checkout/sessions/:sessionIdUpdate a session

Create a checkout session

POST /v0/checkout/sessions

Creates a new checkout session with an order and returns a payment URL to redirect your customer to.

Request body

FieldTypeRequiredDescription
itemsOrderItem[]yesLine items for the order (min 1).
externalIdstringnoYour internal order identifier. Unique per organization and mode. See externalId and idempotency.
taxesOrderTax[]noTaxes applied to the order.
discountsOrderDiscount[]noDiscounts applied to the order.
expiresInMinutesintegernoSession lifetime, 15 -- 1440. Defaults to 60.
customerOrderCustomernoAttach or create a customer.
shippingAddressShippingAddressnoPre-populate the shipping address for the underlying order.
metadataMetadatanoKey/value data attached to the underlying order.
successUrlstringnoRedirect URL after successful payment. Supports {SESSION_ID} and {ORDER_ID} placeholders.
callbackUrlstringnoWebhook URL for payment events. Supports {SESSION_ID} and {ORDER_ID} placeholders. See Webhooks.
requireFromCustomerCheckoutRequireFromCustomernoInputs the customer must provide on the hosted checkout page before payment (phone, shippingAddress, location).
paymentDestinationstringnoOverride 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"
    }
  }
}
  • status is the canonical session state — see CheckoutSessionStatus for all values and their meanings.
  • The active boolean is deprecated — prefer status. See the full CheckoutSession shape.

Retrieve a checkout session

GET /v0/checkout/sessions/:sessionId

Fetch 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/:sessionId

Extend a session's expiry or attach metadata. Both fields are optional.

FieldTypeDescription
extendExpiryintegerMinutes to add to the current expiry time.
sessionDataobjectArbitrary 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 value

Cancel 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/cancel

Only 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:

PlaceholderReplaced 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_xyz789

This 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 same externalId can exist in both test and live mode simultaneously.
  • Returned as: the top-level externalId field 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.

CodeStatusDescription
INVALID_SESSION_ID400Session ID format is invalid.
ORGANIZATION_NOT_FOUND400API key is not associated with an organization.
SESSION_NOT_FOUND404Session does not exist or belongs to another org.
SESSION_CREATION_FAILED400Order or session could not be created.
SESSION_UPDATE_FAILED400Session could not be updated.
CREATE_FAILED400Generic creation error (see message for details).
DUPLICATE_EXTERNAL_ID409An order with this externalId already exists for this org and mode.
INVALID_PAYMENT_DESTINATION400paymentDestination value is not a valid destination ID or address.
PAYMENT_DESTINATION_NOT_FOUND400No matching payment destination found for this organization.
PAYMENT_DESTINATION_NOT_ACTIVE400Destination exists but is inactive, deleted, or not yet verified.