diff --git a/docs/developers/README.mdx b/docs/developers/README.mdx
index 58c6043abdf..df690113f07 100644
--- a/docs/developers/README.mdx
+++ b/docs/developers/README.mdx
@@ -44,6 +44,15 @@ import Settings from '@site/docs/developers/assets/icons/gear.svg';
icon: ,
}
},
+ {
+ type: 'link',
+ label: 'Service-to-service delegation',
+ href: '/developers/service-to-service-delegation',
+ description: 'Exchange a user access token for a downstream API access token so backend services can act for the current user.',
+ customProps: {
+ icon: ,
+ }
+ },
]}
/>
```
diff --git a/docs/developers/audit-logs/README.mdx b/docs/developers/audit-logs/README.mdx
index e74a8ec3757..5a5cf121dcb 100644
--- a/docs/developers/audit-logs/README.mdx
+++ b/docs/developers/audit-logs/README.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 7
+sidebar_position: 8
---
# Audit logs
diff --git a/docs/developers/sdk-conventions/README.mdx b/docs/developers/sdk-conventions/README.mdx
index 6dbd873a311..ceb3c713784 100644
--- a/docs/developers/sdk-conventions/README.mdx
+++ b/docs/developers/sdk-conventions/README.mdx
@@ -2,7 +2,7 @@
id: sdk-conventions
title: Platform SDK conventions
sidebar_label: Platform SDK conventions
-sidebar_position: 8
+sidebar_position: 9
---
# Platform SDK conventions
diff --git a/docs/developers/service-to-service-delegation.mdx b/docs/developers/service-to-service-delegation.mdx
new file mode 100644
index 00000000000..74e2b205658
--- /dev/null
+++ b/docs/developers/service-to-service-delegation.mdx
@@ -0,0 +1,149 @@
+---
+id: service-to-service-delegation
+title: Service-to-service delegation
+sidebar_label: Service-to-service delegation
+sidebar_position: 5
+---
+
+import TokenExchangePrerequisites from './fragments/_token-exchange-prerequisites.mdx';
+
+# Service-to-service delegation
+
+In some API architectures, a backend service receives a request from a signed-in user and needs to call another backend service while preserving the user's identity.
+
+For example:
+
+```text
+User -> API A -> API B
+```
+
+API B needs to know two things:
+
+1. The caller is a trusted service, such as API A.
+2. The operation is being performed for the original user.
+
+Use Logto's token exchange grant to exchange the user's access token for a new access token whose audience is the downstream API resource. This follows the OAuth 2.0 token exchange pattern and avoids forwarding the original user token to downstream services.
+
+## When to use this flow \{#when-to-use-this-flow}
+
+Use service-to-service delegation when:
+
+- API A is a backend service that can securely authenticate to Logto's token endpoint.
+- API A receives a Logto-issued user access token.
+- API A needs to call API B on behalf of the same user.
+- API B should validate one access token with its own API resource as the audience.
+
+Do not use this flow for pure machine-to-machine access without a user. In that case, use the [client credentials flow](/quick-starts/m2m). For support, admin, or agent scenarios where one user temporarily acts as another user, use [user impersonation](/developers/user-impersonation).
+
+## How it works \{#how-it-works}
+
+```mermaid
+sequenceDiagram
+ participant User as User
+ participant ApiA as API A
+ participant Logto as Logto token endpoint
+ participant ApiB as API B
+
+ User->>ApiA: Authorization: Bearer user access token
+ Note over ApiA: Validate the user token for API A
+
+ ApiA->>Logto: POST /oidc/token with token exchange grant
+ Note over ApiA,Logto: subject_token = user's access token
resource = API B resource indicator
+
+ Logto-->>ApiA: Access token for API B
+ ApiA->>ApiB: Authorization: Bearer exchanged access token
+ Note over ApiB: Validate issuer, audience, expiration, and scopes
+```
+
+The exchanged access token represents the original user (`sub`) and is bound to the downstream API resource (`aud`). The downstream API can also inspect the `client_id` claim to identify the application that initiated the exchange.
+
+## Prerequisites \{#prerequisites}
+
+1. Create API resources for the services involved. See [Protect global API resources](/authorization/global-api-resources).
+2. Configure API B's permissions and assign them to users through roles or organization roles.
+3. Use a server-side application for API A, such as a machine-to-machine app or a traditional web app, so it can authenticate securely with an app secret.
+4. Enable token exchange for API A's application.
+
+
+
+## Request an access token for the downstream API \{#request-an-access-token-for-the-downstream-api}
+
+When API A needs to call API B, make a token exchange request to Logto's [token endpoint](/integrate-logto/application-data-structure#token-endpoint).
+
+For traditional web applications or machine-to-machine applications with an app secret, include the credentials in the `Authorization` header:
+
+```bash
+POST /oidc/token HTTP/1.1
+Host: tenant.logto.app
+Content-Type: application/x-www-form-urlencoded
+# highlight-next-line
+Authorization: Basic
+
+grant_type=urn:ietf:params:oauth:grant-type:token-exchange
+&subject_token=
+&subject_token_type=urn:ietf:params:oauth:token-type:access_token
+&resource=https://api-b.example.com
+&scope=read:orders
+```
+
+Parameters:
+
+1. `grant_type`: Use `urn:ietf:params:oauth:grant-type:token-exchange`.
+2. `subject_token`: The original Logto-issued user access token received by API A.
+3. `subject_token_type`: Use `urn:ietf:params:oauth:token-type:access_token`.
+4. `resource`: The API resource indicator of API B.
+5. `scope`: The downstream permissions API A is requesting for this delegated call. Logto issues only the requested scopes that are available to the original user for this resource according to RBAC settings.
+
+Logto returns an access token for API B:
+
+```json
+{
+ "access_token": "eyJhbGci...",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "read:orders"
+}
+```
+
+When decoded, the access token includes claims similar to:
+
+```json
+{
+ "sub": "user_id",
+ "client_id": "api_a_app_id",
+ "iss": "https://tenant.logto.app/oidc",
+ "aud": "https://api-b.example.com",
+ "scope": "read:orders",
+ "exp": 1760000000
+}
+```
+
+Then API A calls API B with the exchanged token:
+
+```bash
+GET /orders HTTP/1.1
+Host: api-b.example.com
+Authorization: Bearer
+```
+
+## Validate the token in API B \{#validate-the-token-in-api-b}
+
+API B should validate the exchanged token just like any Logto-issued API resource access token:
+
+1. Verify the signature using Logto's JWKs.
+2. Check the issuer (`iss`).
+3. Check that the audience (`aud`) matches API B's resource indicator.
+4. Check expiration (`exp`).
+5. Check the required scopes.
+6. Use `sub` as the original user ID.
+7. Optionally check `client_id` if only specific upstream services are allowed to perform delegated calls.
+
+See [Validate access tokens in API](/authorization/validate-access-tokens) for implementation guidance.
+
+## Related resources \{#related-resources}
+
+Protect global API resources
+
+Validate access tokens in API
+
+User impersonation
diff --git a/docs/developers/signing-keys.mdx b/docs/developers/signing-keys.mdx
index 8e1c671249f..490ce5c51c3 100644
--- a/docs/developers/signing-keys.mdx
+++ b/docs/developers/signing-keys.mdx
@@ -2,7 +2,7 @@
id: signing-keys
title: Signing keys
sidebar_label: Signing keys
-sidebar_position: 5
+sidebar_position: 6
---
# Signing keys
diff --git a/docs/developers/webhooks/README.mdx b/docs/developers/webhooks/README.mdx
index 0238f136093..ec13ce92732 100644
--- a/docs/developers/webhooks/README.mdx
+++ b/docs/developers/webhooks/README.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 6
+sidebar_position: 7
---
# Webhooks