favi

Docs

One publishable key, five endpoints, no SDK. This page is the entire reference; the same surface is at /docs.md, /openapi.json and /llms.txt for tools — and coding agents — that read those.

Making a request

Put the company's domain in the path and your publishable key in the query string. Scheme, www., paths and casing are all stripped, so anything recognisable works and resolves to the same cached image.

GET https://img.favi.sh/{domain}?key=pk_your_key
<img src="https://img.favi.sh/stripe.com?key=pk_your_key"     width="32" height="32" alt="" />

Every response carries access-control-allow-origin: *, so the same URL works in a canvas, an OG image or a PDF renderer as well as an <img> tag. There is no hotlink rule: fetch it from a server, cache it, put it in an email.

Formats and sizes

WebP by default, at roughly half the bytes of PNG. Add &format=avif for another 20–30% off with transparency intact, &format=png for older clients, or &format=jpg if you need it — JPG has no transparency, so it flattens onto white.

Usually you should skip the size parameter: we store one image per domain — at whatever resolution that site actually publishes, up to 256 px — so a 12 px inline mention and a 32 px avatar share one cache entry when you set width and height on the <img>. For the places that need real pixels (canvas, PDFs, email), &size=64 downscales, &width/&height (or w/h) set exact dimensions, and &retina=true doubles the request for high-DPI screens. Nothing is ever upscaled: ask for 800 from a site that publishes 64 and you get 64 real pixels, not 800 invented ones.

Two more switches: &greyscale=true desaturates (the logo-wall pattern), and &fallback=404 returns a 404 on any miss instead of a generated lettermark, for callers that want to detect misses. While a first crawl is in flight the placeholder is served with status 202 and a 30-second cache — the real logo replaces it on the next fetch.

Migrating from logo.dev? The parameter names match, and token works as an alias for key — switching is a hostname swap.

Dark backgrounds

Add &theme=dark when the logo will sit on a dark background. A dark, near-monochrome mark — the kind that vanishes on a dark UI — is served recolored white; anything colorful already survives a dark background and serves unchanged, so the parameter is always safe to set.

There is no theme=auto: the server can't see your user's color scheme through a CDN. Resolve it client-side with a <picture> element:

<picture>  <source media="(prefers-color-scheme: dark)"          srcset="https://img.favi.sh/github.com?key=pk_your_key&theme=dark" />  <img src="https://img.favi.sh/github.com?key=pk_your_key" width="32" height="32" alt="" /></picture>

Ticker: logo by stock symbol

GET /ticker/AAPL?key=pk_your_key resolves a stock ticker to the company's domain and redirects to its logo, every image parameter carried along — the same shape as /name. Symbols are case-insensitive. A bare symbol is a US listing (NYSE, Nasdaq, AMEX, OTC); share classes take either spelling (BRK.B or BRK-B). Other exchanges use the Yahoo-style suffix: SHOP.TO (TSX), 7203.T (Tokyo), 0700.HK (Hong Kong), RELIANCE.NS (NSE), BHP.AX (ASX) and so on.

Listings come from Wikidata (every exchange above) and SEC filings (the complete US symbol list), so international coverage follows Wikidata's — deep for the major exchanges, thinner on US small caps. The honest number: 9,832 of 18,369 listings resolve across 32 exchanges today, and the US list is the weakest at 35%. An unknown ticker is never guessed into a domain: it serves a lettermark of the symbol, or 404 with fallback=404; x-favi-result says which. No crawl is queued and nothing is metered for a miss.

<img src="https://img.favi.sh/ticker/AAPL?key=pk_your_key" alt="Apple"><img src="https://img.favi.sh/ticker/SHOP.TO?key=pk_your_key&size=32" alt="Shopify">

Brand profiles

GET /brand/stripe.com?key=pk_your_key returns everything we know about a domain as JSON: name, description, brand colors, a blurhash, social links, and every logo URL variant. Included on every plan and counted as one ordinary request — this is the data other providers meter separately at around 5¢ a call. Fields a site never published come back null rather than invented, and an unknown domain answers 202 while its first crawl runs.

