> The index of these docs, for agents: https://traktor.fun/llms.txt

# Authentication

How to create, send and revoke an API key, and exchange one for a browser session.

A metered route takes an API key as a bearer token, a few routes take no credential, and the account routes also take a browser session or a Traktor sign-in.

## API keys

Create keys on your profile's [API tab](https://traktor.fun/profile?tab=api), up to your plan's API keys limit ([Plans and limits](https://traktor.fun/docs/plans.md)). Every key the tab makes starts `tk_live_`. There is no test mode: a `tk_test_` key, which only Traktor issues, reads the same live data under the same limits. A key's last characters are a checksum, so a mistyped key is refused before it is looked up.

Revoke a key on the same tab, and requests with it are refused within seconds, or within 5 minutes when the revocation cannot reach the servers' shared cache. A stream open on it closes with `4003` at the first check after that, which runs every minute, and a [webhook](https://traktor.fun/docs/notifications.md#webhooks) made with it, or with a browser session it opened, is posted nothing more. The routes that create and revoke keys take only your Traktor sign-in, so a key cannot make or revoke another.

## Sending a key

Send `Authorization: Bearer $TRAKTOR_API_KEY` with every request. A request without it answers `401 missing_credentials`.

Never put a key in a query string. A query parameter named `key`, `apikey`, `api_key`, `token`, `access_token`, `auth` or `secret` answers `400 key_in_query` before the key is checked, on every metered route and on `GET /v1/plans` and `GET /v1/meta/filters`. Rotate a key you sent that way: proxies write URLs to their logs.

On the trade stream the key goes in the first frame, `auth`. A parameter with one of those names in the socket's URL closes the socket with `4015`.

## When a key is refused

- `401 invalid_key`: the key is malformed or unknown; the two answer alike.
- `401 key_revoked`: the key was revoked or has expired, or its account is closed.
- `402 subscription_inactive`: the account is suspended.

A malformed or unknown key counts against your IP's budget for refused keys, the "Invalid keys" limit on the endpoint blocks. Once that budget is spent, a key the server must look up again is refused as `invalid_key` too, even a valid one, until the budget refills. If you are sure of the key, wait a minute and retry. An IPv6 caller counts as its /64.

On the trade stream a refused key closes the socket: `4002` for an invalid or revoked key, `4003` for a suspended account. The market stream refuses one before the upgrade, with the status and code a REST read answers with.

## Routes without a credential

`GET /v1/plans`, `GET /v1/meta/filters`, `GET /v1/alert-capabilities`, `GET /v1/public/market`, `GET /v1/public/search` and `WS /v1/market/stream` take no credential. The market stream also takes a key on its handshake, which meters it: [Under a key](https://traktor.fun/docs/market-stream.md#under-a-key). Traktor will require a sign-in for market data, but does not yet. When it does, these market reads and a market socket with no key will answer `401 sign_in_required` to a request without a Traktor sign-in, and a socket opened on a sign-in will close `4003` within two minutes of that sign-in ending; by code, read the market with an API key on the paid routes. Each but `GET /v1/alert-capabilities` has a per-IP limit, on its endpoint block and in [Every limit](https://traktor.fun/docs/plans.md#every-limit); `GET /v1/plans` and `GET /v1/meta/filters` share one. Once market data needs a sign-in, Traktor can also hold each signed-in account to a budget of its own on the market reads and the market socket, listed beside the per-IP one; until it enforces them, nobody is refused for them.

## Account routes

The alert, notification and Telegram link routes, but for minting a group's link code, take any of three bearer tokens: an API key, a browser session (`trk_session_…`) or a Traktor sign-in (`trk_trading_…`).

- A browser session is opened from an API key by the exchange below. It is stored as a hash, bound to that key, and ends with it.
- A Traktor sign-in is traktor.fun's own: its web server sends the token from an httpOnly cookie, so no script can hold one. Use a key or a browser session.
- `GET` and `DELETE /v1/browser-sessions/current` take only a browser session. An API key or a sign-in there answers `401 session_required`.
- Routes that take Telegram's signed init data, or only a Traktor sign-in, serve Traktor's web app and its Telegram Mini App, and are not part of this reference, but for `POST /v1/telegram/link-codes`, which [Alerts](https://traktor.fun/docs/alerts.md#telegram) lists for the link a group's admin opens.

The exchange reads your key from `TRAKTOR_API_KEY`; export the `sessionToken` it returns as `TRAKTOR_SESSION_TOKEN` to run the two samples after it.

### `POST /v1/browser-sessions/exchange`

Exchanges an API key, sent in the body, for a browser session that dies with the key.

- URL: `https://api.traktor.fun/v1/browser-sessions/exchange`
- Credential: API key, in the body

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `apiKey` | body | string | yes |  |  | The API key to exchange. |

Limits:

- Exchanges: 10 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Session lifetime: 12 hours, per request
- Live sessions: 10 kept; the oldest is revoked, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/browser-sessions/exchange" \
  -H "Content-Type: application/json" \
  -d "{\"apiKey\":\"$TRAKTOR_API_KEY\"}"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/browser-sessions/exchange", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ apiKey: process.env.TRAKTOR_API_KEY }),
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/browser-sessions/exchange",
    json={"apiKey": os.environ["TRAKTOR_API_KEY"]},
)
print(response.json())
```

Response `201`:

```json
{
  "data": {
    "sessionToken": "trk_session_7Yw2c9QeN4kV1pR8tL3xM6bZ0aH5sJ2uF9gD4nK1qE0",
    "expiresAt": "2026-10-04T00:00:00.000Z",
    "account": {
      "id": "5f0c2d1e-8a7b-4c3d-9e2f-1a0b9c8d7e6f",
      "email": null,
      "name": null,
      "plan": "free"
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `invalid_api_key`, `invalid_key`, `key_revoked`, `subscription_inactive`, `account_inactive`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `GET /v1/browser-sessions/current`

The account a browser session belongs to, and when it expires.

- URL: `https://api.traktor.fun/v1/browser-sessions/current`
- Credential: Browser session (`trk_session_…`)

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s "https://api.traktor.fun/v1/browser-sessions/current" \
  -H "Authorization: Bearer $TRAKTOR_SESSION_TOKEN"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/browser-sessions/current", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_SESSION_TOKEN}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/browser-sessions/current",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_SESSION_TOKEN']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "account": {
      "id": "5f0c2d1e-8a7b-4c3d-9e2f-1a0b9c8d7e6f",
      "email": null,
      "name": null,
      "plan": "free"
    },
    "expiresAt": "2026-10-04T00:00:00.000Z"
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `session_required`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `DELETE /v1/browser-sessions/current`

Revokes the browser session that sends it.

- URL: `https://api.traktor.fun/v1/browser-sessions/current`
- Credential: Browser session (`trk_session_…`)

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X DELETE "https://api.traktor.fun/v1/browser-sessions/current" \
  -H "Authorization: Bearer $TRAKTOR_SESSION_TOKEN"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/browser-sessions/current", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_SESSION_TOKEN}` },
});
console.log(response.status);
```

```python Python
import os
import requests

response = requests.delete(
    "https://api.traktor.fun/v1/browser-sessions/current",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_SESSION_TOKEN']}"},
)
print(response.status_code)
```

Response `204`: no body.

Errors: `session_required`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md
