Skip to content

Exchange external IDs for a platform JWT.

POST
/auth/token-exchange
curl --request POST \
--url https://shiftagent.example.com/auth/token-exchange \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "tenant_external_id": "acme:tenant:128231", "user_external_id": "acme:user:9f27c1", "ttl_seconds": 900 }'

Trades a (tenant, user) external-ID pair for a short-lived platform JWT scoped to that user — the per-user credential for conversation operations. Requires the integration service key.

  • Both parties must already exist (this endpoint does not provision — upsert first): unknown IDs → 404 not-found.
  • Suspended tenant or deactivated user → 403 (tenant-suspended / insufficient-scope). This is the lazy enforcement leg of lifecycle reconciliation: deprovisioned identities fail here even before a sweep runs.
  • Default TTL 15 minutes, max 60 (ttl_seconds). Adapters may cache the JWT until expires_at but must never persist it.
Media type application/json

Body for tokenExchange.

object
tenant_external_id
required

The tenant’s namespaced external ID.

string
<= 255 characters
user_external_id
required

The user’s namespaced external ID (within that tenant).

string
<= 255 characters
ttl_seconds

Requested token lifetime.

integer
default: 900 >= 60 <= 3600
Examples
Example exchange

Per-request user context

{
"tenant_external_id": "acme:tenant:128231",
"user_external_id": "acme:user:9f27c1",
"ttl_seconds": 900
}

Short-lived platform JWT for the user.

Media type application/json

A short-lived platform JWT scoped to one user.

object
object
required
string
Allowed value: token
token
required

The JWT. Cache until expires_at; never persist.

string
token_type
required
string
Allowed value: Bearer
expires_at
required

RFC 3339 / ISO 8601 timestamp, UTC.

string format: date-time
tenant_id
required

Resolved internal tenant ID.

string
/^tnt_[A-Za-z0-9]+$/
user_id
required

Resolved internal user ID.

string
/^usr_[A-Za-z0-9]+$/
Examples
Example token

Platform JWT (illustrative)

{
"object": "token",
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfMDFoeng4amFuZTAwMSJ9.example",
"token_type": "Bearer",
"expires_at": "2026-07-02T10:15:00Z",
"tenant_id": "tnt_01hzx8acme001",
"user_id": "usr_01hzx8jane001"
}

Missing or invalid credentials — no bearer token, an unknown/revoked sk_int_ key, or an expired platform JWT.

Media type application/problem+json

RFC 9457 problem+json error envelope. type is a URI under https://shiftagent.example.com/problems/{slug} (deployment host substituted); see the API-level problem registry for every slug.

object
type
required

Problem type URI (registry slug).

string format: uri-reference
title
required

Short, human-readable summary of the problem type.

string
status
required

HTTP status code.

integer format: int32
detail

Human-readable explanation specific to this occurrence.

string
instance

URI reference identifying this occurrence.

string format: uri-reference
request_id

Correlation ID for support and log lookup.

string
conflicting_resource_id

On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).

string
errors

On validation-error, field-level details.

Array<object>
object
pointer
required

JSON pointer to the offending field.

string
message
required

What failed.

string
Examples
Example unauthorized

Missing or invalid bearer token

{
"type": "https://shiftagent.example.com/problems/insufficient-scope",
"title": "Unauthorized",
"status": 401,
"detail": "Provide a valid sk_int_ service key or platform JWT.",
"request_id": "req_01hzx8auth001"
}

Forbidden — tenant-suspended (writes to a suspended tenant), insufficient-scope (key/token lacks the scope or a platform JWT reaches beyond its user), or approval-signature-invalid (approval assertion failed verification).

Media type application/problem+json

RFC 9457 problem+json error envelope. type is a URI under https://shiftagent.example.com/problems/{slug} (deployment host substituted); see the API-level problem registry for every slug.

object
type
required

Problem type URI (registry slug).

string format: uri-reference
title
required

Short, human-readable summary of the problem type.

string
status
required

HTTP status code.

integer format: int32
detail

Human-readable explanation specific to this occurrence.

string
instance

URI reference identifying this occurrence.

string format: uri-reference
request_id

Correlation ID for support and log lookup.

string
conflicting_resource_id

On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).

string
errors

On validation-error, field-level details.

Array<object>
object
pointer
required

JSON pointer to the offending field.

string
message
required

What failed.

string
Examples

Suspended tenant rejects conversation writes

{
"type": "https://shiftagent.example.com/problems/tenant-suspended",
"title": "Tenant suspended",
"status": 403,
"detail": "Tenant tnt_01hzx8acme001 is suspended; conversation writes are rejected.",
"request_id": "req_01hzx8sus001"
}

Not found — the resource does not exist, was deprovisioned, or lies outside the integration key’s subtree (indistinguishable by design).

Media type application/problem+json

RFC 9457 problem+json error envelope. type is a URI under https://shiftagent.example.com/problems/{slug} (deployment host substituted); see the API-level problem registry for every slug.

object
type
required

Problem type URI (registry slug).

string format: uri-reference
title
required

Short, human-readable summary of the problem type.

string
status
required

HTTP status code.

integer format: int32
detail

Human-readable explanation specific to this occurrence.

string
instance

URI reference identifying this occurrence.

string format: uri-reference
request_id

Correlation ID for support and log lookup.

string
conflicting_resource_id

On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).

string
errors

On validation-error, field-level details.

Array<object>
object
pointer
required

JSON pointer to the offending field.

string
message
required

What failed.

string
Examples
Example not_found

Unknown resource

{
"type": "https://shiftagent.example.com/problems/not-found",
"title": "Not found",
"status": 404,
"detail": "No tenant with external_id acme:tenant:999999.",
"request_id": "req_01hzx8nf001"
}

Unprocessable — validation-error (schema/semantic validation failed; errors[] lists JSON-pointer details) or role-required (user has multiple roles and no role_id was given).

Media type application/problem+json

RFC 9457 problem+json error envelope. type is a URI under https://shiftagent.example.com/problems/{slug} (deployment host substituted); see the API-level problem registry for every slug.

object
type
required

Problem type URI (registry slug).

string format: uri-reference
title
required

Short, human-readable summary of the problem type.

string
status
required

HTTP status code.

integer format: int32
detail

Human-readable explanation specific to this occurrence.

string
instance

URI reference identifying this occurrence.

string format: uri-reference
request_id

Correlation ID for support and log lookup.

string
conflicting_resource_id

On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).

string
errors

On validation-error, field-level details.

Array<object>
object
pointer
required

JSON pointer to the offending field.

string
message
required

What failed.

string
Examples

Field-level validation failure

{
"type": "https://shiftagent.example.com/problems/validation-error",
"title": "Validation error",
"status": 422,
"detail": "One or more fields failed validation.",
"errors": [
{
"pointer": "/skill_access/skill_ids/0",
"message": "skl_01hzx8unknown does not belong to the effective repository."
}
],
"request_id": "req_01hzx8val001"
}