> ## 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.

# Tokens, Refresh, and Revocation

> Trench Partner API credentials: what each one does, how long it lives, how rotation works, and the reuse conditions that revoke a connection outright.

Every Partner API request is authenticated with a user access token issued through the OAuth flow. This page covers what the credentials are, how long they live, and how to renew them without breaking a connection.

## Credential Prefixes

Every Trench credential carries a prefix, so you can assert on the shape of a value before storing it.

| Prefix       | Credential         |
| ------------ | ------------------ |
| `tclient_`   | Client ID (public) |
| `trench_cs_` | API key            |
| `trench_ac_` | Authorization code |
| `trench_at_` | Access token       |
| `trench_rt_` | Refresh token      |

<Note>
  The `Authorization` header is checked against the `trench_at_` prefix before any database lookup happens. Sending a refresh token where an access token belongs therefore fails as `401 invalid_token` rather than as an expiry or scope error.
</Note>

## Lifetimes

| Credential         | Lifetime                        | Notes                                           |
| ------------------ | ------------------------------- | ----------------------------------------------- |
| Authorization code | 10 minutes                      | Single use                                      |
| Access token       | 30 minutes (`expires_in: 1800`) | Sent on every `/partner/v1/*` call              |
| Refresh token      | 60 days                         | A fresh 60-day token is minted on every refresh |

Because each refresh issues a new 60-day token, an actively used connection never lapses, and your users are not asked to reconnect on a schedule.

## Storing Tokens

Store the pair against your own user record.

```text theme={null}
your_users
  id: 4821
  trench_access_token:  trench_at_…
  trench_refresh_token: trench_rt_…
  trench_expires_at:    <timestamp>
```

Trench stores only hashes of these values, so support cannot read a token back to you.

## Exchanging the Code

Exchange the code from your backend, authenticating with HTTP Basic. Your API key should never reach a browser.

<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,
    }),
  });

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

  ```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,
    },
  )

  tokens = res.json()
  ```
</CodeGroup>

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

Credentials can also be sent as `client_id` and `client_secret` form fields if Basic auth is awkward in your HTTP client. Responses are returned with `cache-control: no-store`.

The `redirect_uri` must match the one stored with the code, and the `code_verifier` must hash to the `code_challenge` you sent. Only `S256` is accepted, and the verifier must be 43 to 128 characters of `[A-Za-z0-9_-]`.

<Warning>
  Authorization codes are single use. Presenting one a second time revokes every token that code issued and returns `invalid_grant`.
</Warning>

## Refreshing

```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_…
```

Refresh tokens rotate on every use. Each refresh returns a new pair and marks the old refresh token as replaced, so both new values need to be persisted.

Three practices follow from rotation:

1. **Refresh lazily**, on a `401` or when your stored expiry has passed, rather than on a timer.
2. **Never refresh concurrently for the same user.** Two refreshes of the same token at the same instant count as reuse, so one wins and the connection is revoked. If several of your workers can refresh the same user, take a lock or funnel refreshes through one place. The same applies to exchanging an authorization code, which should happen once, from one process.
3. **Never retry a `401` in a loop.** Refresh once, and if that fails, send the user back through authorization.

<Warning>
  Reusing a rotated-out refresh token is treated as a sign of compromise and revokes the user's entire grant, including every access and refresh token issued under it rather than only the one you replayed. The user will need to reconnect from scratch.
</Warning>

<Note>
  Only the refresh token rotates. The previous access token is not revoked and remains valid until its own 30-minute expiry, so for a short window both tokens work. This is convenient for requests already in flight during a refresh, but it does mean refreshing is not a way to cut off a leaked access token. To do that, revoke it explicitly.
</Note>

## Tokens and Your API Key

Every token records the API key that minted it. A refresh rebinds the new pair to whichever key you present, so an app that has rotated ends up with tokens tied to the key it still holds rather than the original.

<Warning>
  Revoking an API key revokes every live token it minted, both access and refresh, and those users have to reconnect from scratch. This is deliberate, since it is what makes revocation a real kill switch for a leaked key rather than a 60-day wait. See [Rotating a Key](/concepts/registration#rotating-a-key) for the sequence that avoids disconnecting anyone.
</Warning>

## Revoking

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

What gets revoked depends on which token you send, and the difference is significant.

| Token sent    | Effect                                                            |
| ------------- | ----------------------------------------------------------------- |
| Refresh token | Revokes the whole grant, ending the connection                    |
| Access token  | Revokes only that access token, leaving the refresh token working |

To disconnect a user properly, send the refresh token. Sending the access token only forces an early refresh. Revocation returns `200` with an empty body in either case, including for tokens that were already dead.

Users can also disconnect your app from their Trench settings, Trench can suspend an app, and revoking an API key ends every connection made through it. All three take effect on the next request and surface as `401 invalid_token`.

<Note>
  Every authentication failure returns the same `401 invalid_token` with no distinguishing detail, so expired, revoked, malformed, unknown, and suspended-client cases all look identical from the outside. Since you cannot branch on the reason, we recommend refreshing once and treating a second failure as a lost connection.
</Note>

## Checking the Connection

```bash cURL theme={null}
curl 'https://api.tren.ch/partner/v1/me' \
  -H "Authorization: Bearer trench_at_…"
```

```json theme={null}
{
  "user": {
    "walletSolana": "…",
    "twitterHandle": "…",
    "tradingEnabled": true
  },
  "client": "tclient_…",
  "scopes": ["profile:read", "trade:execute", "governance:vote"]
}
```

Both `walletSolana` and `twitterHandle` may be `null`. The `scopes` array reflects what the token can actually do, which may differ from what you requested at authorize time, so we recommend treating it as the source of truth.

<Note>
  A value of `tradingEnabled: false` means the user has not delegated their wallet for 1-click trading. Trades and governance writes will fail with `delegation_missing` until they do, so send them back through the authorize flow.
</Note>
