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

# Authentication

> Authenticate with the QWED API using API keys. Learn header and SDK authentication methods, key formats, environment variables, and security best practices.

How to authenticate with the QWED API.

## API keys

All requests require an API key, except `GET /health` and `GET /health/identity`, which are unauthenticated by design — `qwed init` calls the identity endpoint before it has a key to send.

### Header authentication

```bash theme={null}
curl -H "X-API-Key: qwed_your_key" https://api.qwedai.com/v1/health
```

### SDK authentication

```python theme={null}
from qwed_sdk import QWEDClient

client = QWEDClient(api_key="qwed_your_key")
```

## Environment variables

Set your API key as an environment variable:

```bash theme={null}
export QWED_API_KEY=qwed_your_key
```

Then the SDK will auto-detect it:

```python theme={null}
client = QWEDClient()  # Uses QWED_API_KEY env var
```

## API key format

QWED API keys use a fixed prefix followed by a fixed-length body. The prefix tells you the key type. The body shape tells you the key version.

| Prefix | Type |
| - | - |
| `qwed_live_` | Production API key (v2 or legacy v1 body) |
| `qwed_test_` | Non-production test/sandbox key |
| `qwed_agent_` | Agent token |

### Production key versions

| Version | Status | Pattern | Body |
| - | - | - | - |
| v2 | Current, checksummed | `qwed_live_[A-Za-z0-9]{36}` | 30 random Base62 characters (\~178.6 bits of entropy) followed by a 6-character checksum |
| v1 | Legacy, still accepted | `qwed_live_[A-Za-z0-9_-]{43}` | 43 base64url characters (32 random bytes, padding stripped, 256 bits of entropy). No checksum. |

QWED issues new keys in the v2 format. Existing v1 keys keep working and are never force-rotated. `qwed_test_` keys use the same body versions; a test key can be a structurally valid v2 (checksummed) key, so treat the prefix—not the shape—as the production/non-production signal.

The v2 checksum is CRC32 (IEEE, as in `zlib`) over the ASCII encoding of the first 30 body characters. QWED Base62-encodes the result with the alphabet `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` and zero-pads it to a width of 6. You can validate a v2 key offline: split the body into the first 30 and last 6 characters, recompute the checksum, and accept only on an exact match. This reduces random false-positive matches to about 2^-32 without a database call.

<Note>
  The checksum authenticates the key's shape, not its authorization. A key with a valid checksum still has to pass the server-side HMAC lookup before QWED accepts it.
</Note>

```python theme={null}
import zlib

ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

def base62(n: int, width: int = 6) -> str:
    out = ""
    while n:
        n, r = divmod(n, 62)
        out = ALPHABET[r] + out
    return out.rjust(width, "0")

def is_valid_v2(key: str) -> bool:
    prefix = "qwed_live_"
    if not key.startswith(prefix):
        return False
    body = key[len(prefix):]
    if len(body) != 36 or any(c not in ALPHABET for c in body):
        return False
    random_part, checksum = body[:30], body[30:]
    return base62(zlib.crc32(random_part.encode("ascii"))) == checksum
```

### Scanning for leaked keys

If you build a secret scanner, leak detector, or pre-commit hook, use these rules:

* **Match only `qwed_live_`.** Exclude `qwed_test_` from high-confidence production-secret matching. Registered scanner patterns match only keys with the `qwed_live_` prefix, so scanners do not report test keys to QWED. The revocation sink itself applies no prefix filter.
* **Anchor with lookarounds, not `\b`.** A v1 body can end in `-`, which no word boundary borders. Use charset-complement lookarounds so a match ends exactly where the body alphabet stops.
* **Post-process v2 matches.** Recompute the checksum and discard matches that fail. For v1, enforce the exact shape only.
* **With the RE2 patterns below, use the capture group.** Send group 1 as the token and resume scanning after it; the whole match includes surrounding delimiter characters.

```text theme={null}
# v2 (current)
(?<![A-Za-z0-9])qwed_live_[A-Za-z0-9]{36}(?![A-Za-z0-9])

# v1 (legacy)
(?<![A-Za-z0-9_-])qwed_live_[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])
```

