Hosted checkout
For taking splits from your own checkout, rather than the SharePay dashboard.
1. Create a checkout session
Send your API key as x-merchant-api-key on:
POST /api/merchants/checkout/sessions
Call this from your server. The API key must never reach the browser, and the header is not on the CORS allow-list, so a browser request would be stripped of it and rejected anyway.
{
"amount": 120.00,
"order_reference": "ORDER-123",
"return_url": "https://your-shop.example/order/complete",
"cancel_url": "https://your-shop.example/basket"
}
Returns 201 with { token, checkout_url }. The session is valid for 2 hours.
| Field | Required | Notes |
|---|---|---|
amount | Yes | Pounds, not pence. Must be between 1 and 5000 inclusive. Decimals are fine, so £120.50 is 120.50. |
order_reference | No | Your own reference, shown to the buyer. Defaults to Order. |
return_url | Yes | Where the buyer is sent once the split is set up. |
cancel_url | No | Where a buyer who backs out is sent. The hosted page renders a link to it, but only before the split is set up. See below. |
currency | No | Defaults to gbp. gbp is the only accepted value. |
cancel_url is only offered before the split existsWhen you send one, the hosted page renders a Cancel and return to <your business name> link below the card, and the session lookup returns the value
as cancel_url. Omit the field and no link is rendered at all: the page does not
fall back to return_url, because this flow only ever sends a buyer to
return_url on success and a merchant handler reading that hit could book an
order that was never placed.
The link only appears while the buyer is still signing in or choosing shares. It disappears the moment the split is created. From that point every participant has a card hold against them, and a link that merely walks back to your shop would read as "this releases the holds" when it does not. Unwinding a live split is cancel or refund, not navigation.
So a hit on your cancel_url means "the buyer walked away before placing
anything", and it is the only case you will hear about. A buyer who
authorises their share and then abandons the split sends you nothing, and
neither does one who closes the tab. Do not treat cancel_url as an
abandonment feed. Reconcile on the token instead: a session that never produces
a split, or a split that never reaches checkout_split.paid, is the real
signal. See
Reconciling without a webhook.
"amount": 12000 is read as £12,000 and rejected, because it is over the
£5000 ceiling. A £120 order is "amount": 120.
The direct POST /api/checkout-splits endpoint is the
other way round: it takes each participant's amountMinor in pence. The
two endpoints do not share a unit.
Set your allowed return domain first
allowed_domain must be set on your account before this call will succeed. Set
it on the Developers page in your dashboard, under "Allowed return domain".
While it is unset, every return_url is rejected.
What the check actually does, for both return_url and cancel_url:
- The URL must parse, and its scheme must be
https:. - Its hostname must equal
allowed_domain, compared case-insensitively. - The path, port and query string are not checked. With an allowed domain
of
your-shop.example, bothhttps://your-shop.example/order/completeandhttps://Your-Shop.example:8443/anything?x=1pass. - Subdomains are not covered.
https://shop.your-shop.example/...does not match an allowed domain ofyour-shop.example.
cancel_url is only checked when you send one.
When the call returns 400
| Response | Cause |
|---|---|
Only gbp is supported | currency was set to anything other than gbp. |
amount must be between £1 and £5000 | amount was outside that range, or was not a number. Remember it is in pounds. |
return_url must be https and match your allowed return domain | No allowed_domain set, or the URL failed the hostname check above. |
cancel_url must be https and match your allowed return domain | Same check, on the cancel_url you sent. |
Finish Stripe onboarding before accepting checkouts | Your account is not stripe_charges_enabled yet. See Become a merchant. |
An invalid or missing API key returns 401 Invalid API Key.
This endpoint is limited to 120 requests a minute, keyed by your API key,
and returns 429 Too many checkout session requests. Please slow down. beyond
that. RateLimit-* headers are on every response.
The endpoint is not idempotent and does not accept an idempotency key. A retry
creates a second session for the same order. Sessions are cheap on their own,
because a session holds no money, but two live sessions for one basket means a
buyer can set up two splits and place two sets of card holds. Record the
token against your order before retrying, and if you already have one, reuse
it rather than creating another.
2. Redirect the buyer
Redirect the buyer to checkout_url. SharePay hosts the buyer-facing page,
where they sign in and set the split up: inviting participants and choosing
amounts. The split itself is created there, not by your API-key call. The
buyer returns to your return_url when they are done.
On that page the buyer may split with at most 20 people, and their chosen
shares must add up exactly to the session amount. A split can only be set up
once per session: a second attempt gets
409 This checkout already has a split.
Each share must be at least £1. A buyer who gives someone less gets
400 Each share must be at least £1. on the hosted page and nothing is
created. That also caps how many ways a small order can go: a £15 order cannot
be split more than 15 ways, whatever the 20-person limit says.
Each participant is then charged their share plus a flat 20p SharePay service
fee, shown to them as three lines before they authorise. Your amount is
what you receive, less only your own Stripe fees. See
what SharePay charges.
Until they create the split, the page also offers a way back to your
cancel_url, if you sent one. Once the split exists that link is gone, and so
is any signal to you that the buyer changed their mind.
3. Store the token
token is not just an ingredient of checkout_url. It is the only handle your
server has on the order afterwards:
GET /api/merchants/checkout/sessions/<token>
returns the session's status and, once the buyer has set the split up, its
split_id, which is the same id webhooks carry as data.split_id. Since your
API key cannot read splits, this is the reconciliation path for a hosted
checkout. The full response, its 404 behaviour and how to poll it are in
Reconciling without a webhook.
Save token against your own order record at creation time, and treat it as a
secret: the lookup needs no credentials and exposes the order amount and your
business name.
Refunds on hosted checkout
You can cancel and refund the splits taken through your own checkout. The creator of one of these splits is the buyer, because the buyer is who sets it up on the hosted page, but cancel and refund accept the merchant the split was taken against as well, so both calls work for you.
They are dashboard operations, not API-key ones. Your API key does not
authenticate POST /api/checkout-splits/:id/cancel or .../refund; a
signed-in session on the SharePay account that owns the merchant does. Your
server cannot refund an order on its own.
Refund returns the whole split. To return one participant's share rather
than all of them, refund that charge in your own Stripe dashboard: every share
is a charge on your own connected account, and each participant's
stripe_payment_intent_id is on GET /api/checkout-splits/:id. Each of those
charges is the share plus the customer's 20p service fee. Refund one in full
and SharePay reverses its own fee, so the 20p comes back out of our balance and
not yours; refund one only in part and the fee stays with us. See
Manage splits.
A worked example
A minimal server-side handler that turns a basket into a SharePay checkout and redirects the customer:
// POST /checkout/sharepay on your own server
app.post("/checkout/sharepay", async (req, res) => {
const basket = await loadBasket(req.session.basketId);
const response = await fetch(
"https://api.share-pay.co.uk/api/merchants/checkout/sessions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-merchant-api-key": process.env.SHAREPAY_API_KEY,
},
body: JSON.stringify({
// Pounds, not pence, on this endpoint. £1 to £5000.
amount: basket.totalPence / 100,
order_reference: basket.orderReference,
return_url: "https://your-shop.example/order/complete",
// Where the "Cancel and return" link on the hosted page goes. Only
// offered before the buyer sets the split up, so a hit here means an
// untouched basket, never an abandoned split.
cancel_url: "https://your-shop.example/basket",
}),
},
);
if (!response.ok) {
const { error } = await response.json().catch(() => ({}));
return res.status(502).send(error ?? "Could not start SharePay checkout");
}
const { checkout_url, token } = await response.json();
// Keep the token: it is how you look this order up later, and the only
// split-state surface reachable with an API key.
await saveSharePayToken(basket.orderReference, token);
res.redirect(303, checkout_url);
});
When the buyer lands back on your return_url, treat it as "the split has been
set up", not "the money has arrived". Wait for the
checkout_split.paid webhook before you fulfil the order, and
reconcile on the token for any
order that never produces one.
A complete integration, including signature verification and fulfilment, is published as a reference storefront. See Testing.