Service-to-service integration
A complete walkthrough for the case where a backend service needs to call ScaiKey's admin API on its own behalf — no user involvement. End state: your service has a token-acquiring loop, calls a protected endpoint successfully, and handles token expiry cleanly.
If you're integrating an interactive user-facing app instead, this isn't the right tutorial — use authorization_code flow.
Decide on scope upfront#
A service-to-service integration in ScaiKey is one SERVICE (or WEB) application using the client_credentials grant. Before you register, decide:
- Which tier? If your service operates across all tenants (e.g. a usage-aggregation job), it's
GLOBAL. If it only talks to one tenant's resources, it'sTENANT. - What does it need to do? This determines
allowed_scopes. Read-only services needadmin:read. Services that create/update/delete needadmin:write. Narrower scopes likeusers:read,groups:readexist if you want to limit blast radius.
You can change allowed_scopes later, but you cannot change the application's scope (GLOBAL/PARTNER/TENANT) — that's set at registration.
1. Register the application#
In the admin UI ($SCAIKEY/admin/applications), create a new application. For a GLOBAL service:
| Field | Value |
|---|---|
| Name | MyService (use something a human can recognize in audit logs) |
| Type | SERVICE |
| Scope | GLOBAL |
| Allowed scopes | openid, admin:read (and admin:write if needed) |
| Token lifetime | 3600 (default 1 h) — bump only if you have a reason |
Save. The UI displays the client_id and a one-time client_secret. Copy both immediately into your service's configuration — the secret cannot be retrieved later. If it leaks, rotate by generating a new secret on the same application.
1 2 3 4 | |
2. Acquire a token#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
The same shape works in any language — POST with Basic auth, body grant_type=client_credentials, cache the response by expiry. Don't fetch a fresh token on every call; ScaiKey will give you the same token-shape every time and the issuance load is wasted.
For tenant-scoped apps, swap the URL to /api/v1/auth/tenants/{slug}/oauth/token.
3. Call a protected endpoint#
1 2 3 4 5 6 7 8 | |
If you get 403 Platform token requires admin:read or admin:write scope, your application's allowed_scopes doesn't include the admin scope — see Troubleshooting → Platform token 403.
4. Handle token expiry mid-request#
A token can expire while a request is in flight (in practice this is rare with a 30-second refresh window, but it happens). The clean handler:
1 2 3 4 5 6 7 8 9 10 | |
Don't retry indefinitely; one retry is enough to handle the race. Anything else is a real auth failure.
5. Logging and auditing#
Every admin API call generates an audit log entry on ScaiKey's side, recording your client_id as the actor. The action shows up under the user's audit view at $SCAIKEY/admin/audit. Your service's client_id should therefore be:
- Specific — one application per integration, not a single shared "service-account" identity reused everywhere. When something goes wrong you want to know which service called.
- Named recognizably — the audit log shows the application's
name, so name it after your service, not after a person.
What you should not do#
- Don't share a client_secret between environments. Register a separate app for staging, dev, prod — different
client_id/client_secretpairs, so you can revoke one without affecting the others. - Don't put
admin:writeon services that only read. Least privilege — if the service ever gets compromised, the blast radius is smaller. - Don't ship the
client_secretto a browser or mobile app.client_credentialsis for confidential clients only. If your code path ever runs on a user's device, useauthorization_codewith PKCE instead. - Don't issue tokens from your service to its own clients. ScaiKey is the OAuth provider; your service is a client. If your service is itself an authentication boundary for further downstream callers, you want Token Exchange — see Concepts → OAuth and OIDC.