# Authentication

> Two key types: publishable keys for the browser, secret keys for the server. Both are sent via the `x-api-key` header (or the `key` query parameter), both lock to allow-listed domains, and both rotate without downtime.

## Key types

### Publishable keys

Prefix `pk_`. Safe to ship in an `<img>` tag. Bound to the domains you allow-list in the key's settings. Can only call the avatar endpoint, never billing or workspace settings.

### Secret keys

Prefix `sk_`. Server only. Sent via the `x-api-key` header (not as a Bearer token). Use for backend prefetching, batch enrichment, or any flow where you do not want the key in the browser.

**Never ship a secret key to the browser.** Secret keys can read your workspace usage. If one lands in a frontend bundle, revoke it in the console and audit your traffic. Publishable keys exist so you do not have to.

## Using a publishable key

Pass the publishable key as the `key` query parameter; the browser is the caller. Lock the key to your domains in **Console → API Keys → Allowed domains** so nobody else can hot-link your quota.

```html
<img src="https://avtrz.dev/v1/avatar?key=pk_live_7f3e21bc94aa&linkedin_url=linkedin.com/in/alex-rivera&size=128" alt="" />
```

Responds with `302 → image/webp` and `Cache-Control: public, max-age=86400`.

## Using a secret key

For server-side fetches (populating a database column, prefetching a list, wiring an agent), use a secret key via the `x-api-key` header. The endpoint still returns a `302`; either follow it or hand the `Location` URL to your client.

```js
const res = await fetch(
  "https://avtrz.dev/v1/avatar?linkedin_url=linkedin.com/in/alex-rivera&size=256",
  {
    headers: { "x-api-key": process.env.AVTRZ_SECRET! },
    redirect: "manual",
  }
);
const cdnUrl = res.headers.get("location");
```

```python
import httpx, os

res = httpx.get(
    "https://avtrz.dev/v1/avatar",
    params={"linkedin_url": "linkedin.com/in/alex-rivera", "size": 256},
    headers={"x-api-key": os.environ["AVTRZ_SECRET"]},
    follow_redirects=False,
)
cdn_url = res.headers["location"]
```

```shell
curl -i "https://avtrz.dev/v1/avatar?linkedin_url=linkedin.com/in/alex-rivera&size=256" \
  -H "x-api-key: $AVTRZ_SECRET"
```

## Rotating keys safely

Keys are independent: creating a new one does not affect the old one. Roll deploys at your own pace, then revoke the old key when you are done. There is no maintenance window.

1. **Create** a new key of the same type. The old one keeps working.
2. **Deploy** the new key to your environments. Watch the usage chart switch over.
3. **Revoke** the old key once 100% of traffic is on the new one.

## Authentication errors

Authentication errors return `401` as JSON; a quota error returns `402`. Both responses ship `Cache-Control: no-store`.

| Error | Status | Description |
| --- | --- | --- |
| `missing_key` | 401 | No `x-api-key` header or `?key=` query parameter. Common during a deploy where env vars have not propagated. |
| `invalid_key` | 401 | The key is not recognized, is disabled, or has been revoked. |
| `domain_blocked` | 401 | A publishable key was used from a domain not on its allow-list. Update **Allowed domains** in the key's settings. |
| `quota_exceeded` | 402 | You are over your monthly request or new-profile quota. Cached redirects keep serving; new lookups stop. |
