# Authentication

Every request to `POST /api/v1/tasks` must be authenticated with a project API
key. BugBoard supports two schemes — **bearer** (publishable keys) and **HMAC
signatures** (secret keys) — chosen by your key type. For help deciding, see
**[Choosing a key type](/docs/installation/choosing-a-key)**.

> Authentication proves _who_ is reporting. To also keep the report **body**
> unreadable in transit — in the browser network tab, at proxies, and in logs —
> see **[Encrypted Transport](/docs/security/encryption)**, which uses a
> separate project encryption key.

## Publishable keys (bearer)

Send the key as a bearer token:

```yaml
Authorization: Bearer bb_pub_<48 hex chars>
```

Publishable keys are write-only and scoped to a single project. They're meant to
be embedded in client code, so secrecy is not what protects them — the origin
allow-list and your monthly quota are.

## Secret keys (HMAC signature)

The secret is **never sent**. Instead, each request carries three headers
computed from the signing secret:

```yaml
X-Bb-Key-Id: bbk_<id>
X-Bb-Timestamp: <unix seconds>
X-Bb-Signature: <hex HMAC-SHA256>
```

### Signing algorithm

```text
sign(method, path, body, signingSecret, keyId):
    timestamp = current unix time in SECONDS, as a string
    payload   = timestamp + "." + UPPERCASE(method) + "." + path + "." + sha256_hex(body)
    signature = hmac_sha256_hex(key = signingSecret, message = payload)
    return headers:
        X-Bb-Key-Id    = keyId
        X-Bb-Timestamp = timestamp
        X-Bb-Signature = signature
```

- **`path`** is the request path **with its leading slash and no query string** —
  for ingestion always `/api/v1/tasks`.
- **`body`** is the **exact raw bytes** you transmit. Re-serialising the JSON
  (different key order or whitespace) **breaks the signature** — sign the same
  bytes you send.
- **`timestamp`** is **integer Unix seconds sent as digits only** — a millisecond
  value (JavaScript's `Date.now()`) is rejected, so divide by 1000 first.
- Both `sha256_hex(body)` and the final signature are **lowercase hex**. The server
  compares them exactly, so uppercase hex or base64 will not match.
- The server rejects any timestamp more than **±300 seconds** from its clock
  (replay protection). A bad signature is a hard `401` with no `Retry-After`.

The server SDKs implement this for you; the full reference is in the
**[API Reference](/docs/api-reference)**.

## Origin allow-list (publishable keys only)

A publishable key may carry a list of allowed origins. When the list is
non-empty, the server only accepts requests whose `Origin` (falling back to
`Referer`) matches an entry; an **empty list accepts any origin**. Entries may be
a full origin (`https://app.example.com`) or a bare host (`app.example.com`), and
a mobile identifier in the `X-Bb-App-Id` header is matched too.

> **This is a junk filter, not a security wall.** A real browser can't forge
> `Origin`, so it blocks _other sites_ from using your key — but a script can
> send any `Origin`. The real cap on a leaked key is the
> **[quota](/docs/rate-limits)**.

## Getting and rotating keys

1. Open your project → **Settings → API Keys** (requires the `manageApiKey` ability).
2. **Create key**, choose **Publishable** or **Secret**, name it, and for
   publishable keys optionally add allowed origins.
3. Copy the shown value(s) immediately — BugBoard stores only a hash.
4. **Revoke** any key at will (password-confirmed). Revocation is immediate and
   per-key, so you can rotate a leaked key without breaking other integrations.

## Errors

Authentication failures return JSON:

| Status | Meaning                                                  |
| ------ | -------------------------------------------------------- |
| `401`  | Missing/invalid key, bad signature, or expired timestamp |
| `403`  | Publishable key used from a disallowed origin            |

See **[Error Codes](/docs/error-codes)** for the full list.
