Docs

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
EndpointPath
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=S256

The 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.

ScopeGrants
organisation:read / organisation:writeThe organisation profile
tickets:read / tickets:writeJobs (tickets)
customers:read / customers:writeCustomers
devices:read / devices:writeCustomer devices
services:read / services:writeServices
inventory:read / inventory:writeInventory
quotes:read / quotes:writeQuotes
invoices:read / invoices:writeInvoices
payments:read / payments:writePayments
calendar:read / calendar:writeCalendar events
stations:read / stations:writeStations
tax-rates:read / tax-rates:writeTax rates
ticket-statuses:read / ticket-statuses:writeJob status values
read / writeEverything above (coarse)

Two rules to remember:

  • Every :write scope implies the matching :read.
  • GET requests need the entity's :read scope; POST, PUT, and DELETE need :write. A request without the required scope fails with 403 and 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.