Console & adminWorkload identity

Workload identity

⚠️
Preview — this feature is being built; the design below is the spec.

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 identityKeep 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 identityPlatforms with no OIDC identity to offer
Kubernetes workloads with projected service-account tokensQuick scripts and experiments
Any team whose security review forbids static secrets

How it works

  1. 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, repository must be acme/billing and ref must be refs/heads/main).
  2. At run time, your workload gets an OIDC token (a signed JWT) from its platform.
  3. It calls POST /v1/oauth/token with that JWT.
  4. 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.
  5. 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

ProviderIssuerSetup guide
GitHub Actionshttps://token.actions.githubusercontent.comGitHub Actions
GitLab CIhttps://gitlab.com or your self-managed URLGitLab CI
Google Cloudhttps://accounts.google.comGoogle Cloud
AWS (via IAM OIDC tokens)Your account’s issuer URLAWS
Azure / Entra workload identityhttps://login.microsoftonline.com/<tenant>/v2.0Azure
KubernetesYour cluster’s service-account issuerKubernetes
Any OIDC providerMust publish a discovery document and keys over HTTPSFollow 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.

FieldRequiredValue
grant_typeYesurn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYesThe OIDC JWT from your platform.
subject_token_typeYesurn:ietf:params:oauth:token-type:jwt
service_accountYesThe service account’s id, such as svcacct_01JC....
requested_lifetime_secondsNo60 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" }
StatuserrorWhen
400invalid_requestA required field is missing or malformed.
400unsupported_grant_typegrant_type is not token exchange.
400invalid_grantSignature, expiry, issuer, audience or claims do not match a trust rule. The description names the first mismatch but never echoes the token.
401invalid_clientThe service account does not exist or is disabled.
429slow_downMore 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.
Was this page helpful?