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

# Authentication

> Authenticate to the Thrad Advertiser API with an org-bound Bearer key, and manage that key from the Platform dashboard.

The Thrad Advertiser API authenticates every request with an organization-bound secret key passed as a Bearer token. Each request carries the access of exactly one organization — there are no user logins, OAuth flows, or session cookies on this API.

<ParamField header="Authorization" type="string" required>
  Your secret key as a Bearer token: `Authorization: Bearer ak_…`. Keys are prefixed `ak_`. There is **one active key per organization**.
</ParamField>

All endpoints are relative to the base URL:

```
https://api.thrad.ai/v1
```

<Note>
  Store your key as an environment variable (`THRAD_ADS_API_KEY` in these docs) and never commit it to source control. The full secret is shown **only once**, at the moment you create or rotate it.
</Note>

## Making an authenticated request

Send the key in the `Authorization` header on every call. The canonical check is `GET /v1/ad_account`, which returns the organization's default ad account.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thrad.ai/v1/ad_account \
    -H "Authorization: Bearer $THRAD_ADS_API_KEY"
  ```

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

  resp = requests.get(
      "https://api.thrad.ai/v1/ad_account",
      headers={"Authorization": f"Bearer {os.environ['THRAD_ADS_API_KEY']}"},
  )
  resp.raise_for_status()
  print(resp.json())
  ```
</CodeGroup>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "id": "8f3a1c20-3d2e-4a9b-bf5e-1a2b3c4d5e6f",
    "name": "Acme Inc.",
    "url": "https://acme.example.com",
    "timezone": "UTC",
    "currency_code": "USD"
  }
  ```
</ResponseExample>

<Tip>
  The Advertiser API returns **bare** OpenAI-style shapes — a single object, or a list as `{ "object": "list", "data": [...], "count": N, "first_id": "...", "last_id": "...", "has_more": false }`. It does **not** use the `{ success, data, error, meta }` envelope. The only exception is the JWT-side key-management endpoints documented below.
</Tip>

## Authentication errors

A missing `Authorization` header, a malformed header, or an unknown / expired / revoked key all return `401` with the bare error shape. The response is intentionally uniform — the API never reveals **why** a key was rejected, so it cannot be used as an oracle to distinguish an unknown key from an expired or revoked one.

<ResponseExample>
  ```json 401 — missing credentials theme={null}
  {
    "error": {
      "message": "Authentication credentials were not provided.",
      "type": "authentication_error",
      "param": null,
      "code": "auth_required"
    }
  }
  ```

  ```json 401 — invalid key theme={null}
  {
    "error": {
      "message": "Invalid API key.",
      "type": "authentication_error",
      "param": null,
      "code": "invalid_api_key"
    }
  }
  ```
</ResponseExample>

| Status             | Code              | When                                                                                                                        |
| ------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | `auth_required`   | No `Authorization` header was sent.                                                                                         |
| `401 Unauthorized` | `invalid_api_key` | The header is malformed, or the key is unknown, expired, or revoked. Also returned once the owning organization is deleted. |

<Warning>
  A JWT (dashboard session token) can never reach `/v1/`, and an `ak_` key can never reach the internal `/api/` surface. Use the `ak_` key only against `https://api.thrad.ai/v1`.
</Warning>

## Rate limits

Throttling is per **key** (not per IP and not per organization), so the two keys that briefly coexist during a rotation each get their own budget.

| Scope                                    | Limit                                                 |
| ---------------------------------------- | ----------------------------------------------------- |
| All `/v1/` requests                      | **1000 / hour** per key                               |
| `POST /v1/campaigns` (charging endpoint) | **60 / hour** per key, on top of the 1000/hour bucket |

Exceeding a limit returns `429` with `type: "rate_limit_error"` and code `rate_limit_exceeded`.

```json 429 Too Many Requests theme={null}
{
  "error": {
    "message": "Request was throttled.",
    "type": "rate_limit_error",
    "param": null,
    "code": "rate_limit_exceeded"
  }
}
```

## Managing keys

API keys are created and managed from the **Thrad Platform dashboard** under **Settings → API keys**. Only organization **owners** and **admins** can mint, rotate, or revoke a key.

<CardGroup cols={3}>
  <Card title="Create" icon="key-round">
    Mint the organization's one active key. The secret is shown once.
  </Card>

  <Card title="Rotate" icon="refresh-cw">
    Mint a replacement. The previous key stays valid for a 30-minute grace window.
  </Card>

  <Card title="Revoke" icon="ban">
    Disable a key immediately, with no grace period.
  </Card>
</CardGroup>

The management endpoints live **outside** `/v1/`, under the internal Platform API at `https://api.thrad.ai/api/organizations/{org_public_id}/api-keys/`. They are JWT/dashboard-gated (an `ak_` key cannot call them) and they speak the platform `{ success, data, error, meta }` envelope — unlike the bare `/v1/` surface. Most advertisers never call these directly; the dashboard does it for you.

