Skip to main content

Confirm

Are you sure?

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 · 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:

POST /meta → 200
{
  "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.

Unfurl a link, with a screenshot fallback
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")}

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 card template
<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

  1. 1
    A user pastes a link. Save the message immediately with a placeholder card.
  2. 2
    Send /meta async with report_to and labels carrying your message id — it is a model call on top of a page load, so it takes seconds, not milliseconds.
  3. 3
    When the webhook arrives, fill the card; if the image is missing, request the screenshot the same way and read result_url from its webhook — the async counterpart of X-Result-Url.
  4. 4
    The 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:

WhatA monthCredits eachCredits
New links unfurled (metadata)15,000575,000
Fallback cards for links with no image4,00014,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_name or 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.

FAQ

Frequently asked questions

Why does /meta use a model instead of just parsing tags?
Because pages declare the same fact in different places — Open Graph, Twitter tags, JSON-LD, microdata, plain HTML — and often disagree. The model reconciles them into one object. That is also why it costs 5 credits rather than 1.
Can I use the screenshot URL directly in an img tag?
Yes. X-Result-Url is a signed link that needs no API key and stays valid for 30 days. Over async, the same link arrives in the webhook as result_url.
What happens with single-page apps that set their tags in JavaScript?
They work. The page is rendered in headless Chrome before its metadata is read, so tags written by scripts are there.
Do repeated pastes of the same link cost credits?
No, not inside max_age (7 days by default). Results are cached per organization, so the second paste — by anyone in your app — is a free cache hit.

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.