Checkout Sessions API

Create and manage checkout sessions for one-time payments and subscriptions.

Create a Checkout Session

POST/v1/checkout-sessions

Auth: Server key required

Request Body

{
  "amount": 99.99,
  "currency": "USD",
  "mode": "embedded",
  "paymentType": "oneTime",
  "successUrl": "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://yoursite.com/cancel",
  "customerEmail": "[email protected]",
  "customerFirstName": "Jane",
  "customerLastName": "Doe",
  "customerPhone": "+18765550142",
  "notifyCustomer": false,
  "sendReceipt": true,
  "notificationUrl": "https://yoursite.com/webhooks/checkout",
  "lineItems": [
    {
      "name": "Pro Plan",
      "quantity": 1,
      "unitAmount": 99.99,
      "description": "Monthly subscription"
    }
  ],
  "metadata": {
    "orderId": "order_12345"
  },
  "expiresAt": "2025-01-16T10:30:00Z",
  "allowSavedCards": true,
  "enableClickToPay": true
}

Response

{
  "id": "cs_live_abc123",
  "object": "checkout_session",
  "status": "open",
  "amount": 99.99,
  "currency": "USD",
  "mode": "embedded",
  "paymentType": "oneTime",
  "clientSecret": "cs_live_abc123_secret_xyz789",
  "url": null,
  "successUrl": "https://yoursite.com/success?session_id=cs_live_abc123",
  "cancelUrl": "https://yoursite.com/cancel",
  "customer": {
    "email": "[email protected]",
    "name": "Jane Doe"
  },
  "paymentStatus": "unpaid",
  "expiresAt": "2025-01-16T10:30:00Z",
  "createdAt": "2025-01-15T10:30:00Z"
}

Payer details

customerEmail, customerFirstName and customerLastName (at most 100 characters each) and customerPhone (at most 50) are all optional. Send whatever you have, so guest orders with only a name and phone still name their payer. They are stored on the session, returned by GET /v1/checkout-sessions/{id} and in checkout.session.* webhooks, and recorded as the payer on the invoice a one-time session creates.

Two optional switches decide what we send the payer. Each falls back to a setting in your dashboard's merchant configuration, and the session reports the result under the same name:

  • notifyCustomer: email and text the payer the session's invoice. Falls back to Message Checkout Customers, which is off unless you turn it on, so your own system stays the one talking to your customers. A session's invoice is never chased: no payment reminders, and it is never marked past due.
  • sendReceipt: email the payer a receipt when they pay (we need their email). Falls back to Send Checkout Receipts, which is on unless you turn it off.

Expiry

A session can be paid for 30 minutes unless you pass expiresAt (ISO 8601), between 10 minutes and 24 hours from now; anything else is refused with validation_error. After it, the session can't be paid, its status reads expired and checkout.session.expiredis sent. A session handed back for an invoice that already has an open one keeps that session's expiry.

Webhooks for one session

Pass notificationUrl to have this session's checkout.session.completed, checkout.session.expired and checkout.session.cancelled events sent there, and the payment.* and dispute.* events of the payments made through it (including refunds made in the dashboard, holds, and chargebacks), with no webhook endpoint set up in your dashboard. They use the usual envelope, retries and X-Webhook-Signature: sha256=… header, and go to your configured endpoints too if you have any.

  • The URL must be https on a public host (no localhost or private addresses), at most 2048 characters.
  • Send your own notificationSecret (16–200 characters) to verify every session with one secret, or leave it out and use the notificationSecret returned when the session is created. It is only in that response.

Return URL placeholder

Put {CHECKOUT_SESSION_ID} anywhere in successUrl or cancelUrl (as-is or percent-encoded). It is replaced with the session id when the session is created, so the hosted page redirects to, and the session reports, the final URL.

Paying an existing invoice

To collect an invoice you already issued, pass invoiceId (its numeric id) or invoiceReference (the reference code from its pay link, not the display invoice number). The session charges that invoice instead of creating one.

{
  "invoiceReference": "INV-7F3K2Q",
  "mode": "hosted",
  "paymentType": "oneTime",
  "successUrl": "https://yoursite.com/paid?session_id={CHECKOUT_SESSION_ID}"
}
  • Only with paymentType: "oneTime". Sending both fields, or lineItems, is a validation_error; description is ignored.
  • Amount, currency and payment options come from the invoice (amount = its total due). If you also send amount, currency or paymentOptionGroupId, they must match.
  • The invoice must be yours and in the same mode as your key; otherwise 404 invoice_not_found.
  • A paid, cancelled, rejected, expired, draft or zero-balance invoice is refused with invoice_not_payable.
  • While a session on the invoice is still open, creating another returns that same session.
  • If the session expires or is cancelled, your invoice is left as it was and can be paid later.
  • checkout.session.completed carries invoiceId and invoiceReference.

