favi

Docs

One endpoint. This page is the entire reference.

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://favi.sh/{domain}?key=pk_your_key
<img src="https://favi.sh/stripe.com?key=pk_your_key"
     width="32" height="32" alt="" />

Formats and sizes

WebP by default, at roughly half the bytes of PNG. Add &format=png if you need to support something older.

There is no size parameter. We store one image per domain — at whatever resolution that site actually publishes, up to 256 px — and serve it to everyone, so a 12 px inline mention and a 32 px avatar share one cache entry rather than fragmenting into three. Set width and height on the <img> to control display size.

Most sites publish somewhere between 32 and 192 px, so the mark you get is as sharp as the one the site gives its own browser tab — no sharper, and we don't pretend otherwise by upscaling it.

Keys and allowed domains

Keys start with pk_ and are publishable — they ship in your page source and are not secrets. 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.

Errors

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 logo
lettermark:pending     crawling now, try again shortly
lettermark:failed      no usable logo exists for this domain
rejected:referer       key is not allowed on this site
rejected:missing       no key supplied
lettermark:over-quota  monthly limit reached

A malformed domain is the one exception and returns 400.

React

Optional, and it only wraps the things everyone re-implements: the fallback, the sizing attributes that stop your layout shifting as logos load, and the preconnect.

import { FaviProvider, Logo } from '@favi/react'

<FaviProvider apiKey="pk_your_key">
  <Logo domain="stripe.com" size={32} fallback={<Initials />} />
</FaviProvider>

Migrating from logo.dev

Same URL shape. Swap the host and rename the parameter:

- https://img.logo.dev/stripe.com?token=YOUR_TOKEN&format=png
+ https://favi.sh/stripe.com?key=pk_your_key

Then open Migrate, paste every domain you already show logos for, and wait for the progress bar. That crawls them before you cut over — otherwise every domain is a cache miss on the first real user who hits it, and your app fills with placeholders for an hour.

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, ask for it on the Migrate page, or:

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.

Limits

Free is 10,000 requests a month, Pro is 500,000. Over the limit you keep getting lettermarks rather than errors.

There is also a per-key cap on how many new domains you can introduce per day. It is set high enough that no normal integration notices, and exists because crawling is the one genuinely scarce resource here. Bulk pre-warming goes through Migrate instead.