Become a merchant
A one-time setup. Do this in the SharePay dashboard (recommended) at
/merchant, or via the API.
A business is added to a SharePay account; it is not an account of its own. If you already use SharePay, sign in with that account and add the business from the profile card in the sidebar. Do not make a second account with a business email: your identity is verified once, on your account, and every business you run sits under it. If you have already done that, the owner of the business can move it to your account from Team, or support can.
SharePay sets every business up as a UK Stripe account, paying out in pounds. That is fixed — nothing you send changes it — so if your business cannot complete Stripe's UK checks, we cannot take you on yet.
Register
POST /api/merchants/register authenticates a signed-in SharePay session
(Authorization: Bearer <supabase token>), not an API key, because it is the
call that issues you one.
{
"name": "Corner Coffee",
"legal_name": "Corner Coffee Ltd",
"business_type": "limited_company",
"business_address": "12 High Street, Leeds, LS1 1AA",
"business_phone": "+44 113 496 0000",
"company_number": "12345678"
}
| Field | Required | Notes |
|---|---|---|
name | Yes | Your trading name. This is what customers see: the checkout header, participant emails, the Stripe charge description |
legal_name | Yes | The registered name of the entity |
business_type | Yes | One of sole_trader, limited_company, partnership, limited_liability_partnership, charity, other |
business_address | Yes | The registered address, as one string. You may send business_address_parts instead, and the text is re-derived from it |
business_email | Yes | A valid address. This is your business contact, not the login |
business_phone | Yes | 7 to 30 characters |
company_number | Conditional | Required for limited_company and limited_liability_partnership. Optional otherwise |
vat_number | No | If you are VAT registered |
webhook_url | No | Set it here instead of later. It goes through the same URL checks |
A missing or malformed field returns 400 with an error of
Complete the required business details and a details array of
{ field, message } naming each one.
Returns 201 with { id, name, webhook_url }. No API key and no webhook
secret are created here. Make each one under Settings, Developers when you
need it.
The API key (sp_prod_..., or sp_test_... in test mode) authenticates your server's hosted-checkout
calls. It is shown once, when you create it, and never again: we keep only a
hash of it. Copy it straight into your server's environment. If you lose it,
revoke it and create another; it cannot be recovered. A business can hold
several keys, each with a name, and revoke one without disturbing the rest.
The webhook secret signs the events we send to you, so you can verify they came from SharePay. See Webhooks.
Older keys keep working exactly as before. Keys made before 27 September 2026
look like sp_sk_ followed by hex and are live keys. Keys that start
sp_sk_live_ or sp_sk_test_ came before the sp_prod_ and sp_test_ names
and keep their mode.
One business per login unless you say otherwise. Registering a second returns
409 {"code":"merchant_exists","error":"You already have a business. Confirm if you meant to add another."};
send allow_additional: true to add one deliberately. Colleagues do not each
register: invite them into the business you just made, and they join it with a
role. See Team access and roles.
One company number, one business. Registering a company that is already on
SharePay returns 409 {"code":"company_already_registered", ...} with the
business name; ask its owner to invite you, or contact support if the first
registration was the wrong one.
Everything on this page beyond GET /api/merchants/me needs the Owner or
Admin role. A Finance or Member colleague cannot create or revoke keys, see
the webhook secret, or touch Stripe onboarding.
API keys
Create a key under Settings, Developers, or with the endpoint below. The whole key is returned once, in that response, and never again: we store only a hash of it. Copy it straight into your server's environment and keep it out of client code and version control.
POST /api/merchants/api-keys
{ "name": "Production server" }
name is optional and helps you tell keys apart later. Returns 201 with the
key in key, alongside its id, name, mode, last_four and created_at.
A business can hold several keys. List them with GET /api/merchants/api-keys,
which returns each key's id, name, mode, last_four, created_at and
last_used_at, but never the key. A lost key cannot be recovered. Revoke it
and create another:
POST /api/merchants/api-keys/:id/revoke
Revoking stops that key working immediately and leaves your other keys alone. Anyone who can manage settings can revoke a key at any time, whether or not the business has finished verifying, so a leaked key can always be stopped.
A live key needs your business verified. That means the owner has verified
their identity in the SharePay app and, for a limited company or LLP, Companies
House confirms the company is active and lists them. Until then, creating a live
key returns 403 {"code":"live_key_needs_verification", ...}. The business's
verification_status in GET /api/merchants/me reads verified once it is
done. A business that was taking payments before verification existed
(verification_grandfathered: true) can create a live key without it.
GET /api/merchants/credentials no longer returns an API key, because there is
none to return. Asking POST /api/merchants/credentials/rotate to rotate
api_key returns 400 {"code":"use_api_keys", ...}.
Webhook secret
The webhook secret is created on request as well. Create it, or replace it, from Settings, Developers, or:
POST /api/merchants/credentials/rotate
{ "target": "webhook_secret" }
Returns { target, value, mode } carrying the new secret. The first call creates it;
a later one replaces it, and the old value stops verifying immediately. With the
X-SharePay-Mode: test header it is the test endpoint's secret instead: test
and live webhooks have separate URLs and secrets.
Unlike an API key, the webhook secret stays readable, because you need it to
verify our signatures: the Developers page shows it, as does
GET /api/merchants/credentials, which returns { mode, webhook_url, webhook_secret } for the mode you ask for (live unless you send
X-SharePay-Mode: test), with null for anything not yet created. We store it
encrypted.
Connect Stripe
POST /api/merchants/connect returns { url }, a Stripe hosted onboarding
link for a Standard account. Complete it. When Stripe confirms the account,
your merchant flips to stripe_charges_enabled and you can start taking
splits.
That link is one-shot and expires in about five minutes, so never store it. If
yours goes stale, POST /api/merchants/connect/refresh mints a fresh one for
the same account. Calling it before you have started onboarding returns
400 {"code":"no_stripe_account","error":"Connect Stripe first."}.
Check your status any time with GET /api/merchants/me. Any role may read it,
and the fields that matter for onboarding are:
| Field | Notes |
|---|---|
stripe_charges_enabled | The flag to gate your checkout on. True once Stripe has enabled charges |
stripe_details_submitted | Stripe has the details, but may not have finished verifying them |
stripe_account_id, stripe_connected_at | Your Stripe account, and when it was linked |
business_role | Your own role in this business: owner, admin, finance or member |
platform_fee_bps | Your fee, in hundredths of a percent. 300 is the standard 3% |
platform_fee_discount_ends_at | When a promotional rate reverts to the standard 3%, or null |
webhook_url, allowed_domain | Your integration settings |
id, name, created_at | Your merchant |
gates | What each feature still needs in the mode you asked in (x-sharepay-mode), or null when it is ready: issue_invoice, issue_invoice_bank_transfer, take_split. Each is { code, message, fix: { label, path } } |
It also returns the business and invoicing details you registered —
legal_name, business_type, business_email, business_phone, the address
fields, vat_number and company_number — because the dashboard's Settings
page prefills its forms from them.
Poll this rather than waiting to hear from us. When the
account still looks incomplete, /me reconciles straight from Stripe on read,
so it is right even if Stripe's account.updated event lagged or never
arrived.
Stripe Connect can decline for reasons outside your control, and it decides
that inside its own hosted flow. SharePay is not notified when that happens,
so nobody will contact you about it. If you come back from Stripe and
stripe_charges_enabled is still false, contact support.
Our team is notified only when SharePay itself cannot start onboarding, that is
when POST /api/merchants/connect fails with a 502. Either way the merchant
record you registered is kept, and calling POST /api/merchants/connect again
is safe: it reuses the Stripe account you already started rather than opening a
second one.
Limits
Only the endpoints in the table below are rate limited, and only those send
back RateLimit-* headers telling you how much you have left. Everything else
answers without them, so check your remaining budget on a limited endpoint —
not on just any response.
These have no limit and no headers: GET /api/merchants/me,
POST /api/merchants/connect, PATCH /api/merchants,
GET /api/merchants/webhooks/deliveries and POST /api/checkout-splits.
| Endpoint | Limit | Keyed by | Body on 429 |
|---|---|---|---|
POST /api/merchants/checkout/sessions | 120 per minute | Your API key | Too many checkout session requests. Please slow down. |
POST /api/merchants/api-keys, POST /api/merchants/credentials/rotate, POST /api/merchants/webhooks/test, POST /api/merchants/webhooks/deliveries/:id/resend | 5 per 10 minutes | Your IP address, shared across all of them | Too many requests. Please try again later. |
GET /api/checkout-splits/pay/:token, GET /api/merchants/checkout/sessions/:token | 60 per minute | Caller IP | Too many requests. Please try again shortly. |
POST /api/checkout-splits has no rate limit.
The five-per-ten-minutes limit is the one you will hit first, usually while setting up webhooks, because test sends, resends and key rotations all come out of the same allowance.
Two things make it easier to hit than it looks. It counts by IP address, not by account, so everyone in your office shares one allowance. And it also covers our public contact, waitlist and partner forms — so a contact-form submission plus a couple of webhook tests from the same address uses it up just as fast as five key rotations.
Errors on the merchant endpoints
These apply across the session-authenticated routes under /api/merchants.
| Status | Body | Cause |
|---|---|---|
401 | Unauthorized | Missing, malformed or expired Authorization: Bearer header |
403 | {"code":"consent_required","error":"Consent required", ...} | The signed-in account has not accepted the current terms or privacy policy. Accept them in the app or on the website, then retry |
404 | No merchant for this user | You have not registered a merchant |
404 | {"code":"no_merchant","error":"Register a merchant first"} | The same, from the Stripe Connect routes |
409 | {"code":"merchant_exists","error":"You already have a business. Confirm if you meant to add another."} | Registering a second business without allow_additional: true |
409 | {"code":"company_already_registered","error":"This company is already on SharePay. ...","business_name":"..."} | The company number is held by another business. Ask its owner for a team invite |
400 | {"error":"Complete the required business details","details":[...]} | A required registration field is missing or malformed. details names each one |
403 | {"code":"business_forbidden", ...} | Your role does not allow it. Seeing credentials, connecting Stripe and PATCH /api/merchants all need Owner or Admin |
409 | {"code":"business_selection_required","error":"Select a business workspace before continuing."} | Your login is in more than one business. See Being in more than one business |
400 | Invalid webhook_url: <reason> | webhook_url failed the URL checks |
400 | {"code":"no_stripe_account","error":"Connect Stripe first."} | connect/refresh before connect |
502 | Could not start Stripe onboarding or Could not refresh Stripe onboarding link | Stripe rejected the account-link request |
500 | Something went wrong! | Unhandled server error |