{  "domain": "stripe.com", "status": "ok",  "name": "Stripe", "brandColor": "#635bff",  "colors": [    { "hex": "#635bff", "r": 99, "g": 91, "b": 255, "share": 0.873 },    { "hex": "#0a2540", "r": 10, "g": 37, "b": 64, "share": 0.127 }  ],  "blurhash": "LdI5Twoh~ot9ohfRoffR~oofWPob",  "description": "Financial infrastructure to grow your revenue",  "socials": { "twitter": "https://twitter.com/stripe", … },  "logos": { "webp": "…", "png": "…", "jpg": "…" }}

colors is read off the logo's visible pixels, most prominent first — up to five, each with its share of the mark. White is left out unless the logo is nothing but white; brandColor is the first entry that can carry a letter legibly, which is what the fallback mark is set on. blurhash is the logo on white, 4×3 components, for a placeholder while the image loads. Both are null for domains crawled before we recorded them, until the next crawl.

Subdomains

A subdomain that has no logo of its own inherits its parent's: ask for app.stripe.com and you get stripe's real mark, not a placeholder — resolved against the public suffix list, so news.bbc.co.uk inherits from bbc.co.uk, never from co.uk. If the subdomain publishes its own icon, that wins as soon as we've crawled it.

If a subdomain resolves to the wrong mark for your brand, tell us and we'll pin it — overrides are available on every plan, not just the expensive one.

Keys and allowed domains

Keys start with pk_ and are publishable — they ship in your page source and are not secrets. Make as many as you like on any plan; the quota is per account, not per key. The control that matters is the domain allowlist: set it on each key and the key only works when requests come from your sites. Subdomains are included, and localhost is allowed for development.

A request from somewhere else still returns an image — a lettermark — rather than an error, so a misconfigured allowlist never puts a broken image in front of your users. It shows up as a rising rejected count on your dashboard instead.

What happens on a miss

If we have not seen a domain before, you get a generated lettermark straight away and we queue a crawl. It is usually replaced within seconds. Misses are cached for 30 seconds and real logos for 30 days, so the placeholder does not linger.

Some domains never resolve to a usable logo — no icon, or nothing above 8 px. Those keep serving a lettermark, which is a reasonable answer rather than a failure.

The result header

The image endpoint returns 200 with an image for every well-formed request, including ones we reject. A non-200 on an <img> is a broken image in your interface, which is worse for your users than a generic mark. Check the x-favi-result header to see what actually happened:

hit                     served a real logolettermark:pending      crawling now (202), try again shortlylettermark:failed       no usable logo exists for this domainlettermark:blocked      removed at the owner's request; see /legal/takedownlettermark:over-quota   monthly limit reachedlettermark:unknown-ticker   /ticker only: symbol not in our listingsrejected:missing        no key suppliedrejected:unknown        key does not existrejected:disabled       key was revokedrejected:referer        key is not allowed on this siterejected:rate-limited   over the per-key burst limit

With fallback=404 every lettermark: and rejected: outcome becomes a 404 with the same header. A malformed domain is the one exception and returns 400 regardless.

React, or any framework

There is no SDK, because the API is a URL: no handshake, no pagination, no response types to generate. What every codebase re-implements is the fallback, the sizing attributes that stop your layout shifting as logos load, and the preconnect. That is a component, and here is the whole of it:

// <link rel="preconnect" href="https://img.favi.sh"> in <head> saves a round trip.export function Logo({ domain, size = 32, ...rest }) {  const [failed, setFailed] = useState(false)  if (failed) return <Initials name={domain} size={size} />  return (    <img      src={`https://img.favi.sh/${domain}?key=pk_your_key&size=${size}`}      width={size} height={size} alt="" loading="lazy"      onError={() => setFailed(true)} {...rest}    />  )}

The onError branch almost never runs — an over-quota, rejected or unknown domain still returns an image — but it costs nothing and covers a network failure.

Migrating from another provider

The domain stays in the path with every provider, so the change is the host and the key parameter:

