# avatarapi API reference

The whole API is the URL. Base URL `https://avatarsapi.com`. Success is image bytes; add `?json` for the metadata envelope. No auth on the anonymous tier. Image URLs are never versioned: the bare URL has to keep working forever.

```sh
curl https://avatarsapi.com/github/Fyko   # the image, 400px, cached 7d
```

<!-- Kept in step with src/Docs.tsx, which renders /docs. -->

## Lookups

| Route | Behaviour |
| --- | --- |
| `/:source/:handle` | One named Source. `/x/jack`, `/telegram/durov`. |
| `/email/:address` | Walks the email Ladder: gravatar and github race, then BIMI. |
| `/domain/:domain` | Walks the domain Ladder: BIMI, then DuckDuckGo and Google favicons. |
| `/:handle` | Infers email or domain from the Handle's shape. A bare username is rejected with `EAMBIGUOUS`: a username is ambiguous without a Source, so name one. |
| `/p/:publishableKey/…` | Runs any Lookup through one Project: its Plan, config revision, and cache identity. Key authorization is resolved before every cache read; revocation takes effect on the next request. |
| `/sources` | JSON catalogue of every Source and the Ladder each Kind uses. |
| `/health` | `{"status":"ok","sources":72}` |

### Prefixed Handles

Some Sources take a `type:value` prefix when the id is not a username: `/spotify/artist:0OdU…`, `/steam/group:valve`, `/linkedin/company:anthropic`, `/apple-store/name:Things@us`. A Handle is one path segment: percent-encode any slash (`/producthunt/products%2Ffoo`).

## Query parameters

| Param | Does | Range | Default |
| --- | --- | --- | --- |
| `?json` | The metadata envelope instead of image bytes. | flag | off |
| `?ttl=` | Edge-cache lifetime; sets `cache-control`. Accepts seconds or units: `3600`, `1h`, `28d`. Starter Projects only; other Lookups ignore it and use the default. | 1h–28d, clamped | 7d |
| `?size=` | Square output in px. Sources that accept a size use it. | 16–1000, clamped | 400 |

Unknown or repeated parameters return `EQUERY`. The retired `fallback` parameter returns `EFALLBACK` for every value except the legacy no-op `fallback=false`. A Miss is a 404 envelope; use an image error handler until managed Project Fallbacks ship.

```
https://avatarsapi.com/x/jack?size=64
```

## Responses

Hit, with `?json`:

```json
{
  "version": 1,
  "data": {
    "url": "https://github.com/Fyko.png?size=400",
    "source": "github",
    "kind": "username"
  }
}
```

Miss or fault:

```json
{
  "version": 1,
  "error": {
    "code": "ENOTFOUND",
    "message": "github: avatar URL answered 404",
    "docs": "https://avatarsapi.com/docs#errors"
  }
}
```

`message` is for humans and will change; branch on `code`. `version` covers the JSON envelope only; image URLs are never versioned.

## Errors

| Code | Status | Means |
| --- | --- | --- |
| `EAMBIGUOUS` | 400 | A bare username needs a Source named. |
| `EFALLBACK` | 400 | Anonymous runtime Fallback URLs are retired. |
| `EQUERY` | 400 | A query parameter is unknown or repeated. |
| `EKEY` | 401 | The publishable key is invalid, revoked, or not permitted to run Lookups. |
| `EFORBIDDEN` | 403 | A resolved or redirected address is private or reserved, so we refuse to follow it. |
| `ENOTFOUND` | 404 | No Source had an avatar for this Handle. A Miss is an ordinary answer, not a malfunction. |
| `ERATELIMIT` | 429 | Fresh anonymous or free-Project work exceeded its per-minute or daily limit. Cache Hits never trip it. |
| `EUPSTREAM` | 502 | A Source is broken, unreachable, or returned an unusable image. Often transient. |
| `ETIMEOUT` | 504 | The Lookup outran its budget. |
| `EINTERNAL` | 500 | Our fault. Retry; if it persists, tell us. |

`EUPSTREAM` is always 502, even when the Source answered 429 or 403: their rate limit is not ours, and forwarding it would tell you we throttled you.

## Caching

Every response carries `x-cache-status: HIT | MISS`: the Worker's own edge cache, checked before any Source is contacted. There is no redirect: the validated bytes come back from this URL, so browsers cache the image directly, and forwarded upstream `etag`s let them revalidate with a 304 instead of pulling bytes again.

| Outcome | `cache-control` |
| --- | --- |
| Validated Hit | `public, max-age=<ttl>, stale-while-revalidate=86400` |
| Ordinary Miss or route fact | `public, max-age=300` |
| Query rejection, security refusal, rate limit, or Fault | `no-store` |

## Every Hit is probed

A candidate URL is only a Hit after validation. `gravatar?d=404` and `github.com/:user.png` are well-formed for handles that don't exist; reachability is what separates a Hit from a Miss. Every candidate and redirect must:

- use HTTP or HTTPS and resolve only to public A / AAAA addresses
- complete within 8 seconds and return a 2xx `image/*` response
- fit within 2 MiB
- chain through at most three redirects, each validated before it is fetched

## Limits & Plans

| Plan | Limit | Price |
| --- | --- | --- |
| anonymous | 50 fresh Lookups / day per IP. Attribution required. | $0 |
| free project | 1,000 fresh Lookups / day per Project. Attribution required. Includes a browser-safe `pk_` key. | $0 / project / mo |
| starter | No daily or per-minute limit, custom `?ttl`, no attribution. Includes 5,000 unique avatar identities each month. | $19 / project / mo |

Cached images do not count toward usage or rate limits. Each Project is billed separately, and upgrading keeps its key and image URLs. Daily limits reset at midnight UTC; a 429 carries `retry-after` and `x-rate-limit-scope`. Free tiers also have a burst limit of 120 fresh Lookups / minute per edge location. Attribution means a visible "Avatars by avatarsapi.com" link where the images appear.

```html
<img src="https://avatarsapi.com/p/pk_…/github/Fyko" alt="">
```
