Authentication
Bearer credentials, credential types, OAuth2 flows, and scopes.
Authentication
Every request carries a Bearer credential. Pick the one that fits, keep it server-side, and scope it tightly.
curl https://api.storebay.co.uk/v1/sites \
-H "Authorization: Bearer sb_live_9c1f4b7e2a6d40f8b1c3e5a7d9f0b2c4"Credential types
| Type | Best for | How | Notes |
|---|---|---|---|
| API key | Server-to-server integrations | Authorization: Bearer sb_live_… / sb_test_… | Only a SHA-256 hash is stored; plaintext shown once at creation. Rotate/revoke are first-class and take effect immediately. |
| OAuth2 client-credentials | Machine clients that prefer short-lived tokens | POST /oauth/token → Bearer access token | For an operator's own backend integrations. |
| OAuth2 authorization-code + PKCE | Marketplace apps acting on behalf of an operator | /oauth/authorize → code → /oauth/token | 2-hour access tokens; refresh tokens rotate. PKCE (S256) is required. |
| Static token | Simple, low-risk integrations | Authorization: Bearer … | Convenience credential; scope-gated like the rest. |
Keys and tokens are environment-bound — a sb_live_… key only ever touches live data and a
sb_test_… key only ever touches test data (see Sandbox). Never store a key or
client secret in plaintext at rest.
Warning
Never embed a key in a browser, mobile app, or any client you don't control — treat it like a password.
Note
The plaintext key is shown once at creation. StoreBay stores only a SHA-256 hash.
API keys
The simplest path for a server-to-server integration. Generate a key in your StoreBay developer
settings (https://app.storebay.co.uk); it is shown once, in full, at creation. Send it as a
Bearer credential on every request:
curl https://api.storebay.co.uk/v1/sites \
-H "Authorization: Bearer sb_test_…"Rotating or revoking a key takes effect immediately — the next request with the old key gets
401 unauthorized.
OAuth2 client-credentials
For your own backend talking to your own StoreBay account, without managing a long-lived key.
- Your backend calls
POST /oauth/tokenwithgrant_type=client_credentials, yourclient_id,client_secret, and thescopeyou need. - StoreBay returns a Bearer
access_token, valid forexpires_inseconds. - Use it as
Authorization: Bearer sb_at_…on/v1calls until it expires, then request a new one the same way.
curl -X POST https://api.storebay.co.uk/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=client_credentials \
-d client_id=app_018f9c2a7b3e7c1a9f2d3a5b6c7d8e9f \
-d client_secret=… \
-d 'scope=agreements:read billing:read'const method = 'POST'
const res = await fetch('https://api.storebay.co.uk/oauth/token', {
method,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'app_018f9c2a7b3e7c1a9f2d3a5b6c7d8e9f',
client_secret: '…',
scope: 'agreements:read billing:read',
}),
})
const { access_token, expires_in } = await res.json()import requests
res = requests.post(
"https://api.storebay.co.uk/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": "app_018f9c2a7b3e7c1a9f2d3a5b6c7d8e9f",
"client_secret": "…",
"scope": "agreements:read billing:read",
},
)
access_token = res.json()["access_token"]{
"access_token": "sb_at_1f2e3d…",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "agreements:read billing:read"
}OAuth2 authorization-code + PKCE (marketplace apps)
The flow for a marketplace app acting on behalf of an operator who installs it — the operator authorizes your app once, and you act within the scopes they granted.
- Redirect the operator to
/oauth/authorizewith yourclient_id,redirect_uri, thescopeyou're requesting, and a PKCEcode_challenge(methodS256). - The operator signs in to StoreBay and approves the request; StoreBay redirects back to your
redirect_uriwith a one-time authorizationcode. - Exchange the
code— together with your PKCEcode_verifier— atPOST /oauth/tokenfor a 2-hour access token (sb_at_…) and a refresh token (sb_rt_…). - When the access token expires, call
POST /oauth/tokenwithgrant_type=refresh_tokento rotate a fresh access/refresh pair. The old refresh token is revoked in the same transaction — replaying it returnsinvalid_grant.
sb_rt_… is never a valid /v1 credential — only sb_at_… access tokens are accepted there.
Create an OAuth client for your app in your StoreBay developer settings
(https://app.storebay.co.uk).
An authenticated call
curl https://api.storebay.co.uk/v1/agreements \
-H "Authorization: Bearer sb_test_…"const res = await fetch('https://api.storebay.co.uk/v1/agreements', {
headers: { Authorization: 'Bearer sb_test_…' },
})
const { data, meta } = await res.json()import requests
res = requests.get(
"https://api.storebay.co.uk/v1/agreements",
headers={"Authorization": "Bearer sb_test_…"},
)
data = res.json()["data"]Scopes
Scopes gate resources. A credential is granted a set of scopes at issue time; a request that
touches a resource without the matching scope is rejected 403 insufficient_scope. Scopes are
resource:action (read or write); write implies the ability to create, update, and (where
supported) delete a resource — it does not imply read, so a credential wanting both requests
both explicitly. Request the narrowest set you need.
| Scope | Grants access to |
|---|---|
sites:read / sites:write | sites, widgets (read-only) |
units:read / units:write | unit-types, units, availability |
catalogue:read / catalogue:write | products, coverage-products, coupons, rate-plans, price-rules |
contacts:read / contacts:write | contacts |
reservations:read / reservations:write | reservations, orders |
agreements:read / agreements:write | agreements, subscriptions |
billing:read / billing:write | invoices, credit-notes, payments, payment-methods, mandates |
crm:read / crm:write | deals, tasks, notes |
compliance:read / compliance:write | contracts, identity-verifications |
access:read / access:write | access-credentials, access-events, hardware-devices |
integrations:read / integrations:write | integration-connections |
messages:read / messages:write | messages |
webhooks:read / webhooks:write | webhooks (endpoint management) |
events:read | events (event log, read-only) |
reports:read | reports |
imports:read / imports:write | imports |
The scope catalogue is platform-defined and stable; new resources add new scopes additively (see
the Changelog). Enforcement is exact-match: an endpoint refuses a request
without exactly the scope it needs, rather than trying to infer a looser match — so a credential
scoped billing:write alone still can't GET /v1/payments without billing:read too.
Environments and going live
sb_live_… and sb_test_… credentials share the same base URL and the same endpoints — the
environment is selected by the credential, never by a different host. Build and test everything
against sb_test_… in the sandbox, then swap the credential to go live. Nothing
else in your integration changes.