GuidesUTILITY APIS

Favicon APIs compared for JavaScript

An img tag pointing at a favicon API needs no CORS, so every service below works for display. Drawing the icon to a canvas, exporting it or reading it as a blob needs Access-Control-Allow-Origin, which on 28 October 2026 icon.horse, Yandex and unavatar sent and Google's s2 endpoint (16 to 256 px PNGs) and DuckDuckGo did not. The free cors.dev proxy (no signup, about 600 requests a minute per IP) relays those icons with CORS headers to browser requests, which carry an Origin header.

Favicon APIs compared

Every service below was probed on 28 October 2026 with stripe.com, github.com, developer.mozilla.org and a domain with no website. Each one works in an img tag. The CORS columns decide whether you can also draw the icon to a canvas, hash it or store it as a blob.

Favicon APIs tested from the browser, checked October 2026
ServiceURL patternSizesUnknown domainCORS headerPixels readable
Google s2www.google.com/s2/favicons?domain=DOMAIN&sz=6416 to 256 px PNG404, 16 px globeNoneThrough cors.dev
DuckDuckGoicons.duckduckgo.com/ip3/DOMAIN.icoThe site's own file404, 48 px placeholderNoneThrough cors.dev
icon.horseicon.horse/icon/DOMAINLargest found, up to 256 px200, fallback imageAllow-Origin: *Directly
Yandexfavicon.yandex.net/favicon/v2/DOMAIN?size=3216 or 32 px PNG200, 1 by 1 px PNGAllow-Origin: *Directly
unavatarunavatar.io/DOMAINThe site's own file200, SVG logoAllow-Origin: *Directly
Site favicon.icoDOMAIN/favicon.icoWhatever the site ships404 on example.comNone on github.comThrough cors.dev

Google s2 favicons: sizes and redirects

https://www.google.com/s2/favicons?domain=DOMAIN&sz=64 (or domain_url=) answers 301 to one of t0 to t3.gstatic.com at /faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://DOMAIN&size=64, which serves the PNG. Neither response carries Access-Control-Allow-Origin, and a CORS request has to pass the check on both hops.

  • sz is a request, not a promise: stripe.com came back at 16, 32, 64, 128 and 256 px (433 bytes at 64 px, 1,555 bytes at 256 px), but sz=512 fell back to 16 px.
  • github.com came back at 32 px (519 bytes) for every sz from 32 to 256, so check the real width before scaling.
  • Without sz the icon is 16 px (330 bytes for github.com).
  • A domain without an icon gets 404 with a 16 px globe (726 bytes): an img still shows something, and fetch() can tell the icon is missing.
  • icons.bitwarden.net/DOMAIN/icon.png returned byte-identical files (519 bytes for github.com, 433 for stripe.com), also without CORS headers.