- https://img.logo.dev/stripe.com?token=YOUR_TOKEN&format=png- https://cdn.brandfetch.io/stripe.com/w/64/h/64?c=YOUR_CLIENT_ID- https://logo.clearbit.com/stripe.com+ https://img.favi.sh/stripe.com?key=pk_your_key

logo.dev's parameters are accepted under the same names. Brandfetch's path segments become query parameters (/w/64/h/64 is &width=64&height=64). Clearbit had no parameters. Each has a page with the full mapping: logo.dev, Brandfetch, Clearbit.

The index already covers the domains real applications ask for — four and a half million sites plus everything any customer has ever requested — so a cutover is the URL change and nothing else. Keep your existing onError fallbacks; they stay useful.

When a logo is out of date

We re-check every domain roughly every 60 days. If a company rebranded and you need it sooner, signed in:

POST https://favi.sh/api/refresh{ "domain": "stripe.com" }

It usually updates within seconds. Cached copies at the edge are cleared at the same time, so you should not have to wait for them to expire.

Agents and MCP

A coding agent can do everything the dashboard does — keys, allowlists, refreshes, usage — without a browser. Two doors, one credential: an ordinary favi session token, obtained by the RFC 8628 device flow and sent as a bearer. Revoke it by signing that session out: POST /api/auth/sign-out with the bearer, a JSON content type and an empty {} body.

MCP. Add the server and sign in when your client asks — it is OAuth, so the client opens a browser, you approve once, and it keeps its own token. No keys to paste. Revoke a client by signing it out.

claude mcp add --transport http favi https://favi.sh/mcp# then, inside Claude Code:  /mcp  →  favi  →  Authenticate

Tools: logo_url, search_brands, get_brand, whoami, list_keys, create_key, update_key, revoke_key, usage, refresh_domain. Each is a thin face on the HTTP route of the same name, so the audit log, rate limits and roles are the ones the dashboard has. The server speaks Streamable HTTP with JSON replies and follows the MCP authorization spec (discovery at /.well-known/oauth-authorization-server), so any compliant client works.

Plain HTTP. For scripts and clients without OAuth, the device flow gives the same access as a bearer token:

curl -s -X POST https://favi.sh/api/auth/device/code   -H 'content-type: application/json' -d '{"client_id":"my-script"}'# → { user_code, device_code, verification_uri_complete, interval }# approve at https://favi.sh/device?user_code=…, then poll:curl -s -X POST https://favi.sh/api/auth/device/token -H 'content-type: application/json'   -d '{"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code":"…","client_id":"my-script"}'# → { access_token } — then:curl -s https://favi.sh/api/keys -H "Authorization: Bearer <access_token>"curl -s -X POST https://favi.sh/api/keys -H "Authorization: Bearer <access_token>"   -H 'content-type: application/json' -d '{"label":"prod","allowedDomains":["example.com"]}'

Account endpoints: GET/POST /api/keys, PATCH/DELETE /api/keys/{id}, GET /api/usage, POST /api/refresh, GET /api/me. Key creation and changes need the admin role on the account; the rest any member.

Limits

Free is 500,000 requests a month, Pro is 2,000,000, counted per account across all your keys. Over the limit you keep getting lettermarks rather than errors, and we email the account owner at 80% and 100% so it never comes as a surprise.

Two smaller limits exist because crawling is the one genuinely scarce resource here. Each account may introduce 500 new domains a day — domains nobody has ever asked us for — counted across all its keys like the monthly quota, and each key may send 6,000 requests a minute to the origin, which the edge cache keeps almost nobody near. Both are set so that no normal integration notices. Refreshes are 20 an hour per account.

Changelog

2026-09-10 — A homepage that redirects to a different site no longer hands that site's name and logo to the requesting domain. Search returns one entry per brand rather than one per domain sharing its logo. Usage rows are camelCase numbers, lastUsedAt on keys is written, and /api/usage carries a plan.

2026-09-09 — Pre-warming (POST /api/warm and the Migrate page) removed. The index already holds the domains real applications ask for, and a miss fills in within seconds of the first request. POST /api/refresh is unchanged.