# Documentation

> One endpoint, one line of HTML: give icons a domain, get its favicon back as a crisp PNG. This page covers usage, the fallback behavior, caching, rate limits, and how verified tenants get higher limits.


## Getting started

The fastest integration is an image tag:

```html
<img src="https://icon0.eu/domain/example.eu" width="32" height="32" alt="">
```

That is the whole SDK. No account, no API key, no client library. The same URL works from CSS and from code:

```css
.site-icon {
  background-image: url("https://icon0.eu/domain/example.eu");
  background-size: 16px 16px;
}
```

```js
const res = await fetch("https://icon0.eu/domain/example.eu")
const blob = await res.blob() // image/png - CORS is open to every origin
```

## Icon endpoints

| Endpoint | What |
|---|---|
| `GET /domain/{domain}` | the domain's favicon as a PNG (anonymous rate limits) |
| `GET /t/{public_id}/domain/{domain}` | the same, on your tenant path (elevated rate limits once the referring domain is verified) |

Both endpoints always answer with `Content-Type: image/png` and never with a broken image: if a site has no usable favicon, a deterministic colored letter tile is served instead (see [Fallback behavior](#fallback-behavior)).

### How icons are resolved

For each domain we look for icons in this order and pick the source that renders best at 32 pixels:

1. HTML `<link rel="icon">` and `<link rel="apple-touch-icon">` tags
2. Web app manifest icons
3. A probe of `/favicon.ico`

Supported source formats: ICO, PNG, GIF, JPEG, BMP, WebP, and SVG (rasterized server-side). The output is always a PNG.

**Sizes.** Add `?sz=` for other sizes: any value from 16 to 256, default 32 (the square form `?sz=64x64` is tolerated). Requests of 80px and larger are rendered from a high-resolution master (up to 256px, sourced from apple-touch icons, web manifests, or SVG) when the site offers one; smaller requests use the site's small-optimized favicon art, which is usually pixel-hinted for exactly those sizes. 256 is the maximum quality supported.

```html
<img src="https://icon0.eu/domain/github.com?sz=64" width="64" height="64">
```

### Fallback behavior

When no usable favicon exists, the response is still a valid PNG: a colored letter tile derived deterministically from the domain - the same domain always produces the same tile, so your UI stays stable. Fallback responses are marked with the header:

```
X-Icons-Fallback: 1
```

Check for it if you want to treat generated tiles differently from real favicons.

### Caching

Responses are built to be cached hard:

```
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
ETag: "…"            (strong)
Access-Control-Allow-Origin: *
```

Browsers keep an icon for 24 hours and may serve it stale for up to 7 days while revalidating. Conditional requests with `If-None-Match` get a `304 Not Modified`. In practice, most of your users' requests never reach our servers at all.

## Rate limits

Rate limits exist purely to protect the systems - they are not a business model, and the service is free at every tier.

| | Per minute | Per day |
|---|---|---|
| Anonymous (per IP) | 30 | 1,000 |
| Verified tenant (whole tenant) | 3,000 | 100,000 |
| Verified tenant (per-IP slice) | 300 | 10,000 |

Limiting is done with a salted in-memory hash of the caller's IP; the salt rotates daily at 00:00 UTC and nothing is persisted - see the [Privacy policy](/legal/privacy-policy). Requests over the limit answer `429 Too Many Requests`.

## Verified tenants

Higher limits require no payment - only proof that you control the domain your pages load icons from:

1. **Sign in** at [icon0.eu/console](https://icon0.eu/console) - your organization becomes your tenant, and the console shows your icon base URL (`https://icon0.eu/t/{public_id}/domain/`).
2. **Add your domain** in the console (or via the API below).
3. **Publish the TXT record**: create a DNS TXT record at `_icons-verify.<host>` whose value is the `verify_token` the console shows.
4. **Verify**: click Verify (or `POST /api/v1/domains/{id}/verify`). Fresh DNS changes can take a few minutes to propagate.

From then on, requests to your `/t/{public_id}/domain/…` base whose `Referer` or `Origin` matches a verified domain are served under the tenant limits.

```html
<img src="https://icon0.eu/t/{public_id}/domain/example.eu" width="32" height="32" alt="">
```

## Tenant API reference

The management API lives under `https://icon0.eu/api/v1` and authenticates with the OIDC access token the console itself uses (`Authorization: Bearer …`). All endpoints are JSON; errors use `{"error":{"code":"…","message":"…"}}`.

| Endpoint | What |
|---|---|
| `GET /api/v1/config` | public: auth issuer/client, host, and the current limit values |
| `GET /api/v1/me` | `{tenant_id, public_id, plan, roles, icon_base}` |
| `GET /api/v1/domains` | list your domains, each with `verify_token`, `txt_record`, `verified_at` |
| `POST /api/v1/domains` | add a domain - `{"host":"app.example.eu"}` (409 `host_taken`, 400 invalid/reserved) |
| `POST /api/v1/domains/{id}/verify` | run the DNS TXT check; returns the domain plus `txt_ok` |
| `DELETE /api/v1/domains/{id}` | remove the domain (204) |

## Privacy in one paragraph

We do not log IP addresses, user agents, or referrers. The only per-visitor state anywhere in the system is the salted, in-memory rate-limit hash described above, and its salt rotates daily - even we cannot link yesterday's traffic to today's. Icons are fetched from the target sites and cached as public image data; nothing about the people loading them is stored. Everything runs on servers in the European Union. The full picture: [Privacy policy](/legal/privacy-policy) and [Compliance](/legal/compliance).

## More

- [About us](/docs/about-us)
- [Terms of service](/legal/terms-of-service) · [Privacy policy](/legal/privacy-policy) · all legal pages are linked in the footer
- Machine-readable overview: [/llms.txt](/llms.txt); this page raw: [/docs.md](/docs.md)
