Use cases
Every pasted link, a proper card
Title, description, image and favicon for any URL — and a screenshot when the page never set an image.
By Roger Campos · Last updated: September 2026
TL;DR
A link preview needs a title, a description, an image and a favicon for any URL a user pastes. URLpipe's /meta returns them as one JSON object with URLs made absolute, for 5 credits. When a page has no image, /screenshot captures its first screen for 1 credit and hands back a link you can use in an img tag directly.
Free plan, no credit card. 1,000 credits a month.
The job
The card, and where each field comes from
Chat apps, CMS editors, bookmark managers and CRMs all do the same thing when someone pastes a URL: fetch the page, read its tags, and draw a card. The hard part is the long tail — pages with Open Graph tags and without, JSON-LD instead of meta tags, relative image paths, and single-page apps that write their tags from JavaScript.
/meta loads the page in a browser and reads it with a model, which reconciles the different places a page can declare the same fact. You get one object:
{
"title": "Example Domain",
"description": "Illustrative examples in documents.",
"language": "en",
"main_image_url": "https://example.com/cover.jpg",
"favicon_url": "https://example.com/favicon.ico",
"author_name": "Jane Doe",
"feed_url": "https://example.com/feed.xml",
"publication_date": "2026-01-01T00:00:00Z",
"additional_author_information": { "twitter": "@janedoe" }
}URLs come back absolute, resolved against the page, so main_image_url and favicon_url go straight into your markup. Any field can be null.
Fallback
When there is no image: a screenshot you can link to
About the worst card is a title with a grey box. When main_image_url is null, take a screenshot of the page's first screen at card proportions. The response carries an X-Result-Url header: a signed link to the image that needs no API key, valid for 30 days, so it goes into an <img> without you storing or proxying anything. Copy the bytes to your own storage if the card has to outlive that.
import os, requests
API = "https://urlpipe.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['URLPIPE_API_KEY']}"}
def unfurl(url: str) -> dict:
meta = requests.post(f"{API}/meta", headers=HEADERS,
json={"url": url, "sync": True}, timeout=75).json()
image = meta.get("main_image_url")
if not image: # no og:image: photograph the first screen instead
shot = requests.post(f"{API}/screenshot", headers=HEADERS, json={
"url": url,
"sync": True,
"screenshot_options": {"full_page": False, "viewport_width": 1200,
"viewport_height": 630, "format": "webp"},
"page_options": {"block_cookie_banners": True},
}, timeout=75)
image = shot.headers.get("X-Result-Url")
return {"url": url, "title": meta.get("title"),
"description": meta.get("description"),
"image": image, "icon": meta.get("favicon_url")}const API = "https://urlpipe.dev";
const headers = {
Authorization: `Bearer ${process.env.URLPIPE_API_KEY}`,
"Content-Type": "application/json",
};
const post = (path, body) =>
fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
export async function unfurl(url) {
const meta = await (await post("/meta", { url, sync: true })).json();
let image = meta.main_image_url;
if (!image) {
// no og:image: photograph the first screen instead
const shot = await post("/screenshot", {
url,
sync: true,
screenshot_options: { full_page: false, viewport_width: 1200,
viewport_height: 630, format: "webp" },
page_options: { block_cookie_banners: true },
});
image = shot.headers.get("X-Result-Url");
}
return { url, title: meta.title, description: meta.description,
image, icon: meta.favicon_url };
}# 1. The card's text, image and icon
curl -X POST https://urlpipe.dev/meta \
-H "Authorization: Bearer $URLPIPE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/post", "sync": true}'
# 2. Only if main_image_url was null: a 1200x630 first screen, and its keyless link
curl -sD - -o /dev/null -X POST https://urlpipe.dev/screenshot \
-H "Authorization: Bearer $URLPIPE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/post", "sync": true,
"screenshot_options": {"full_page": false, "viewport_width": 1200,
"viewport_height": 630, "format": "webp"}}' \
| grep -i '^x-result-url'Rendering
Drawing the card safely
Everything in the card comes from a page someone else wrote, so treat it as untrusted input. Escape the title and description like any user text, cap their length, and fall back to the favicon — or to the domain name — when there is no image at all.
<a class="card" href="{{ url }}" rel="noopener nofollow ugc" target="_blank">
{% if image %}
<img src="{{ image }}" alt="" loading="lazy" referrerpolicy="no-referrer"
width="1200" height="630">
{% endif %}
<div class="card-body">
<img src="{{ icon }}" alt="" width="16" height="16" loading="lazy">
<strong>{{ title | truncate: 90 }}</strong>
<p>{{ description | truncate: 200 }}</p>
</div>
</a>language is an ISO 639-1 code, useful for a lang attribute on the card so screen readers pronounce a foreign title correctly. publication_date and author_name turn a generic card into an article card when they are present. The Open Graph guide covers what pages declare and how often they get it wrong.
Flow
Do it when the link is posted, not when the card is drawn
- 1A user pastes a link. Save the message immediately with a placeholder card.
- 2Send
/metaasync withreport_toandlabelscarrying your message id — it is a model call on top of a page load, so it takes seconds, not milliseconds. - 3When the webhook arrives, fill the card; if the image is missing, request the screenshot the same way and read
result_urlfrom its webhook — the async counterpart ofX-Result-Url. - 4The next person to paste the same URL inside seven days gets the stored result, free and at once: results are cached across your whole organization, not per user.
/scrape with ["meta", "screenshot"] gets both off one page visit when you always want the screenshot. A screenshot that sets its own viewport loads the page again anyway, so for card-sized fallbacks the two separate calls above are the simpler shape.
Credits
What it costs
An app whose users paste 15,000 distinct links a month, a quarter of which have no image, at list price:
| What | A month | Credits each | Credits |
|---|---|---|---|
| New links unfurled (metadata) | 15,000 | 5 | 75,000 |
| Fallback cards for links with no image | 4,000 | 1 | 4,000 |
79,000 credits; the cheapest way to buy it is Pro ($49, 55,000 credits) plus 24,000 credits of overage at $1.50 per 1,000 credits — $85.00 a month. Popular links repeat, and every repeat inside the freshness window is free, so distinct URLs — not pastes — are what you are billed for.
Limits
What URLpipe does not do for previews
- oEmbed and players. It returns metadata and images, not the embeddable video or tweet widget a provider's oEmbed endpoint gives you.
- Every tag. The fields are the nine above. There is no
og:type,og:site_nameor canonical URL field. - Instant answers. /meta renders the page and calls a model; plan for seconds, and do it async.
- Keep your images forever. The keyless link lasts 30 days, the retention window. Copy images you need longer.
Endpoints
The endpoints on this page
- Check a page's Open Graph tags and metadata
Paste a link and get the page's metadata — title, description, author, publication date, feed and main image — cleaned into one structured object.
Try it free - Screenshot any website from its URL
Paste a link and get a full-page PNG of the rendered page — JavaScript executed, exactly as a real browser would draw it. Great for previews, monitoring and visual QA.
Try it free
FAQ
Frequently asked questions
Why does /meta use a model instead of just parsing tags?
Can I use the screenshot URL directly in an img tag?
What happens with single-page apps that set their tags in JavaScript?
Do repeated pastes of the same link cost credits?
Build it on the free plan.
Free plan, no card. Confirm your email and your API key is live — you'll be making real requests in minutes.