Retrieve a Checkout Session

GET/v1/checkout-sessions/{id}

Auth: Publishable key + client secret, the session's client secret alone (hosted page), or a server key

Response

{
  "sessionId": "cs_live_abc123",
  "status": "completed",
  "amount": 99.99,
  "currency": "USD",
  "customerEmail": "[email protected]",
  "customerFirstName": "Jane",
  "customerLastName": "Doe",
  "customerPhone": "+18765550142",
  "notifyCustomer": false,
  "sendReceipt": true,
  "allowGuestSavedCards": false,
  "paymentType": "one_time",
  "mode": "hosted",
  "expiresAt": "2025-01-15T11:00:00Z",
  "merchantName": "Yard Shop",
  "merchantLogoUrl": "https://cdn.example.com/logo.png",
  "allowedPaymentMethods": null,
  "successUrl": "https://yoursite.com/success",
  "cancelUrl": "https://yoursite.com/cancel",
  "clientId": 512,
  "subscriptionId": 88,
  "invoiceId": 4410,
  "refund": {
    "refundable": true,
    "reason": "refundable",
    "message": "This payment can be refunded in full.",
    "manualRefundRequired": false,
    "lastAttempt": null
  }
}

refundsays whether the session's payment can be refunded through the API right now, by the same rule the refund endpoint applies. The same object is on the checkout.session.* webhooks. When manualRefundRequired is true, the API can no longer refund the payment: refund the customer directly, or contact support. Reasons: refundable, no_payment, not_paid, already_refunded, refund_pending (a refund is under way), refund_failed (the last attempt failed or didn't finish; still refundable, retry), settled (already paid out to your balance; manual), method_not_supported (manual). lastAttempt describes the most recent refund attempt, if any: outcome (submitted, completed, pending or failed), attemptedAt and attempts.

clientId is the customer the session is for, subscriptionId is the subscription a subscription session created (once confirmed), and invoiceId is the invoice it pays. A subscription session needs a subscriptionPlanId and clientId of your own (see customers); anyone else's is refused at create with validation_error. The subscription is in the key's mode, and a session that starts a free trial (nothing to pay now) completes at once and still sends checkout.session.completed.

Statuses: created, payment_pending, processing, completed, expired, cancelled.

Confirm a Checkout Session

POST/v1/checkout-sessions/{id}/confirm

Auth: Publishable key + client secret

Request Body

{
  "paymentMethodId": "pm_card_visa_1234",
  "returnUrl": "https://yoursite.com/complete"
}

Response

{
  "id": "cs_live_abc123",
  "object": "checkout_session",
  "status": "complete",
  "paymentStatus": "paid",
  "paymentResult": {
    "id": "pay_xyz789",
    "status": "succeeded",
    "amount": 99.99,
    "currency": "USD"
  }
}

Tokenize Card

POST/v1/checkout-sessions/{id}/tokenize

Auth: Publishable key + client secret

Request Body

{
  "cardNumber": "4242424242424242",
  "expiryMonth": 12,
  "expiryYear": 2027,
  "cvc": "123",
  "billingDetails": {
    "name": "Jane Doe",
    "email": "[email protected]"
  }
}

Response

{
  "id": "pm_card_visa_1234",
  "object": "payment_method",
  "type": "card",
  "card": {
    "brand": "visa",
    "last4": "4242",
    "expiryMonth": 12,
    "expiryYear": 2027
  },
  "createdAt": "2025-01-15T10:31:00Z"
}

List Saved Cards

GET/v1/checkout-sessions/{id}/saved-cards

Auth: Publishable key + client secret

Response

{
  "object": "list",
  "data": [
    {
      "id": "pm_card_visa_1234",
      "card": {
        "brand": "visa",
        "last4": "4242",
        "expiryMonth": 12,
        "expiryYear": 2027
      },
      "isDefault": true
    },
    {
      "id": "pm_card_mc_5678",
      "card": {
        "brand": "mastercard",
        "last4": "8210",
        "expiryMonth": 6,
        "expiryYear": 2026
      },
      "isDefault": false
    }
  ]
}

Verify Saved Card

POST/v1/checkout-sessions/{id}/saved-cards/verify

Auth: Publishable key + client secret

Initiates OTP verification for a saved card before using it for payment.

