Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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:

  1. 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's TENANT.
  2. What does it need to do? This determines allowed_scopes. Read-only services need admin:read. Services that create/update/delete need admin:write. Narrower scopes like users:read, groups:read exist 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.

bash
1
2
3
4
# Your service's config (typically env vars or a secret store)
export SCAIKEY_CLIENT_ID="<from the UI>"
export SCAIKEY_CLIENT_SECRET="<from the UI, shown once>"
export SCAIKEY_BASE="https://scaikey.scailabs.ai"

2. Acquire a token#

python
 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
import os, time, httpx

class ScaiKeyClient:
    def __init__(self):
        self.base = os.environ["SCAIKEY_BASE"]
        self.client_id = os.environ["SCAIKEY_CLIENT_ID"]
        self.client_secret = os.environ["SCAIKEY_CLIENT_SECRET"]
        self._token = None
        self._token_expiry = 0

    def _fetch_token(self):
        r = httpx.post(
            f"{self.base}/api/v1/platform/oauth/token",
            auth=(self.client_id, self.client_secret),
            data={"grant_type": "client_credentials"},
            timeout=10,
        )
        r.raise_for_status()
        body = r.json()
        self._token = body["access_token"]
        # Refresh 30 s before actual expiry to avoid races
        self._token_expiry = time.time() + body["expires_in"] - 30

    def token(self):
        if not self._token or time.time() >= self._token_expiry:
            self._fetch_token()
        return self._token

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#

python
1
2
3
4
5
6
7
8
def list_tenants(client):
    r = httpx.get(
        f"{client.base}/api/v1/admin/tenants",
        headers={"Authorization": f"Bearer {client.token()}"},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

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:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def call_with_retry(client, method, path, **kwargs):
    headers = {"Authorization": f"Bearer {client.token()}"}
    r = httpx.request(method, f"{client.base}{path}", headers=headers, **kwargs)
    if r.status_code == 401:
        # Token rejected — invalidate cache and retry once
        client._token = None
        headers["Authorization"] = f"Bearer {client.token()}"
        r = httpx.request(method, f"{client.base}{path}", headers=headers, **kwargs)
    r.raise_for_status()
    return r

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_secret pairs, so you can revoke one without affecting the others.
  • Don't put admin:write on services that only read. Least privilege — if the service ever gets compromised, the blast radius is smaller.
  • Don't ship the client_secret to a browser or mobile app. client_credentials is for confidential clients only. If your code path ever runs on a user's device, use authorization_code with 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.
Updated 2026-05-17 12:20:39 View source (.md) rev 1