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

# POST /oauth/token

> Exchange an authorization code for a token pair, or rotate a refresh token. Authenticated with your client ID and an API key.

Issues a token pair. Two grant types are supported: `authorization_code` and `refresh_token`.

|                  |                                                                                  |
| ---------------- | -------------------------------------------------------------------------------- |
| **Auth**         | HTTP Basic, with your `client_id` as the username and an API key as the password |
| **Content type** | `application/x-www-form-urlencoded` or `application/json`                        |
| **Rate limit**   | 30 / minute, keyed by IP                                                         |

<Warning>
  This endpoint requires an API key, so it should only ever be called from your backend and never from a browser or mobile client.
</Warning>

Credentials can also be passed as `client_id` and `client_secret` body fields if Basic auth is awkward in your HTTP client, where `client_secret` is your API key.

## Exchange an Authorization Code

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.tren.ch/oauth/token' \
    -u "$CLIENT_ID:$API_KEY" \
    -d grant_type=authorization_code \
    -d code=trench_ac_… \
    -d redirect_uri=https://yourapp.com/oauth/callback \
    -d code_verifier=…
  ```

  ```javascript Node.js theme={null}
  const basic = Buffer.from(`${CLIENT_ID}:${API_KEY}`).toString("base64");

  const res = await fetch("https://api.tren.ch/oauth/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: REDIRECT_URI,
      code_verifier: verifier,
    }),
  });
  ```

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

  res = requests.post(
    "https://api.tren.ch/oauth/token",
    auth=(CLIENT_ID, API_KEY),
    data={
      "grant_type": "authorization_code",
      "code": code,
      "redirect_uri": REDIRECT_URI,
      "code_verifier": verifier,
    },
  )
  ```
</CodeGroup>

| Field           | Required | Notes                                                                 |
| --------------- | -------- | --------------------------------------------------------------------- |
| `grant_type`    | Yes      | `authorization_code`                                                  |
| `code`          | Yes      | The `trench_ac_` code from your callback                              |
| `redirect_uri`  | Yes      | Must match the URI stored with the code                               |
| `code_verifier` | Yes      | 43 to 128 chars of `[A-Za-z0-9_-]`, hashing to the original challenge |

## Refresh a Token Pair

```bash cURL theme={null}
curl -X POST 'https://api.tren.ch/oauth/token' \
  -u "$CLIENT_ID:$API_KEY" \
  -d grant_type=refresh_token \
  -d refresh_token=trench_rt_…
```

| Field           | Required | Notes                          |
| --------------- | -------- | ------------------------------ |
| `grant_type`    | Yes      | `refresh_token`                |
| `refresh_token` | Yes      | The current `trench_rt_` token |

## Response

The response is identical for both grants.

```json theme={null}
{
  "access_token": "trench_at_…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "trench_rt_…",
  "scope": "profile:read trade:execute"
}
```

| Field           | Notes                                                     |
| --------------- | --------------------------------------------------------- |
| `access_token`  | Valid for 30 minutes                                      |
| `token_type`    | Always `Bearer`                                           |
| `expires_in`    | Seconds until the access token expires, always `1800`     |
| `refresh_token` | A new token valid for 60 days, replacing the one you sent |
| `scope`         | Space-joined scopes carried by this access token          |

Responses are sent with `cache-control: no-store` and `pragma: no-cache`.

<Note>
  The pair returned here is bound to the API key you authenticated with. Refreshing on a different key rebinds it, and revoking a key invalidates every token that key issued. See [Rotating a Key](/concepts/registration#rotating-a-key).
</Note>

<Warning>
  Refresh tokens rotate on every use, so both new values need to be persisted atomically. If you store the access token but drop the refresh token, the connection cannot be recovered and the user will have to reconnect.
</Warning>

## Errors

| Status | Error                      | Cause                                                                                                                                                  |
| ------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `invalid_request`          | A required field for the grant type is missing                                                                                                         |
| `400`  | `unsupported_grant_type`   | `grant_type` is neither supported value                                                                                                                |
| `400`  | `invalid_grant`            | Code expired, already used, or PKCE verification failed. Also returned when a refresh token is unknown, expired, rotated out, or its grant was revoked |
| `401`  | `invalid_client`           | Bad or missing client credentials. The response carries `WWW-Authenticate: Basic realm="trench-oauth"`                                                 |
| `429`  | `too_many_failed_attempts` | Twenty failed authentications against this `client_id` within five minutes. Carries `retry-after`                                                      |

Errors may include an `error_description` field with more detail.

<Warning>
  Two `invalid_grant` cases are destructive rather than simply failed. Replaying an authorization code revokes every token that code issued, and replaying a rotated-out refresh token revokes the user's entire grant. Both are treated as evidence of theft.
</Warning>