Request Body

{
  "paymentMethodId": "pm_card_visa_1234"
}

Response

{
  "verificationId": "ver_abc123",
  "status": "otp_sent",
  "maskedPhone": "+1***4567",
  "expiresAt": "2025-01-15T10:35:00Z"
}

Confirm Saved Card OTP

POST/v1/checkout-sessions/{id}/saved-cards/confirm-otp

Auth: Publishable key + client secret

Request Body

{
  "verificationId": "ver_abc123",
  "otp": "123456"
}

Response

{
  "verified": true,
  "paymentMethodId": "pm_card_visa_1234"
}

Refund a Checkout Session

POST/v1/checkout-sessions/{id}/refund

Auth: Server key with the checkout.session.refund scope

Refunds the payment behind a completed session. Only the merchant that owns the session, in the same mode as the key, can refund it; any other session is a 404. Omit amount to refund all that is left. Card payments can be refunded in part, more than once, up to the amount paid: each call refunds amount and answers partially_refunded until nothing is left (refunded). Wallet QR payments refund in full only; a smaller amount is refused with partial_refund_not_supported. A payment already settled to your balance can still be refunded: the amount refunded is taken back from that balance first, and the refund is refused if the balance is short (the settlement fee is not returned). A wallet refund the wallet has accepted but not yet finished comes back with status pending_reversal and completes on its own; the payment.refunded webhook tells you when. Repeating a refund returns alreadyRefunded: true without contacting the processor.

Request Body

{
  "amount": 99.99,
  "reason": "Customer returned the item"
}

Response

{
  "sessionId": "cs_live_abc123",
  "paymentId": 4821,
  "paymentReferenceNumber": "PAY-4821",
  "status": "partially_refunded",
  "amount": 149.99,
  "currency": "USD",
  "alreadyRefunded": false,
  "refundAmount": 99.99,
  "refundedAmount": 99.99,
  "amountRefundable": 50.00
}

Errors: partial_refund_not_supported (400), validation_error (400, more than is left to refund), payment_not_refundable(400 if not yet paid or the rail can't be refunded through the API; 409 if another refund is in progress), refund_failed (409 also when your balance is short of a settled payment's refund), refund_failed (409 if the processor or wallet refused it, 502 for a processing error), nothing_to_refund (409, the session has no payment). A payment.refunded webhook follows each successful refund; for a partial one the payment stays Success and the webhook carries refundAmount (this refund) and refundedAmount (all so far).

refund_faileddoesn't mean nothing was refunded. Part of the refund may have reached the processor or wallet before the error, so don't refund the customer another way. Retry the same call instead: it's safe, because every payment method blocks a second refund of the same payment. The retry either completes the refund or reports it as already refunded.

{
  "success": false,
  "errorCode": "refund_failed",
  "errorMessage": "The refund did not complete: the wallet is unavailable. Part of it may still have reached the payment provider, so do not refund the customer another way; retrying is safe and will not refund twice. Contact support if retries keep failing.",
  "data": {
    "refundable": true,
    "reason": "refund_failed",
    "manualRefundRequired": false,
    "retryable": true
  }
}

A payment_not_refundable error's data has refundable: false, a reason from the session's list above, and manualRefundRequired. When that is true, refund the customer directly or contact support. If another refund of the payment is already in progress, the reason is refund_pending and no manual refund is needed: retry later to get its outcome.

Card Holds (Authorize Now, Capture Later)

Create a oneTime session with "captureMethod": "manual"to hold the payer's card instead of charging it: the payment is authorized only, and you capture it when the order ships, or release it. Only card options that can hold a card are offered on the payment page and the embedded form; a merchant with none gets a validation_error at creation. The session completes when the card is authorized (payment.authorized, then checkout.session.completed, the payment's status Authorized). Nothing is paid, settled or receipted until you capture. A hold lasts 7 days unless the payment option sets its own period; each hold carries its deadline as authorizationExpiresAt. One left alone is released then (payment.authorization_expired).

POST/v1/payments/{paymentId}/capture

Auth: server key with checkout.session.capture. Captures all that was authorized (no amount) or part of it, once; the rest lapses. The payment becomes a normal charge of the captured amount: payment.captured, then payment.succeeded; it pays its invoice, settles and can be refunded like any card payment. After a partial capture, an invoice the session created is lowered to the amount captured (the rest shown as a discount), so it reads paid; your own invoice keeps its total.

{ "amount": 80.00, "reason": "One item out of stock" }
POST/v1/payments/{paymentId}/release

