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

# Token Endpoint

> Exchange an authorization code for access and ID tokens

<Endpoint method="POST" path="/api/oauth/token" />

Exchange an authorization code for an access token (and optionally an ID token if `openid` scope was requested). When the `phone` scope is granted, the ID token and [userinfo](/api-reference/oauth/userinfo) response include `phone_number` and `phone_number_verified`. This must be done server-side — never expose your client secret in client-side code.

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://dhmad.tn/api/oauth/token \
    -H "Content-Type: application/json" \
    -d '{
      "grant_type": "authorization_code",
      "code": "AUTHORIZATION_CODE",
      "redirect_uri": "https://example.com/callback",
      "client_id": "YOUR_CLIENT_ID",
      "client_secret": "YOUR_CLIENT_SECRET",
      "code_verifier": "YOUR_CODE_VERIFIER"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://dhmad.tn/api/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code: 'AUTHORIZATION_CODE',
      redirect_uri: 'https://example.com/callback',
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET',
      code_verifier: 'YOUR_CODE_VERIFIER'
    })
  });

  const tokens = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://dhmad.tn/api/oauth/token',
      json={
          'grant_type': 'authorization_code',
          'code': 'AUTHORIZATION_CODE',
          'redirect_uri': 'https://example.com/callback',
          'client_id': 'YOUR_CLIENT_ID',
          'client_secret': 'YOUR_CLIENT_SECRET',
          'code_verifier': 'YOUR_CODE_VERIFIER'
      }
  )

  tokens = response.json()
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "openid profile email phone",
    "id_token": "eyJhbGciOiJIUzI1NiIs..."
  }
  ```
</ResponseExample>

## Request Body

<ParamField body="grant_type" type="string" required>
  Must be `authorization_code`
</ParamField>

<ParamField body="code" type="string" required>
  The authorization code received from the authorize endpoint callback
</ParamField>

<ParamField body="redirect_uri" type="string" required>
  Must exactly match the `redirect_uri` used in the authorization request
</ParamField>

<ParamField body="client_id" type="string" required>
  Your OAuth client ID
</ParamField>

<ParamField body="client_secret" type="string" required>
  Your OAuth client secret
</ParamField>

<ParamField body="code_verifier" type="string">
  PKCE code verifier (required if `code_challenge` was provided during authorization)
</ParamField>

## Response Fields

<ParamField response="access_token" type="string">
  Bearer token for accessing protected resources (e.g., the UserInfo endpoint). Expires in 1 hour.
</ParamField>

<ParamField response="token_type" type="string">
  Always `Bearer`
</ParamField>

<ParamField response="expires_in" type="number">
  Token lifetime in seconds (3600 = 1 hour)
</ParamField>

<ParamField response="scope" type="string">
  Space-separated list of granted scopes
</ParamField>

<ParamField response="id_token" type="string">
  OpenID Connect ID token (JWT). Only returned if `openid` scope was requested. Contains user claims including `sub`, `email`, `name`, and KYC verification status (`kyc_verified`, `kyc_status`).
</ParamField>

## ID Token Claims

When `openid` scope is included, the ID token contains:

| Claim            | Description                                                    |
| ---------------- | -------------------------------------------------------------- |
| `iss`            | Issuer (e.g., `https://dhmad.tn`)                              |
| `sub`            | User ID                                                        |
| `aud`            | Your client ID                                                 |
| `exp`            | Expiration timestamp                                           |
| `iat`            | Issued-at timestamp                                            |
| `email`          | User's email address                                           |
| `email_verified` | Whether the email is verified                                  |
| `name`           | Full name                                                      |
| `given_name`     | First name                                                     |
| `family_name`    | Last name                                                      |
| `kyc_verified`   | Whether the user has approved KYC on DHMAD                     |
| `kyc_status`     | KYC status: `"pending"`, `"approved"`, `"rejected"`, or `null` |

## Error Responses

**Unsupported grant type:**

```json theme={null}
{
  "error": "unsupported_grant_type",
  "error_description": "Only 'authorization_code' grant type is supported"
}
```

**Invalid or expired code:**

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "Invalid or expired authorization code"
}
```

**Invalid client credentials:**

```json theme={null}
{
  "error": "invalid_client",
  "error_description": "Invalid client credentials"
}
```

**Redirect URI mismatch:**

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "Invalid redirect_uri. Must exactly match the URI used during authorization."
}
```

**Invalid PKCE verifier:**

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "Invalid code_verifier"
}
```

***

<Warning>
  Authorization codes expire after 10 minutes and can only be used once. Exchange the code immediately after receiving it.
</Warning>
