API reference
Every endpoint your integration touches, in one place. Each row links to the page that explains it.
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://api.share-pay.co.uk |
| Sandbox | https://demo.share-pay.co.uk/api |
Credentials never cross between them. See Testing.
Authentication
Three different things authenticate the endpoints below, and mixing them up is the most common integration mistake.
| Auth | Header | What it reaches |
|---|---|---|
| API key | x-merchant-api-key: sp_prod_... | Starting a hosted checkout. Nothing else. Use it from your server only: a browser cannot send this header, and the key must never reach a web page anyway |
| Session | Authorization: Bearer <supabase token> | Everything under /api/merchants and /api/checkout-splits. This is a signed-in person, not your server |
| None | — | The lookups a buyer's browser makes. There is no password: the token in the address is the credential, so treat every one as a secret |
If your login belongs to more than one business, add
x-sharepay-merchant-id: <merchant_id> to session-authenticated calls, or they
answer 409 business_selection_required. See Being in more than one
business.
Amount units
There is no single rule. Check the endpoint.
| Where | Field | Unit |
|---|---|---|
POST /api/merchants/checkout/sessions | amount | Pounds. £120 is 120, and 12000 means £12,000 |
POST /api/checkout-splits | amountMinor | Pence. £40 is 4000, minimum 100 |
| Webhook payloads, session lookup, list responses | amount, total_amount | Decimal strings in pounds, two places |
Detail response (GET /api/checkout-splits/:id) | amount, total_amount | Decimal, unformatted. Parse it, do not assume a shape |
Every amount above is a share, before fees. The card is charged the share plus a flat 20p. See what SharePay charges.
Error shape
Every error is JSON with an error message:
{ "error": "Invalid API Key" }
Some carry a machine-readable code alongside it — merchant_exists,
no_merchant, no_stripe_account, consent_required,
business_selection_required, business_forbidden, business_not_found,
billing_card_required (add a card in Settings, Billing), billing_paused
(pay the unpaid SharePay bill in Settings, Billing).
Branch on code where there is one and on the status otherwise; never on the
message text. Validation failures on registration and invoicing add a
details array of { field, message }.
Elsewhere in these docs an error is written as its status and message for brevity; the body is always the object above.
Hosted checkout
The path a shop integrates. See Hosted checkout.
| Endpoint | Auth | Rate limit | Notes |
|---|---|---|---|
POST /api/merchants/checkout/sessions | API key | 120/min per key | Creates a session. amount in pounds. Optional line_items itemise the order and must sum to amount. Optional customer_email, customer_name and flat string metadata (20 keys, 500-char values, 4 KB). Send an Idempotency-Key header to make retries safe: the same key returns the same session for 24 hours; the same key with a different body is 409 idempotency_key_reused. Returns { token, checkout_url }. Needs allowed_domain set, and stripe_charges_enabled with a live key (a test key does not need Stripe) |
GET /api/merchants/checkout/sessions/:token | None | 60/min per IP | What the hosted page loads. Public shape only: status, line_items, split_id, customer_email, and mode (test or live) |
GET /api/merchants/checkout/sessions/:token/status | API key | 120/min per key | Reconciliation. The session with customer_*, metadata, and split (null until the payer chooses): status of pending, paid, canceled or refunded, approved_count, total_count, participants. Another merchant's token is a 404 |
POST /api/merchants/checkout/splits/:id/refund | API key | 120/min per key | Refund a paid order from your server. No amount refunds everything outstanding; an amount is spread pro-rata over the payers to the penny. reason and idempotency_key optional. 409 partial_failure reports a refund Stripe refused part-way, with what went through. See managing splits |
POST /api/merchants/checkout/splits/:id/cancel | API key | 120/min per key | Cancel an order from your server while its split is still pending: holds are released and nobody is charged. 200 {"status":"canceled"}, also when it was already cancelled. Any other status is 409 not_pending with the current status. See managing splits |
POST /api/merchants/checkout/sessions/:token/remind | API key or dashboard session | 120/min per key or IP | Emails every participant still pending. One per participant per 12 h, three calls per session; 429 session_cap_reached after that, 409 nothing_to_remind when there is nobody to chase. Returns { reminded, skipped, next_allowed_at } |
Splits
Session only — your API key authenticates none of these. See Refunds, disputes and order status.
| Endpoint | Roles | Notes |
|---|---|---|
POST /api/checkout-splits | Owner, Admin, Member | You set the split up. amountMinor in pence. No rate limit, and not safe to retry — a repeat makes a second split. Create a split yourself |
GET /api/checkout-splits | All | Your splits, newest first. Summary rows only |
GET /api/checkout-splits/:id | All | The full record, including stripe_payment_intent_id per participant and disputed_at |
POST /api/checkout-splits/:id/cancel | Owner, Admin, Member | Releases holds on a pending split. A 200 does not mean anything was cancelled — re-read the split |
POST /api/checkout-splits/:id/refund | Owner, Admin, Finance | Refunds the whole split, from the dashboard. Only from captured. For partial refunds use the API-key route above |
GET /api/checkout-splits/pay/:token | None | 60/min per IP. One participant's own view, for hosting the pay step yourself |
Your account
See Become a merchant.
| Endpoint | Roles | Notes |
|---|---|---|
POST /api/merchants/register | Any signed-in account | One business per login. Returns api_key and webhook_secret |
GET /api/merchants/me | All | Status, settings and your business_role. Reconciles from Stripe on read — poll this rather than waiting to hear from us |
POST /api/merchants/connect | Owner, Admin | Stripe onboarding link. One-shot, expires in ~5 minutes |
POST /api/merchants/connect/refresh | Owner, Admin | A fresh link for the same account |
GET /api/merchants/credentials | Owner, Admin | Reveals api_key and webhook_secret |
POST /api/merchants/credentials/rotate | Owner, Admin | 5 per 10 min per IP, shared. { target: "api_key" | "webhook_secret" } |
PATCH /api/merchants | Owner, Admin | Sets webhook_url and allowed_domain. An empty value clears the field |
GET /api/merchants/workspaces | Any signed-in account | Every business this login can reach |
Webhooks
See Webhooks.
| Endpoint | Roles | Notes |
|---|---|---|
GET /api/merchants/webhooks/deliveries?limit=50 | All | Delivery log, newest first. limit clamped to 1–100 |
POST /api/merchants/webhooks/test | Owner, Admin | 5 per 10 min per IP. Sends a sample checkout_split.paid |
POST /api/merchants/webhooks/deliveries/:id/resend | Owner, Admin | 5 per 10 min per IP. Fresh timestamp and signature, same data |
Deliveries carry X-SharePay-Signature: t=<unix>,v1=<hex>, which proves the
request came from us, plus X-SharePay-Event-Id (stable across retries, dedupe
on it) and X-SharePay-Attempt. A delivery that is not acknowledged with a 2xx
within 10 seconds is retried eight times over about 47 hours (nine attempts in
all), then given up on and the business emailed. v1 is an HMAC-SHA256 over <t>.<raw body>, keyed on
your webhook secret. Events: checkout_split.paid, checkout_split.canceled,
checkout_split.participant_authorised, checkout_split.refunded,
checkout_session.expired. Split
events carry session_token, customer_email, customer_name and metadata
from the session (all null for a split made on the dashboard). There is no
event for chargebacks or a split that failed.
Team
| Endpoint | Roles | Notes |
|---|---|---|
GET /api/merchants/team | All | Members, pending invites, and your own current_role |
POST /api/merchants/team/invites | Owner, Admin | 5 per 10 min per IP. Invitations last 7 days |
POST /api/merchants/team/invites/:id/resend | Owner, Admin | 5 per 10 min per IP. Replaces the previous link |
DELETE /api/merchants/team/invites/:id | Owner, Admin | Revokes a pending invitation |
PATCH /api/merchants/team/members/:id | Owner, Admin | { role }. Cannot reach the owner |
DELETE /api/merchants/team/members/:id | Owner, Admin | Cannot remove yourself |
POST /api/merchants/team/transfer-ownership | Owner | Needs the business name as confirmation. Demotes you to Admin |
GET /api/merchants/team-invites/:token | None | Shows an invitation before sign-in, with the email masked |
POST /api/merchants/team-invites/:token/accept | The invited person | Single use. Must be signed in as the invited address |
Rate limits at a glance
Only these endpoints are limited, and only these send back RateLimit-*
headers telling you what is left. Check your remaining budget on one of these,
never on any other response.
| Bucket | Keyed by | Routes |
|---|---|---|
| 120 per minute | Your API key | POST /api/merchants/checkout/sessions |
| 60 per minute | Caller IP | GET /api/checkout-splits/pay/:token, GET /api/merchants/checkout/sessions/:token |
| 5 per 10 minutes | Your IP, shared across all of them | Credential rotation, webhook test, delivery resend, every team invite route — and the public contact, waitlist and partner forms |
That last bucket is the one you meet first, and it is per IP rather than per account. See Limits.