Workload identity
Workload identity federation lets your automated systems (CI pipelines, cloud functions, Kubernetes pods) call the Syntic API without a stored API key. Instead, the workload proves who it is with a short-lived token from an identity provider it already trusts, such as GitHub Actions or your cloud provider, and trades it for a Syntic access token that expires within the hour.
There is no long-lived secret to leak, rotate or forget in a repository. Access ends on its own within the hour.
When to use it
| Use workload identity | Keep an API key |
|---|---|
| CI jobs (GitHub Actions, GitLab CI) | Local development on a laptop |
| Services on AWS, Google Cloud or Azure with a built-in identity | Platforms with no OIDC identity to offer |
| Kubernetes workloads with projected service-account tokens | Quick scripts and experiments |
| Any team whose security review forbids static secrets |
How it works
- An Administrator creates a service account in a workspace and a trust rule that says which external identities may act as it: the identity provider’s issuer URL, the expected audience, and conditions on the token’s claims (for example,
repositorymust beacme/billingandrefmust berefs/heads/main). - At run time, your workload gets an OIDC token (a signed JWT) from its platform.
- It calls
POST /v1/oauth/tokenwith that JWT. - Syntic checks the signature against the issuer’s published keys, checks expiry and audience, matches the trust rules, and returns a Syntic access token valid for up to one hour.
- The workload uses that token as
Authorization: Bearer ...on any API call the service account is allowed to make.
Requests made this way are billed to the service account’s workspace and appear in usage and audit logs under the service account’s name.
Supported identity providers
| Provider | Issuer | Setup guide |
|---|---|---|
| GitHub Actions | https://token.actions.githubusercontent.com | GitHub Actions |
| GitLab CI | https://gitlab.com or your self-managed URL | GitLab CI |
| Google Cloud | https://accounts.google.com | Google Cloud |
| AWS (via IAM OIDC tokens) | Your account’s issuer URL | AWS |
| Azure / Entra workload identity | https://login.microsoftonline.com/<tenant>/v2.0 | Azure |
| Kubernetes | Your cluster’s service-account issuer | Kubernetes |
| Any OIDC provider | Must publish a discovery document and keys over HTTPS | Follow the general steps below |
Set it up
Create a service account
Console: Settings → Workspaces → (workspace) → Service accounts → New. Give it a name such as ci-billing, and choose what it may do: inference (call models), agents (use the Agents API), files (Files and Batches). Service accounts never get Admin API access.
Add a trust rule
{
"issuer": "https://token.actions.githubusercontent.com",
"audience": "https://app.syntic.ai",
"conditions": {
"repository": "acme/billing",
"ref": "refs/heads/main"
},
"max_token_lifetime_seconds": 900
}Every condition must match the incoming token’s claims exactly; a value ending in * matches any suffix. A rule needs at least one condition besides issuer and audience, so a rule cannot accidentally trust every repository on a public provider.
Exchange the token in your workload
See the examples below.
The token endpoint
POST /v1/oauth/token
No API key is needed: the incoming JWT is the credential. Send syntic-beta: token-exchange-2026-11-01 while in preview. The body may be JSON or application/x-www-form-urlencoded.
| Field | Required | Value |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | The OIDC JWT from your platform. |
subject_token_type | Yes | urn:ietf:params:oauth:token-type:jwt |
service_account | Yes | The service account’s id, such as svcacct_01JC.... |
requested_lifetime_seconds | No | 60 to 3600, capped by the trust rule. Default 900. |
Response 200 OK:
{
"access_token": "sat_eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 900,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"service_account": "svcacct_01JCB3D5F7H9K1M3P5R7",
"workspace_id": "wrkspc_01JC0A2C4E6G8J0L2N4Q"
}Access tokens start with sat_. Use them exactly like an API key until expires_in runs out, then exchange a fresh JWT. They cannot be refreshed or revoked individually; disable the service account or its trust rule to cut off access at once.
# .github/workflows/summarise.yml
permissions:
id-token: write # lets the job request an OIDC token
jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Get a Syntic token
run: |
JWT=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://app.syntic.ai" | jq -r .value)
TOKEN=$(curl -sS https://app.syntic.ai/v1/oauth/token \
-H "syntic-beta: token-exchange-2026-11-01" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token_type=urn:ietf:params:oauth:token-type:jwt \
-d service_account=svcacct_01JCB3D5F7H9K1M3P5R7 \
--data-urlencode subject_token="$JWT" | jq -r .access_token)
echo "::add-mask::$TOKEN"
echo "SYNTIC_API_KEY=$TOKEN" >> "$GITHUB_ENV"Errors
Token endpoint errors follow the OAuth 2.0 format so standard libraries understand them, plus the Syntic request_id:
{ "error": "invalid_grant", "error_description": "Token claim 'repository' does not match any trust rule.", "request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d" }| Status | error | When |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed. |
| 400 | unsupported_grant_type | grant_type is not token exchange. |
| 400 | invalid_grant | Signature, expiry, issuer, audience or claims do not match a trust rule. The description names the first mismatch but never echoes the token. |
| 401 | invalid_client | The service account does not exist or is disabled. |
| 429 | slow_down | More than 60 exchanges per minute for one service account. |
Calls made with the resulting sat_ token return the normal error envelope; an expired token gives 401 authentication, code expired_token.
Audit
Every exchange writes an audit event, service_account.token_issued, with the issuer, the matched rule and the token’s sub claim, so you can trace any API call back to the exact CI run or pod that made it.
Build notes for the platform team
- Reuse the JWT verification and JWKS caching from single sign-on; cache keys per issuer for 10 minutes and refetch on unknown
kid. sat_tokens are signed JWTs verified at the edge without a database read; embed workspace, service account, scopes and expiry.- Store trust rules per service account; evaluate every rule and accept on the first full match.