Exchange a workload token
POST https://app.syntic.ai/v1/oauth/tokenTrade a short-lived OIDC token from your platform (GitHub Actions, a cloud provider, Kubernetes) for a Syntic access token that acts as a service account. No API key is sent: the incoming JWT is the credential. The body may be JSON, as below, or application/x-www-form-urlencoded.
For how this fits into a full workflow, see Workload identity.
Headers
| Header | Required | Value |
|---|---|---|
syntic-beta | Yes, while in preview | token-exchange-2026-11-01. Without it the call fails with 400 beta_required. See Versioning. |
Content-Type | Yes | application/json |
Body parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
grant_type | string | Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | string | Yes | The signed OIDC token (a JWT) your platform gave the workload. |
subject_token_type | string | Yes | urn:ietf:params:oauth:token-type:jwt |
service_account | string | Yes | The service account id, starting with svcacct_. |
requested_lifetime_seconds | integer | No | 60 to 3600, capped by the trust rule. Default 900. |
Response
200 OK, Content-Type: application/json
{
"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"
}| Field | Type | Description |
|---|---|---|
access_token | string | Starts with sat_. Send it as Authorization: Bearer ... until it expires. |
token_type | string | Always Bearer. |
expires_in | integer | Seconds until the token stops working. |
service_account | string | The service account the token acts as. |
workspace_id | string | The workspace billed for its requests. |
Examples
curl -sS https://app.syntic.ai/v1/oauth/token \
-H "syntic-beta: token-exchange-2026-11-01" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"service_account": "svcacct_01JCB3D5F7H9K1M3P5R7"
}'Errors
This endpoint follows the OAuth 2.0 error format so standard libraries understand it: {"error": "invalid_grant", "error_description": "...", "request_id": "req_..."}. The type column below is empty because the OAuth error string is the code.
| Status | type | code | When |
|---|---|---|---|
| 400 | invalid_request_error | beta_required | The syntic-beta header is missing. |
| 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 match no trust rule. The description names the first mismatch and 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. |
Using the access token
sat_ tokens cannot be refreshed or revoked one by one. Exchange a fresh JWT when one expires; to cut off access immediately, disable the service account or its trust rule. Calls made with the token are billed to the service account’s workspace and recorded in the audit log under its name.