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.
| Service | URL pattern | Sizes | Unknown domain | CORS header | Pixels readable |
|---|---|---|---|---|---|
| Google s2 | www.google.com/s2/favicons?domain=DOMAIN&sz=64 | 16 to 256 px PNG | 404, 16 px globe | None | Through cors.dev |
| DuckDuckGo | icons.duckduckgo.com/ip3/DOMAIN.ico | The site's own file | 404, 48 px placeholder | None | Through cors.dev |
| icon.horse | icon.horse/icon/DOMAIN | Largest found, up to 256 px | 200, fallback image | Allow-Origin: * | Directly |
| Yandex | favicon.yandex.net/favicon/v2/DOMAIN?size=32 | 16 or 32 px PNG | 200, 1 by 1 px PNG | Allow-Origin: * | Directly |
| unavatar | unavatar.io/DOMAIN | The site's own file | 200, SVG logo | Allow-Origin: * | Directly |
| Site favicon.ico | DOMAIN/favicon.ico | Whatever the site ships | 404 on example.com | None on github.com | Through 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.
szis a request, not a promise:stripe.comcame back at 16, 32, 64, 128 and 256 px (433 bytes at 64 px, 1,555 bytes at 256 px), butsz=512fell back to 16 px.github.comcame back at 32 px (519 bytes) for everyszfrom 32 to 256, so check the real width before scaling.- Without
szthe icon is 16 px (330 bytes forgithub.com). - A domain without an icon gets
404with a 16 px globe (726 bytes): animgstill shows something, andfetch()can tell the icon is missing. icons.bitwarden.net/DOMAIN/icon.pngreturned byte-identical files (519 bytes forgithub.com, 433 forstripe.com), also without CORS headers.
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.
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.