> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clinkbill.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Portal token

> Exchange a platform client token for a read-only portal user token.

The portal token endpoint returns a short-lived user token that the platform backend uses to call the read-only [Read APIs](https://docs.clinkbill.com/api-reference/agent-portal/read_apis.md). The endpoint can also return a browser authorization URL when an existing Clink customer has not yet authorized this platform.

## Request

```http theme={null}
POST {API_BASE_URL}/agent/cwallet/platform/portal/tokens
Authorization: Bearer {PLATFORM_CLIENT_TOKEN}
Content-Type: application/json
```

| Field | Required | Meaning |
| - | - | - |
| `email` | Yes | Email the platform has verified for this user. |
| `external_user_id` | No | Stable platform-side user ID for audit and callback binding. |
| `device_id` | No | Stable device identity; defaults to a hash derived from the platform client and customer. |
| `redirect_uri` | First-time authorization | Exact URI registered for the platform client. |
| `state` | First-time authorization | Opaque CSRF value returned on the OAuth callback. |
| `code_challenge` | First-time authorization | PKCE S256 challenge. |
| `code_challenge_method` | First-time authorization | Must be `S256` when `code_challenge` is sent. |

For a new customer or a customer with an active LoginSession, `email` is enough:

```json theme={null}
{
  "email": "user@example.com"
}
```

For a first-time authorization of an existing Clink customer, send the full redirect and PKCE fields:

```json theme={null}
{
  "email": "user@example.com",
  "external_user_id": "platform-user-123",
  "redirect_uri": "https://agent.example.com/oauth/clink/callback",
  "state": "{RANDOM_STATE}",
  "code_challenge": "{S256_CODE_CHALLENGE}",
  "code_challenge_method": "S256"
}
```

## Authorized

Responses use the standard `{ code, msg, data }` envelope. The examples below show the full response.

```json theme={null}
{
  "code": 200,
  "msg": "Success",
  "data": {
    "status": "authorized",
    "customer_id": "ccus_...",
    "token_type": "Bearer",
    "access_token": "<portal user token>",
    "expires_in": 3600,
    "scope": "portal:read"
  }
}
```

The token claims are:

| Claim | Value |
| - | - |
| `aud` | `clink-agent-platform-portal` |
| `scope` | `portal:read` |
| `sub` | `customer_id` |
| `client_id` | Platform client ID |
| `sid` | LoginSession ID |
| `token_use` | `access` |

Use the token as `Authorization: Bearer {PORTAL_TOKEN}` on `/agent/platform/**`.

## Authorization required

```json theme={null}
{
  "code": 200,
  "msg": "Success",
  "data": {
    "status": "authorization_required",
    "customer_id": "ccus_...",
    "authorization_url": "https://api.clinkbill.com/agent/cwallet/oauth/authorize?..."
  }
}
```

This means Clink found the existing customer but this platform client has no active LoginSession for them. Show the URL to the user, complete the standard OAuth callback with PKCE, then call this endpoint again.

The authorization URL carries an `email` prefill for the Portal login page when available. The user still completes email OTP verification; the prefill only saves retyping the address.

If the user cancels or denies authorization, do not retry in a way that bypasses the browser consent. Start a new authorization request instead.

## New and existing users

| User state | Result |
| - | - |
| Email not in Clink | Clink creates the customer and LoginSession, then returns `authorized`. No browser is opened. |
| Customer exists, this platform has an active LoginSession | Returns a new portal access token. |
| Customer exists, this platform has no active LoginSession | Returns `authorization_required` and a URL. No token is returned. |

The portal token is access-only. The platform does not receive a portal refresh token; it obtains a new portal token with the platform client token. The platform token remains the authority for obtaining a user token and must stay on the server.

## Errors

| HTTP status | Meaning |
| - | - |
| `400` | Invalid request, missing email, or invalid/expired LoginSession data. |
| `401` | Missing, expired, or invalid platform client token. |
| `403` | Platform client is disabled, not registered, or lacks the required scope. |
| `429` | Rate limit exceeded. |

Do not include the platform token or the portal token in browser URLs, logs, or Agent-visible output.

## Next

Use the portal token with [Read APIs](https://docs.clinkbill.com/api-reference/agent-portal/read_apis.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.