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:
- The session moves
pending→decision_made. - A contract is opened
pendingfrom the session's quote, with its captures. The Cart Capture isdue, the Installment Capturesupcoming. - 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. - When it lands the contract becomes
active,contract.activatedand thencontract.paidare delivered. - The session moves
decision_made→completed. session.completedis 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.