OAuth
Let users connect your app to their Servicebay organisation with scoped permissions
Alongside API keys, Servicebay supports OAuth 2.1 so a third-party app (or AI agent — see MCP) can be authorized by a Servicebay user without ever handling their credentials or a long-lived key.
An organisation admin signs in, picks which organisation to connect, and approves exactly the permissions your app asked for. Your app receives tokens scoped to that organisation and nothing else.
The OAuth server
The authorization server lives at its own origin (the OAuth base URL below —
https://oauth.servicebay.io in the examples). It publishes standard
discovery metadata, so most OAuth client libraries can configure themselves
from a single URL:
GET {oauth-base}/.well-known/oauth-authorization-server| Endpoint | Path |
|---|---|
| Authorization | /authorize |
| Token | /token |
| Dynamic client registration (RFC 7591) | /register |
Registering a client
Register your app once via the dynamic client registration endpoint:
curl -X POST '{oauth-base}/register' \
-H 'Content-Type: application/json' \
-d '{
"client_name": "My Integration",
"client_uri": "https://example.com",
"redirect_uris": ["https://example.com/oauth/callback"],
"token_endpoint_auth_method": "client_secret_basic"
}'The response contains your client_id (and a client_secret for
confidential clients — shown once, store it safely).
Public clients expire
Clients registered without a secret (token_endpoint_auth_method: "none",
e.g. SPAs and AI agents) expire after 90 days and must re-register.
Confidential clients do not expire.
Authorization flow
Servicebay implements the OAuth 2.1 authorization-code flow. PKCE (S256) is required for all clients; the implicit flow is not supported.
Redirect the user to /authorize
{oauth-base}/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://example.com/oauth/callback
&scope=tickets:read+tickets:write+customers:read
&state=RANDOM_STATE
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256The user signs in with their Servicebay account, chooses which organisation to connect, and reviews the requested permissions. Only organisation admins can authorize apps. The user may grant fewer scopes than you requested — never more.
Exchange the code for tokens
curl -X POST '{oauth-base}/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=AUTH_CODE_FROM_CALLBACK' \
-d 'redirect_uri=https://example.com/oauth/callback' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET' \
-d 'code_verifier=YOUR_PKCE_VERIFIER'The response contains an access_token (valid 1 hour), a
refresh_token (valid 30 days, rotates on every use), and the granted
scope.
Call the API with the access token
Use a Bearer header instead of X-API-Key — everything else about the
API is identical:
curl 'https://developer.servicebay.io/api/v1/organisations/{orgId}/tickets' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'The token is bound to the organisation the user selected; requests to any
other organisation fail with 403.
Refresh when the access token expires
curl -X POST '{oauth-base}/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=YOUR_REFRESH_TOKEN' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET'Each refresh returns a new refresh token; discard the old one.
Scopes
Request only what your app needs — the consent screen shows every scope to the user.
| Scope | Grants |
|---|---|
organisation:read / organisation:write | The organisation profile |
tickets:read / tickets:write | Jobs (tickets) |
customers:read / customers:write | Customers |
devices:read / devices:write | Customer devices |
services:read / services:write | Services |
inventory:read / inventory:write | Inventory |
quotes:read / quotes:write | Quotes |
invoices:read / invoices:write | Invoices |
payments:read / payments:write | Payments |
calendar:read / calendar:write | Calendar events |
stations:read / stations:write | Stations |
tax-rates:read / tax-rates:write | Tax rates |
ticket-statuses:read / ticket-statuses:write | Job status values |
read / write | Everything above (coarse) |
Two rules to remember:
- Every
:writescope implies the matching:read. GETrequests need the entity's:readscope;POST,PUT, andDELETEneed:write. A request without the required scope fails with403and an error naming the missing scope.
Revocation
Users can revoke an app's access at any time from Connected Apps in the developer portal. Revoked tokens stop working within a few minutes; a revoked refresh token stops working immediately.
Rate limits
OAuth-authenticated requests share the same limit shape as API keys: 100
requests per minute per client + organisation pair, reported through the
same X-RateLimit-* headers. See Rate Limiting.