<Steps>
  <Step title="Create or rotate">
    A `POST` mints a new key. If an active key already exists, it is rotated: the prior key is kept valid for the grace window, and the new key's `secret` is returned **once** in the response. Copy it immediately — it is never shown again.
  </Step>

  <Step title="Store the secret">
    Save the `secret` as `THRAD_ADS_API_KEY`. Subsequent list calls only ever return the masked `key_preview`, never the full secret.
  </Step>

  <Step title="Revoke when done">
    A `DELETE` revokes a key immediately. There is no grace period on an explicit revoke — the key stops working that instant.
  </Step>
</Steps>

### Create / rotate a key

`POST /api/organizations/{org_public_id}/api-keys/`

<ParamField path="org_public_id" type="string" required>
  The organization's public UUID.
</ParamField>

<ParamField header="Authorization" type="string" required>
  A dashboard JWT (`Bearer <jwt>`). Owner or admin role required.
</ParamField>

<ParamField body="name" type="string">
  Optional human-readable label for the key (100 characters or fewer).
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.thrad.ai/api/organizations/$ORG_ID/api-keys/ \
    -H "Authorization: Bearer $THRAD_JWT" \
    -H "Content-Type: application/json" \
    -d '{ "name": "production" }'
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "success": true,
    "data": {
      "api_key": {
        "public_id": "2c9f0a44-7b1e-4d3a-9f02-aa11bb22cc33",
        "name": "production",
        "is_active": true,
        "key_preview": "ak_9bj…7f3c",
        "secret": "ak_9b3a2c8e1f047a5d63b9e0214c7a8f3d5Kio_pQ2zR8vL0pZ6kH1s",
        "last_used": null,
        "expires_at": null,
        "created_at": "2026-04-02T12:00:00Z"
      }
    },
    "error": null,
    "meta": {}
  }
  ```
</ResponseExample>

<Warning>
  `secret` is returned **only** on this create/rotate response. After this, list calls expose `key_preview` only. If you lose the secret, rotate to mint a new one.
</Warning>

### List keys

`GET /api/organizations/{org_public_id}/api-keys/`

Returns the organization's active key with the secret masked.

<RequestExample>
  ```bash cURL theme={null}
  curl https://api.thrad.ai/api/organizations/$ORG_ID/api-keys/ \
    -H "Authorization: Bearer $THRAD_JWT"
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "success": true,
    "data": {
      "api_keys": [
        {
          "public_id": "2c9f0a44-7b1e-4d3a-9f02-aa11bb22cc33",
          "name": "production",
          "is_active": true,
          "key_preview": "ak_9bj…7f3c",
          "last_used": "2026-04-02T13:00:00Z",
          "expires_at": null,
          "created_at": "2026-04-02T12:00:00Z"
        }
      ]
    },
    "error": null,
    "meta": {}
  }
  ```
</ResponseExample>

### Revoke a key

`DELETE /api/organizations/{org_public_id}/api-keys/{key_public_id}/`

<ParamField path="org_public_id" type="string" required>
  The organization's public UUID.
</ParamField>

<ParamField path="key_public_id" type="string" required>
  The `public_id` of the key to revoke.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X DELETE \
    https://api.thrad.ai/api/organizations/$ORG_ID/api-keys/$KEY_ID/ \
    -H "Authorization: Bearer $THRAD_JWT"
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "success": true,
    "data": {
      "public_id": "2c9f0a44-7b1e-4d3a-9f02-aa11bb22cc33",
      "revoked": true
    },
    "error": null,
    "meta": {}
  }
  ```
</ResponseExample>

<Note>
  Rotation keeps the old key alive for a **30-minute** grace window so an accidental rotation does not break a live integration outright. Revocation is **immediate** — use it the moment a key is compromised.
</Note>

## Response fields

<ResponseField name="public_id" type="string">
  The key's public UUID. Use this as `{key_public_id}` when revoking.
</ResponseField>

<ResponseField name="name" type="string">
  The label given at creation (may be empty).
</ResponseField>

<ResponseField name="is_active" type="boolean">
  Whether the key is currently active. A rotated-out key reads `false` during its grace window; a revoked key reads `false` immediately.
</ResponseField>

<ResponseField name="key_preview" type="string">
  Masked preview of the key for display — the first 6 characters, an ellipsis, then the last 4 (e.g. `ak_9bj…7f3c`). Never the full secret.
</ResponseField>

<ResponseField name="secret" type="string">
  The full `ak_…` secret. Returned **only** on the create/rotate response, never again.
</ResponseField>

<ResponseField name="last_used" type="string">
  ISO 8601 timestamp (e.g. `"2026-04-02T12:00:00Z"`) of the key's last authenticated request, or `null` if never used.
</ResponseField>

<ResponseField name="expires_at" type="string">
  ISO 8601 timestamp at which the key expires. `null` for an active key; set to the end of the grace window after a rotation, or to the moment of revocation.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp when the key was created.
</ResponseField>
