Create a split
You need a connected Stripe account first: shares are opened directly on it, so a split cannot be created without one. See Become a merchant.
GET /api/merchants/me reports stripe_charges_enabled: true once onboarding
is complete, and that is the flag to gate your own checkout on. Note that
POST /api/checkout-splits itself only checks that a connected account exists,
not that Stripe has enabled charges on it, so a half-onboarded account fails
later at Stripe with the raw Stripe message rather than a clean 400. The
hosted checkout endpoints do check the flag.
From the dashboard
Go to /merchant/splits/new, add each participant's email and amount, and
optionally an order reference. There is no order-total field: the total is the
sum of the amounts you enter, shown on the submit button. Every row needs an
email and a positive amount before the form will submit. The form does not
check the £1 minimum share, so a row under £1 submits and is then rejected
by the API with Each share must be at least £1. Then share the generated pay
links.
From the API
This endpoint requires a signed-in SharePay session belonging to the merchant's
owner, in an Authorization: Bearer <supabase token> header. Passing a
merchantId you do not own returns 403 Not your merchant. There is no
API-key path to this endpoint: if you want a flow your server can drive with an
API key, use the hosted checkout session instead, which
is a separate endpoint.
POST /api/checkout-splits
{
"merchantId": "<your merchant id>",
"orderReference": "ORDER-123",
"currency": "gbp",
"participants": [
{ "email": "alex@example.com", "amountMinor": 4000 },
{ "email": "sam@example.com", "amountMinor": 4000 },
{ "email": "jo@example.com", "amountMinor": 4000 }
]
}
amountMinor is in minor units, so £40.00 is 4000. It must be a positive
integer of at least 100, because each share must be at least £1. The
hosted checkout endpoint is the other way round and
takes its amount in pounds.
amountMinor is the person's share. Their card is charged that plus
SharePay's flat 20p service fee, so a 4000 share is a £40.20 charge, and you
receive the £40. See
what SharePay charges.
Returns 201 with { id, status: "pending", participants: [...] }. Each entry
in participants is:
| Field | Notes |
|---|---|
id | The participant row's id |
email | As you sent it |
amountMinor | Their share, in pence |
feeMinor | The SharePay service fee on their charge, in pence. Their card is charged amountMinor + feeMinor |
paymentIntentId | The Stripe PaymentIntent holding their share |
clientSecret | For confirming that hold yourself, if you are not using the hosted pay page |
payToken | What their pay link is built from |
This create response returns payToken, camelCase. The list and detail
endpoints in Manage splits return the same value as
pay_token, snake_case. Reading pay_token off the create response gives
you undefined.
A 201 also means we have emailed every participant except the creator their
invite, with a pay link in it. See
what we email your customers.
Request fields
| Field | Required | Notes |
|---|---|---|
merchantId | Yes | Must be a merchant the signed-in account owns |
orderReference | Yes | Any string. It is not unique and nothing dedupes on it. It appears on the Stripe charge description, in participant emails, and in webhook payloads |
currency | No | Defaults to gbp. It is not validated here. Anything you send is passed straight to Stripe, so a value other than gbp either opens charges in that currency on a GB account or fails with a raw Stripe error. Send gbp |
participants | Yes | Non-empty array of { email, amountMinor } |
Limits and validation
- No cap on participant count. The hosted checkout caps a split at 20 people; this endpoint does not cap it at all. Shares are opened as one live Stripe PaymentIntent per participant in a sequential loop, so a large array is slow and every element costs a real Stripe call. Size accordingly.
- Each share must be at least £1.
amountMinormust be a positive integer of100or more, and anything smaller is rejected with400 Each share must be at least £1.before any Stripe call is made, so nobody is charged and no hold is placed. There is no maximum per share, and no order total to check the sum against: the split's total is whatever the shares add up to. emailis only checked for being a non-empty string. It is not validated as an address, and an unreachable one simply means that person never gets their invite.
If your request times out
Neither create endpoint is idempotent, and neither accepts an idempotency key. A blind retry after a timeout creates a second split with a second set of card holds on your customers' cards. (The Stripe idempotency key used internally is scoped to a single call, so it stops a duplicate PaymentIntent within one request and nothing more.)
Record your orderReference against the returned id before retrying, and if
you are unsure whether the first attempt landed, check
GET /api/checkout-splits first, which is ordered newest first.
Errors
| Status | Body | Cause |
|---|---|---|
401 | Missing or invalid Authorization header | No Bearer token |
401 | Invalid token | Expired or bad session token |
403 | {"code":"consent_required","error":"Consent required", ...} | The signed-in account has not accepted the current terms or privacy policy |
400 | Validation failed | merchantId or orderReference not strings, or participants missing or empty |
400 | Each participant needs an email and a positive integer amountMinor | A participant failed the per-row check |
400 | Each share must be at least £1. | A participant's amountMinor is under 100 |
404 | Merchant not found | Unknown merchantId |
403 | Not your merchant | The merchantId belongs to another account |
400 | A checkout split needs at least one participant | Empty participant list reaching the engine |
400 | Merchant has no Stripe connected account | No stripe_account_id on the merchant |
400 | the raw Stripe error message | Any PaymentIntent failure. Nothing is captured on this path, so nobody is charged. We try to cancel the holds already placed in that call, and the split is left at creation_failed. See creation_failed |