Same auth. Releases the hold without charging (payment.released); an invoice the session created is cancelled with it.

{
  "paymentId": 4821,
  "referenceNumber": "PAY-4821",
  "status": "captured",
  "authorizedAmount": 99.99,
  "capturedAmount": 80.00,
  "currency": "USD",
  "authorizationExpiresAt": null,
  "capturedAt": "2026-10-12T15:04:00Z",
  "alreadyDone": false
}

status is authorized, captured, released or expired. Repeating a capture or release that went through returns alreadyDone: true. Errors: hold_not_capturable (409: not a card hold, not open, more than was authorized, lapsed, or the processor refused) and hold_operation_failed(502; retrying is safe). A payment that is another merchant's or the other mode's is 404.

Refund One Payment

POST/v1/payments/{paymentId}/refund

Auth: Server key required (scope checkout.session.refund)

Refunds one payment made through one of your checkout sessions (in full, or part of a card payment): for example one of several payments on the invoice a session pays. The payment id is the session's paymentId or a payment.* webhook's id. The body, answer, errors and repeat behaviour are those of the session refund above. A payment that is another merchant's, the other mode's or not from a checkout session is 404. A payment.refunded webhook follows, with the payment's amount and refundedAmount.

{
  "reason": "Customer cancelled the order"
}

Cancel a Checkout Session

POST/v1/checkout-sessions/{id}/cancel

Auth: Server key required

Answers 204 No Content. The session can no longer be paid: its status reads cancelled, the hosted page says it is closed, a confirm is refused, any outstanding wallet QR is withdrawn, an invoice the session created is cancelled with it, and checkout.session.cancelledis sent. A completed session can't be cancelled; refund it instead. A payment already under way when you cancel (a card in 3-D Secure) can still complete: refund it if it does.

Withdraw a Wallet QR Code

POST/v1/checkout-sessions/{id}/withdraw-qr

Auth: Publishable key + client secret, or the client secret alone (hosted page). Scope checkout.session.confirm.

Takes back the wallet QR code (JN Pay / Jam-Dex) a confirm issued, so the payer can choose another payment method. The session stays open. Call it before showing your method picker again: a code left live could be paid on top of whatever the payer picks next. No request body.

Response

{
  "sessionId": "cs_live_abc123",
  "outcome": "withdrawn",
  "status": "payment_pending"
}
  • withdrawn: the code can no longer be paid; the session is back to payment_pending. Confirm again with another option.
  • already_paid: the code had been paid (or the wallet refused to withdraw it because it was); the session is completed. Show the payment as done.
  • failed: the code could not be withdrawn and may still be paid. Nothing changed; keep waiting on it and try again later. message explains, in words you can show the payer.

A cancelled or expired session is refused (400); its codes were already withdrawn.

Get Session Status

GET/v1/checkout-sessions/{id}/status

Auth: Publishable key + client secret

Lightweight endpoint for polling session status (from the client, or from your server with a secret key when a webhook may have been missed). A card hold's session (captureMethod: "manual") also carries, once the card is authorized, the payment to capture or release, its holdStatus (authorized, captured, released, expired), the amount authorized and the capture deadline (authorizationExpiresAt, while it is open).

Response

{
  "status": "completed",
  "captureMethod": "manual",
  "paymentId": 4821,
  "holdStatus": "authorized",
  "authorizedAmount": 99.99,
  "authorizationExpiresAt": "2026-10-19T15:04:00Z"
}

SRC Lookup (Click to Pay)

POST/v1/checkout-sessions/{id}/src/lookup

Auth: Publishable key + client secret

Look up whether the customer is enrolled in Click to Pay (Secure Remote Commerce).

Request Body

{
  "email": "[email protected]"
}

Response

{
  "enrolled": true,
  "cards": [
    {
      "srcCardId": "src_card_abc123",
      "brand": "visa",
      "last4": "4242",
      "expiryMonth": 12,
      "expiryYear": 2027,
      "artUri": "https://cdn.quickpay.com/card-art/visa-4242.png"
    }
  ]
}

SRC Authenticate (Click to Pay)

POST/v1/checkout-sessions/{id}/src/authenticate

Auth: Publishable key + client secret

Authenticate a Click to Pay card selection and obtain a payment token.

Request Body

{
  "srcCardId": "src_card_abc123",
  "validationData": "otp_or_biometric_token"
}

Response

{
  "paymentMethodId": "pm_src_visa_4242",
  "token": "tok_src_abc123",
  "card": {
    "brand": "visa",
    "last4": "4242"
  },
  "authenticated": true
}