Skip to content

Checkout Flow

The whole journey, from a cart to a plan that has paid itself off, with every status change and every event marked. Quick Start is the same path with the calls to copy; this page is what happens between them.

%%{init: {'themeVariables': {'actorLineColor': '#dfdfdf'}}}%%
%% The lifeline is the one part Material's mermaid theme does not reach, and
%% mermaid's own default for it is purple. Set here because mermaid scopes that
%% rule by the diagram's generated id, which no stylesheet can outrank.
sequenceDiagram
    autonumber
    participant S as Customer
    participant M as Your store
    participant P as ShotPay

    M->>P: GET /v1/config
    P-->>M: terms, availability
    S->>M: Chooses layaway
    M->>P: POST /v1/sessions
    P-->>M: 201 · session pending · checkout_url
    M-->>S: Redirect to checkout_url
    S->>P: Reads the quoted plan
    alt Customer accepts
        S->>P: Signs in or creates an account
        S->>P: Accepts the plan
        P->>P: Cart Capture taken, plan opened
        P-->>M: contract.activated
        P-->>M: session.completed
        P-->>S: Back to return_url?session_id&status
        S->>M: Lands on the return page
        M->>P: GET /v1/checkout-sessions/{id}
        P-->>M: status completed · order marked paid
    else Customer walks away
        S->>P: Leaves by "Not now"
        P-->>M: session.cancelled
        P-->>S: Back to cancel_url?session_id&status
    end
    loop Each remaining installment
        P->>P: Collects on the due date
        P-->>M: contract.paid
    end
    P-->>M: contract.completed

Before the session

GET /v1/config reports whether this merchant may sell and on what terms. Read it at install and cache it.

Layaway is allow-listed per state, and states with no rule are refused. That is why customer_state is worth sending in the next step.

Opening the session

POST /v1/sessions — authenticated with the merchant's key.

The session is created pending and snapshots the merchant's terms — the down payment and the reservation fee — so an open quote never moves.

Token cs_ and 32 characters, returned as id. It is the credential for the customer-facing routes, so treat it as one.
Expiry 60 minutes by default, reported as session_ttl_minutes.
Webhooks None.

Store the token against your order.

The customer's page

checkout_url is a ShotPay-hosted page — the bridge page. It reads the session back with GET /v1/sessions/{token}, which takes no API key: the token in the URL is the credential.

The page shows the quoted plan: the total, the reservation fee, what the customer pays today, and every installment with its due date. Until the plan is confirmed the session's plan is null.

A session reads as expired the moment its deadline passes. session.expired follows shortly afterwards rather than on the stroke of the deadline, so treat the status as the truth and the event as the notification.

The page cannot be embedded in a storefront — it is served with frame-ancestors 'self' and a browser will refuse the frame. Customers arrive by full-page redirect, which is the one delivery that works everywhere, in-app browsers included.

Confirming

The customer signs in to ShotPay, or creates an account, in the panel at the top of the page. There is no guest checkout; the account is also where they track the plan afterwards.

Accepting the plan opens it in the name of whoever is signed in. On an installation with card payments configured, the customer picks or saves a method on the page first — a card by default, a bank account beside it — and that method backs the Cart Capture and every Installment Capture after it.

The two are priced differently, and the page says so: a card carries a flat $3.00 platform fee on every capture, settling to the fee MID with the Reservation Fee, and a bank account pays the scheduled amount and nothing more. The rate is snapshotted on the session when it opens, so a quote in flight cannot move, and it is carried onto the contract so the six weeks of captures that follow price off what the customer agreed to.

What happens, in order:

  1. The session moves pending → decision_made.
  2. A contract is opened pending from the session's quote, with its captures. The Cart Capture is due, the Installment Captures upcoming.
  3. The Cart Capture is taken synchronously. A decline is a failed checkout: the agreement stays pending, nothing is delivered, and the customer may try another method from the same page.
  4. When it lands the contract becomes active, contract.activated and then contract.paid are delivered.
  5. The session moves decision_made → completed.
  6. session.completed is delivered.

contract.activated always arrives before the first contract.paid, and session.completed after both. If you only handle one event, handle session.completed: it is the point at which the order is paid for. What to do when it lands is How your store finds out.

Confirming twice is safe

A session that already has a contract returns that contract rather than opening a second, and two confirmations racing each other resolve to the same one. A customer double-clicking cannot buy the same plan twice.

Walking away

A customer who decides against the plan leaves by the checkout page's "Not now" link, which cancels the session before sending them to your cancel_url. The session moves to cancelled and session.cancelled is delivered as they go.

That is the difference between hearing about an abandoned cart now and hearing about it when the session lapses an hour later, so it is the event to release held stock on.

The rest of the plan

Installment Captures come due on the schedule the plan quoted and are drawn by ACH without anyone asking. Each one that settles sends contract.paid — sequence 1 is the Cart Capture, and the Installment Captures are numbered from 1 by installment_number.

The customer can also pay the next one early from the same page — the earliest unpaid installment is the one settled.

When the final capture settles the contract completes and contract.completed is delivered.

When a payment fails

Only an Installment Capture can fail this way; a Cart Capture that declines is a failed checkout, not a delinquency.

When What happens Event
Due date Attempt 1 on the enrolled method. payment.failed if it fails
Attempt 2 One automated retry — a minute later on card, on receipt of the return on ACH. No third. payment.failed
T₀ Attempt 2 failed. The capture is past_due, the consumer is told by email and SMS, and autopay is suspended on that method. contract.past_due
T₀+1 Reminder. —
T₀+2 Grace ends. An $8 late charge is booked as its own receivable, never folded into the draw. The capture is delinquent. contract.delinquent
T₀+7 Final notice. —
T₀+10 Consumer default. The plan is defaulted, the product returns to your inventory, and the customer is refunded what they paid toward the layaway price less the restocking fee; the reservation fee is retained. contract.defaulted

The days are ShotPay's configuration and can differ per state. Both clocks run from T₀ — the failure notification — not from the due date. A Layaway plan unpaid ninety days after it opened is cancelled regardless of where the capture clock sits.

Statuses

Status Means
pending Open, waiting on the customer.
decision_made Accepted; the contract is being opened.
completed Done. The plan exists and the down payment landed.
cancelled Abandoned.
expired Lapsed before anyone acted. Terminal — open a fresh session.
Status Means
pending Opened; the Cart Capture has not landed.
active Running.
completed Every capture settled.
cancelled Ended early.
defaulted Ended unpaid.

All three resolutions are terminal.

Status Means
upcoming Not yet due.
due Due now, or being collected.
paid Settled.
past_due Attempt 2 failed; the grace period is running. Can still be paid.
delinquent Grace ran out; the late charge is assessed. Can still be paid.

What your store should record

When Do
POST /v1/sessions returns Store the session id against the order. Reuse it — a retry opens a second session against one order.
session.completed Call your fulfil function.
session.cancelled Release whatever the cart was holding.
The customer lands on return_url Call the same fulfil function, with the session_id on the URL.
contract.paid Optional. contract_reference and order_reference are both on the payload, so nothing has to be held in state to know which plan moved.
contract.completed Optional. The plan is settled; nothing further is owed.

fulfil is written out in full in How your store finds out; delivery rules are under Webhooks.