On regex engines without lookaround support (for example, RE2), use an explicit delimiter class instead. The key is wrapped in a capture group; the surrounding `(?:^|[^...])` and `(?:[^...]|$)` delimiters match characters that are not part of the key, so always read the match's **group 1** as the token, never the whole match. Submitting the whole match sends one or two extra characters around the key, which breaks checksum validation and the revocation lookup. Group 1 also lets a global scan continue from the end of the key rather than the end of the whole match, so back-to-back keys separated by a single delimiter are not skipped:

```text theme={null}
# v2 (current)
(?:^|[^A-Za-z0-9])(qwed_live_[A-Za-z0-9]{36})(?:[^A-Za-z0-9]|$)

# v1 (legacy)
(?:^|[^A-Za-z0-9_-])(qwed_live_[A-Za-z0-9_-]{43})(?:[^A-Za-z0-9_-]|$)
```

The registered pattern names are `QWED_LIVE_API_KEY` (v2) and `QWED_LIVE_API_KEY_V1` (v1). QWED receives signed leak reports for these patterns at [`POST /webhooks/secret-scanning`](/api/endpoints#post-webhooks-secret-scanning).

### Masking keys

When you display a key in logs, dashboards, documentation, or examples, show the prefix, asterisks, and only the last 4 characters. Never show a full key body.

```text theme={null}
qwed_live_********************************wDHo
```

### Machine-readable spec and test vectors

Scanner and SDK authors can consume the canonical format directly:

* [`spec/api-key-format.json`](https://github.com/QWED-AI/qwed-verification/blob/main/spec/api-key-format.json): prefixes, full and anchored patterns, checksum parameters, and excluded prefixes.
* [`spec/api-key-test-vectors.json`](https://github.com/QWED-AI/qwed-verification/blob/main/spec/api-key-test-vectors.json): synthetic, non-functional valid and invalid keys with expected classifications (`v2`, `v1`, `invalid`, or `excluded` for test-prefix keys that are shape-valid but must never alert). None of these values authenticate.
* [`docs/API_KEY_FORMAT.md`](https://github.com/QWED-AI/qwed-verification/blob/main/docs/API_KEY_FORMAT.md): the human-readable specification.

Run your validator against every vector before shipping.

## API key storage and migration (v7.2)

As of v7.2, QWED stores API keys as HMAC-SHA256 lookup digests keyed by a dedicated server-side secret (`QWED_API_KEY_LOOKUP_SECRET`). Earlier versions used PBKDF2, which added \~67ms of CPU cost to every request on the unauthenticated lookup path. Because API keys are high-entropy random tokens, the key-derivation cost bought no additional brute-force resistance, so it was removed.

<Warning>
  **Breaking change.** There is no legacy fallback: API keys issued before v7.2 no longer authenticate and return `401 Unauthorized`. Each key must be re-issued once. The old raw key is never required:

  * **Portal or API:** sign in with your email and password to get a JWT, then call `POST /auth/api-keys` to mint a new key. This path needs no API key.
  * **Rotation by key ID:** call [`POST /admin/keys/rotate`](/api/endpoints#post-admin-keys-rotate) with the key's ID, authenticated with an already-working key in the same organization. Members can rotate only their own keys. Owners and admins can also rotate other users' keys, unless the key owner holds a higher role. Revoked or inactive keys can't be rotated.

  ```bash theme={null}
  # Before (pre-v7.2 key): now rejected
  curl -H "X-API-Key: qwed_live_old_key" https://api.qwedai.com/v1/health
  # -> 401 Unauthorized

  # After: re-issue via JWT login (no API key required)
  curl -X POST https://api.qwedai.com/v1/auth/signin \
    -H "Content-Type: application/json" \
    -d '{"email": "user@example.com", "password": "..."}'

  curl -X POST https://api.qwedai.com/v1/auth/api-keys \
    -H "Authorization: Bearer eyJhbG..." \
    -H "Content-Type: application/json" \
    -d '{"name": "Production Key"}'
  ```
</Warning>

### Self-hosted configuration

Self-hosted deployments must set `QWED_API_KEY_LOOKUP_SECRET` before starting the server:

* The server fails closed at startup if the variable is missing.
* The value must differ from `QWED_JWT_SECRET_KEY`. Equal values are rejected at startup, because reusing the JWT secret would break every API-key lookup on the next JWT-secret rotation.
* Set the secret before issuing any v7.2 keys. Digests are derived from the secret active at issue time, so changing it later requires a one-time re-issue of all keys.

```bash theme={null}
# Generate a dedicated secret
python -c "import secrets; print(secrets.token_urlsafe(48))"
```

## Security best practices

1. **Never commit API keys** to version control
2. **Use environment variables** in production
3. **Rotate keys regularly** using the dashboard
4. **Set IP allowlists** for production keys
5. **Use test keys** for development

## Agent authentication

Agent tokens are separate from API keys and are used for agent-specific endpoints.

```python theme={null}
# Register agent (requires API key auth)
response = client.register_agent(name="MyBot", ...)
agent_token = response["agent_token"]  # qwed_agent_...

# Use agent token via X-Agent-Token header
client.verify_action(
    agent_id=response["agent_id"],
    action={...}
)
```

Agent tokens are passed via the `X-Agent-Token` header:

```bash theme={null}
curl -X POST https://api.qwedai.com/v1/agents/42/verify \
  -H "X-Agent-Token: qwed_agent_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "What is 2+2?"}'
```

## Scopes

API keys can have restricted scopes:

| Scope | Access |
| - | - |
| `verify:read` | Verification endpoints |
| `agent:write` | Agent management |
| `attestation:read` | Attestation queries |
| `admin:all` | Full access |

## Auth endpoints

The following endpoints manage user accounts and API keys via JWT-based authentication.

Anonymous `/auth/*` routes are rate limited per client IP (default 10 requests per minute). Exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header. See [Per-IP limits on authentication endpoints](/api/rate-limits#per-ip-limits-on-authentication-endpoints).

### POST /auth/signup

Create a new user and organization. Returns a JWT token for immediate use.

**Request:**

```json theme={null}
{
  "email": "user@example.com",
  "password": "securepassword",
  "organization_name": "Acme Corp"
}
```

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "user": {
    "id": "1",
    "email": "user@example.com",
    "org_id": "1",
    "role": "owner"
  }
}
```

### POST /auth/signin

Sign in an existing user.

**Request:**

```json theme={null}
{
  "email": "user@example.com",
  "password": "securepassword"
}
```

**Response:** Same format as `/auth/signup`.

### GET /auth/me

Get the current authenticated user's information. Requires a `Bearer` token in the `Authorization` header.

```bash theme={null}
curl -H "Authorization: Bearer eyJhbG..." https://api.qwedai.com/v1/auth/me
```

### POST /auth/api-keys

Generate a new API key for the current user's organization. Requires JWT authentication.

**Request:**

```json theme={null}
{
  "name": "Production Key"
}
```

**Response:**

```json theme={null}
{
  "id": "1",
  "name": "Production Key",
  "key": "qwed_live_abc123...",
  "created_at": "2026-03-20T12:00:00Z"
}
```

<Warning>The `key` field is only returned once at creation time. Store it securely.</Warning>

### GET /auth/api-keys

List all active API keys for the current user's organization.

### DELETE /auth/api-keys/{key_id}

Revoke an API key. Performs a soft delete.

## Audit endpoints

These endpoints require JWT authentication (Bearer token) and return data scoped to the authenticated user's organization.

### GET /audit/logs

Get audit logs for the current organization.

| Parameter | Type | Default | Description |
| - | - | - | - |
| `limit` | integer | `50` | Maximum records (max 200) |
| `status` | string | — | Filter by `verified` or `blocked` |

### GET /audit/logs/{log_id}

Get detailed information for a single audit log entry.

### GET /audit/export

Export audit logs as a CSV file (up to 1,000 records).


## Related topics

- [API overview](/api/overview.md)
- [Testing your integration](/integration/testing.md)
- [QWED A2A deployment for zero-trust agent gateways](/a2a/deployment.md)
- [QWED-Agent specification v1.1](/specs/agent.md)
- [Rate limits](/api/rate-limits.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.