Skip to main content

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.

FieldRequiredNotes
amountYesPounds, not pence. Must be between 1 and 5000 inclusive. Decimals are fine, so £120.50 is 120.50.
order_referenceNoYour own reference, shown to the buyer. Defaults to Order.
return_urlYesWhere the buyer is sent once the split is set up.
cancel_urlNoWhere 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.
currencyNoDefaults to gbp. gbp is the only accepted value.
cancel_url is only offered before the split exists

When 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.

This endpoint takes pounds, not pence

"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, both https://your-shop.example/order/complete and https://Your-Shop.example:8443/anything?x=1 pass.
  • Subdomains are not covered. https://shop.your-shop.example/... does not match an allowed domain of your-shop.example.

cancel_url is only checked when you send one.

When the call returns 400

ResponseCause
Only gbp is supportedcurrency was set to anything other than gbp.
amount must be between £1 and £5000amount was outside that range, or was not a number. Remember it is in pounds.
return_url must be https and match your allowed return domainNo allowed_domain set, or the URL failed the hostname check above.
cancel_url must be https and match your allowed return domainSame check, on the cancel_url you sent.
Finish Stripe onboarding before accepting checkoutsYour 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.

If the request times out, do not retry blindly

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.