Bluesky is the friendliest major platform for this job: its public API returns profile data, avatar included, without a login.
The public API
curl "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=bsky.app"
The JSON response has an avatar field with a URL on cdn.bsky.app. No token is needed for public profiles (example). actor takes a handle (jay.bsky.team, or a custom domain handle) or a DID. To fetch several at once, app.bsky.actor.getProfiles takes a list (docs).
Two things to handle yourself:
- It is JSON, not an image. You make a request, read
avatar, then render that URL. In a browser that means a fetch before the image can start loading. - Avatars change. The CDN URL includes the image's content id, so a new picture means a new URL. Cache the lookup, not the URL forever.
If you are already building on AT Protocol, use the API directly.
One image URL
<img src="https://avatarsapi.com/bluesky/bsky.app" alt="">
avatarapi does the lookup on a cache miss and answers with the image itself, so it goes straight into src with no fetch step. It is cached at the edge for 7 days by default, and the URL stays the same when the picture changes.
?size=16..1000sets the square size.- A handle that does not resolve returns a 404 with
"code": "ENOTFOUND", soonerrorcan show your placeholder.
When the image URL wins
Use it when your app shows people from several networks. The same pattern covers Mastodon (/mastodon/user@instance), Twitch, X, and GitHub, so one <img> component handles all of them.
https://avatarsapi.com/github/torvalds in an <img src>. No key, no signup. Read the docs or create a Project.