DuckDuckGo, icon.horse, Yandex and unavatar

  • DuckDuckGo (https://icons.duckduckgo.com/ip3/DOMAIN.ico) returns the site's own icon file: a 6,518-byte ICO with 16 and 32 px images for github.com, the same size as https://github.com/favicon.ico, a 15,086-byte ICO with 16, 32 and 48 px for stripe.com, and a 763-byte 48 px PNG for developer.mozilla.org. Unknown domains get 404 with a 1,478-byte placeholder. createImageBitmap() decoded the stripe.com ICO in all 3 engines and picked the 48 px image.
  • icon.horse (https://icon.horse/icon/DOMAIN) returns the largest icon it finds: 256 px (26,199 bytes) for github.com, 180 px for stripe.com, 48 px for developer.mozilla.org. Unknown domains get 200 with a fallback image, so the status can't tell you the icon is missing. Its homepage lists a free tier, icon.horse's Pro plan at $79 a month for up to 100,000 icons a month, and an Enterprise plan with self-hosting.
  • Yandex (https://favicon.yandex.net/favicon/DOMAIN) returns 16 px PNGs (268 bytes for stripe.com); the /favicon/v2/DOMAIN?size=32 form returned 32 px. Unknown domains get 200 with a 70-byte 1 by 1 PNG. No official documentation was found, so treat it as unsupported.
  • unavatar (https://unavatar.io/DOMAIN) returned the same 15,086 bytes as DuckDuckGo for stripe.com. Unknown domains get 200 with a 569-byte SVG logo, or 404 with ?fallback=false. Its docs allow 25 anonymous requests a day per IP, require attribution on the free plan and answer 429 with code ERATE and a retry-after header past the limit.

Displaying needs no CORS, reading pixels does

An img tag loads a cross-origin image without CORS, so every URL in the table displays fine. Reading the pixels back is what the browser blocks. In Chromium 153, Firefox 155 and WebKit 26.6 (Playwright 1.63, 28 October 2026), the Google s2 icon for stripe.com at sz=64 displayed at 64 by 64 and drew onto a canvas, but toDataURL() threw a SecurityError:

  • Chromium: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
  • Firefox and WebKit: The operation is insecure.

Setting img.crossOrigin = 'anonymous' doesn't help when the server sends no CORS header: the same Google URL then failed to load at all. Chromium logged Access to image at 'https://www.google.com/s2/favicons?domain=stripe.com&sz=64' from origin 'http://127.0.0.1:8001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. WebKit rejected the redirect: Cross-origin redirection to https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://stripe.com&size=64 denied by Cross-Origin Resource Sharing policy: Origin http://127.0.0.1:8001 is not allowed by Access-Control-Allow-Origin. Status code: 301. icon.horse with crossOrigin = 'anonymous' loaded at 180 px and exported in all 3. More on this error in tainted canvas.

Load favicons through cors.dev

For Google, DuckDuckGo, Bitwarden or a site's own /favicon.ico, put https://proxy.cors.dev/ in front of the URL. Every service in the table came back 200 through the proxy with Access-Control-Allow-Origin set to the calling origin and X-Cors-Source: upstream; the proxy keeps the query string and follows Google's redirect. It relays images only to requests with an Origin header, which fetch() in a page always sends; curl without one gets 502 with X-Cors-Error: unsupported_response.

Load a favicon as an ImageBitmap through cors.dev
async function loadFavicon(domain, size = 64) {
  const source = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=${size}`;
  const response = await fetch(`https://proxy.cors.dev/${source}`);

  // Google answers 404 with a generic globe when it has no icon for the site
  if (response.status === 404) return null;
  if (!response.ok) {
    const reason = response.headers.get('X-Cors-Error') ?? `HTTP ${response.status}`;
    throw new Error(`Favicon failed: ${reason}`);
  }

  return createImageBitmap(await response.blob());
}

const icon = await loadFavicon('stripe.com');
if (icon) {
  const canvas = document.querySelector('canvas');
  canvas.width = icon.width;
  canvas.height = icon.height;
  canvas.getContext('2d').drawImage(icon, 0, 0);
  console.log(canvas.toDataURL('image/png').slice(0, 40));
}

loadFavicon(domain, size) returns null when Google answers 404, throws with the X-Cors-Error value on other failures, and otherwise resolves to an ImageBitmap you can draw and export. On 28 October 2026 it returned a 64 by 64 bitmap for stripe.com, 32 by 32 for github.com and null for the unknown domain in all 3 engines, and toDataURL() on the drawn canvas worked.

The free proxy needs no signup or key, takes GET and HEAD only and allows about 600 requests a minute per IP, for images up to 6 MiB. cors.dev Pro ($5 a month for 500,000 requests, 7-day trial with 1,000 requests and no card) allows 3,000 requests a minute per account and 100 in flight, and can cache a GET for up to 300 seconds with X-Cors-Cache when the upstream answers 200 and its headers allow it; cache hits still count as requests. DuckDuckGo and Bitwarden icons qualify, with long max-age values and no Set-Cookie. The Google s2 URL does not: its 301 carries Cache-Control and Expires, and cors.dev Pro never caches after such a redirect, though the gstatic.com URL it points to can be cached. 404 no-icon answers are never cached, and neither is a site favicon sent with Set-Cookie, such as the one on github.com. Favicons rarely change, so storing each icon once on your side saves the most requests.

Good questions.

Which favicon APIs send CORS headers?

On 28 October 2026, icon.horse, Yandex (favicon.yandex.net) and unavatar.io answered with Access-Control-Allow-Origin: *. Google s2, DuckDuckGo's icons.duckduckgo.com, icons.bitwarden.net and https://github.com/favicon.ico sent no CORS header, so their pixels can't be read from a canvas without a proxy.

How do I get a high-resolution favicon?

Google s2 returned up to 256 px for stripe.com, but only because the site has a large icon: sz=512 fell back to 16 px, and github.com stayed at 32 px for every sz from 32 to 256. icon.horse returns the largest icon it finds, 256 px for github.com. DuckDuckGo and unavatar return the site's own file, which is often an ICO with 16 to 48 px images.

How can I tell that a domain has no favicon?

Google s2 and DuckDuckGo answer 404 along with their placeholder image, so response.status === 404 is a reliable signal from fetch(). unavatar answers 200 with an SVG unless you add ?fallback=false, which makes it answer 404. icon.horse and Yandex answer 200 with a fallback image, so their status gives no signal.

Do Clearbit Logo and FaviconKit still work?

No. On 28 October 2026 logo.clearbit.com no longer resolved in DNS, and api.faviconkit.com redirected every request to a 1 by 1 PNG on raw.githubusercontent.com. The public Besticon demo at besticon-demo.herokuapp.com answered 503 with "Offline for Maintenance".

Why does curl get 502 from the proxy for a favicon URL?

The free proxy relays images only to requests that send an Origin header, and curl sends none unless you add one. Browsers include Origin on every cross-origin fetch(), so the same URL works from page code. Add -H "Origin: https://example.com" to test from the command line.