Skip to main content

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 1 credit. 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 reconciles the different places a page can declare the same fact. You get one object, with the same keys for every page:

POST /meta → 200
{
  "final_url": "https://example.com/blog/launch-week",
  "canonical_url": "https://example.com/blog/launch-week",
  "title": "Everything we shipped in launch week",
  "description": "Five days, five releases: what's new and why it matters.",
  "site_name": "Example Blog",
  "type": "article",
  "language": "en",
  "locale": "en-US",
  "authors": [
    {
      "name": "Jane Doe",
      "url": "https://example.com/authors/jane",
      "profiles": [
        "https://x.com/janedoe"
      ]
    }
  ],
  "published_at": "2026-09-01T08:00:00+00:00",
  "modified_at": "2026-09-03T14:30:00+00:00",
  "image": {
    "url": "https://example.com/images/launch-week.jpg",
    "width": 1200,
    "height": 630,
    "alt": "The team on stage",
    "type": "image/jpeg"
  },
  "video": null,
  "favicon_url": "https://example.com/favicon.ico",
  "icons": [
    {
      "url": "https://example.com/favicon.ico",
      "rel": "icon",
      "sizes": "32x32",
      "type": null
    },
    {
      "url": "https://example.com/apple-touch-icon.png",
      "rel": "apple-touch-icon",
      "sizes": "180x180",
      "type": null
    }
  ],
  "logo_url": "https://example.com/logo.svg",
  "feeds": [
    {
      "url": "https://example.com/feed.xml",
      "type": "application/rss+xml",
      "title": "Example Blog"
    }
  ],
  "keywords": [
    "launch",
    "product"
  ],
  "theme_color": "#0f172a",
  "generator": "WordPress 6.6",
  "manifest_url": "https://example.com/site.webmanifest",
  "robots": {
    "index": true,
    "follow": true
  },
  "open_graph": {
    "title": "Everything we shipped in launch week",
    "description": "Five days, five releases.",
    "image": "https://example.com/images/launch-week.jpg",
    "url": "https://example.com/blog/launch-week",
    "type": "article",
    "site_name": "Example Blog",
    "locale": "en_US"
  },
  "twitter": {
    "card": "summary_large_image",
    "site": "@example",
    "creator": "@janedoe",
    "title": null,
    "description": null,
    "image": null
  },
  "alternates": [
    {
      "hreflang": "es",
      "url": "https://example.com/es/blog/launch-week"
    }
  ],
  "oembed_url": "https://example.com/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fexample.com%2Fblog%2Flaunch-week",
  "structured_data_types": [
    "BlogPosting",
    "Organization"
  ]
}

URLs come back absolute, resolved against the page, so image.url and favicon_url go straight into your markup, and image.width and image.height — when the page declares them — let you reserve the space before it loads. A field the page doesn't declare is 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 image 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("image") or {}).get("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. published_at and authors 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 loads the page in a browser, 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,000115,000
Fallback cards for links with no image4,00014,000

19,000 credits; the cheapest plan that covers it is Starter: $19 a month for 20,000 credits. 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

What if the page declares its title three different ways?
Pages declare the same fact in different places — Open Graph, Twitter tags, JSON-LD, microdata, plain HTML — and often disagree. /meta takes each field from the first source in a fixed order of precedence, so the answer is predictable, and returns the raw Open Graph and Twitter tags alongside.
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.