Link previews from the browser. No backend, no CORS errors.
Call one GET from your frontend with a publishable pk_ key and get title, description, image, favicon and oEmbed as JSON. YouTube works too: we fetch server-side with oEmbed, so you don't hit the bot walls that block scrapers on cloud servers. Free 500 requests/month, commercial use OK.
{ "title": "GitHub · Build and ship software",
"images": ["https://github.githubassets.com/…/og.png"],
"favicon": "https://github.com/favicon.ico" }
pk_ keys only work from the origins you allow. curl, Python and Next.js are in the Quickstart.
Why this exists: browsers block fetching other sites (CORS) · Editor.js LinkTool needs a backend endpoint · YouTube returns empty tags to cloud servers
No credit card · CORS enabled · GET /v1/metadata?url=…
Try it on any URL
Paste any URL and see the preview card plus the raw JSON.
Get a free API key
500 requests/month. Commercial use OK. No credit card. One click, key shown right away.
URL in. Preview out.
One request: we fetch the page server-side, parse it, and hand back JSON you can render as a card.
Quickstart
Authenticate with Authorization: Bearer $KEY or X-API-Key: $KEY.
curl "https://unfurlr.onrender.com/v1/metadata?url=https://github.com" -H "Authorization: Bearer $KEY"
# or -H "X-API-Key: $KEY"
curl -X POST https://unfurlr.onrender.com/v1/metadata/batch -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"urls":["https://a.com","https://b.com"]}'const r = await fetch("https://unfurlr.onrender.com/v1/metadata?url=" + encodeURIComponent("https://github.com"), {
headers: { Authorization: `Bearer ${KEY}` }
});
const { title, description, images, favicon } = await r.json();import requests
r = requests.get("https://unfurlr.onrender.com/v1/metadata",
params={"url": "https://github.com"},
headers={"Authorization": f"Bearer {KEY}"}, timeout=15)
print(r.json()["title"])// In the browser use a publishable pk_ key locked to your origin (never your secret key), or proxy via your backend.
function LinkPreview({ url }) {
const [p, setP] = useState(null);
useEffect(() => {
fetch("https://unfurlr.onrender.com/v1/metadata?url=" + encodeURIComponent(url), {
headers: { "X-API-Key": KEY }
}).then(r => r.json()).then(setP);
}, [url]);
if (!p) return null;
return <a href={p.url}><img src={p.images?.[0]} alt="" /><b>{p.title}</b><p>{p.description}</p></a>;
}// app/api/preview/route.ts (key stays on the server)
export async function GET(req: Request) {
const url = new URL(req.url).searchParams.get("url") ?? "";
const r = await fetch("https://unfurlr.onrender.com/v1/metadata?url=" + encodeURIComponent(url), {
headers: { Authorization: `Bearer ${process.env.UNFURLR_KEY}` },
next: { revalidate: 86400 }
});
return Response.json(await r.json(), { status: r.status });
}Why unfurlr
The boring parts of link previews, handled.
Works from the browser
CORS stops your frontend from fetching other sites. We fetch server-side, and this API allows CORS.
Handles messy HTML
Falls back from OG to Twitter tags to plain HTML. Relative image paths come back absolute. oEmbed is discovered automatically.
Batch built in
POST /v1/metadata/batch takes up to 10 URLs.
Predictable
One URL = one request. Hard monthly cap, so no surprise bills.
Safe
Private and internal addresses are blocked (SSRF protection). Errors are clear: timeout, blocked, non-HTML, upstream 4xx/5xx.
Usage at a glance
Check GET /v1/usage, or read the X-Quota-Remaining response header.
v1 reads server-rendered HTML and does not run JavaScript.
Pricing
One plan while we're early. It's free.
Free: 500 requests/month
- 500 requests/month
- Commercial use is OK
- No card needed
- Batch requests included; each URL counts as one request
- Failed fetches don't count
Need always-on or higher limits?
Leave your email and we'll let you know if we add them. Interest list only: no account, no payment, nothing charged. The free plan stays free.
FAQ
Does it run JavaScript on the page?
No. v1 reads server-rendered HTML and does not run JavaScript.
What counts as a request?
One URL = one request, including each URL in a batch. Failed fetches don't count.
What happens when I hit the limit?
There's a hard monthly cap, so no surprise bills. Check GET /v1/usage or the X-Quota-Remaining header.
Can I call it from my frontend?
Yes. The API allows CORS, so you can call it directly from the browser.
Can my site opt out of being fetched?
Yes. See About our crawler for how to opt out.
How does this stay free / will it disappear like other free proxies?
unfurlr is built and run by one person, Smaranjeet. It runs on a free hosting tier with a monthly bandwidth cap. When the cap is reached, the API shows an at-capacity message instead of going down, and URLs that are already cached keep working. The server sleeps when idle, so the first request after a quiet period can take about a minute. The current state is on the status endpoint.
How do I give feedback?
This is an early launch. Tell us what breaks, what's missing, or what you'd pay for: smaran+unfurlr@mail.grokbot.com.
Ship link previews today.
Free key in one click. No card, no SDK, just JSON.