Skip to main content

Metadata API

Link previews and Open Graph from any URL

Everything a link preview needs, in one predictable shape — however the page chose to declare it.

By · Last updated: September 2026

TL;DR

POST a URL to /meta and get one JSON object with the same keys on every page: title, description, site name, kind of page, language, authors, publication and update dates, share image with its size, favicon, icons, logo, feeds, keywords, canonical URL, robots, hreflang alternates, oEmbed endpoint and the raw Open Graph and Twitter tags. URLs come back absolute, and tags injected by JavaScript are read too. 1 credit per URL.

Free plan, no credit card. 1,000 credits a month.

Try it now — no signup

Try it on a page

Try your own URL

Free · no signup · 2 runs every 10 minutes

Paste up to 10 URLs

Each URL counts as one of your free runs; the ones past the limit are listed with a free API key to run them.

Your result will appear here.

Pick one of the pages above to get started.

The request

One POST, one bearer token

POST /meta
curl -X POST https://urlpipe.dev/meta \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/blog/launch-week", "sync": true}'
Response
{
  "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"
  ]
}

The response

The fields you get

Every key is always present. A value the page doesn't declare is null, or an empty list — never a guess, never an invented URL — so your code handles one shape.

FieldWhat it is
titleThe page's own title, without the site name around it
descriptionThe page description
site_nameThe name of the site the page belongs to
typeThe kind of page: article, website, product, video, profile, recipe…
language · localeISO 639-1 code (en), and the most specific tag declared for it (en-US)
authorsEach author's name, their page and their other profiles
published_at · modified_atISO 8601 dates of first publication and last update
image · videoThe share image and video, with the size and type the page declares
favicon_url · icons · logo_urlThe tab icon, every declared icon, and the site's logo
feedsRSS, Atom and JSON feeds, with their titles
keywordsKeywords and tags from meta tags, article tags and structured data
canonical_url · alternatesThe canonical URL, and the page in other languages
open_graph · twitterThe raw og: and twitter: tags, exactly as declared
robots · oembed_url · manifest_url · theme_color · generator · structured_data_typesCrawl directives, embed endpoint, app manifest, theme colour, CMS and schema.org types

Reconciliation

When the page says it three different ways

Real pages declare the same thing in several places with different values — an og:title written for social, a <title> with the site name, an <h1>. /meta takes the first source in a fixed order that the page actually provides:

FieldOrder of precedence
titleog:title → twitter:title → <title> → <h1>, with the site's name stripped
descriptionmeta description → og:description → twitter:description → JSON-LD
languageJSON-LD inLanguage → <html lang> → og:locale → the text itself
imageog:image → twitter:image → JSON-LD image
authorsJSON-LD author → microdata → meta author → rel=author → the byline
published_atJSON-LD datePublished → article:published_time → citation and Dublin Core dates
favicon_urlrel=icon or shortcut icon → alternate icon → apple-touch-icon

Relative URLs are resolved against the page — a page that declares /favicon.ico comes back with a full URL you can use. No model is involved: the page's own declarations are read out of the rendered DOM, so the same page always gives the same answer, at 1 credit.

Rendering

Tags that only exist after JavaScript

Single-page apps often set their title and Open Graph tags from script — React Helmet, Vue Meta, Next.js client navigation. A metadata API that reads the raw HTML sees the app shell's defaults, or nothing. /meta reads the page after it has rendered in headless Chrome, so script-injected tags are there. Note that most social networks' own unfurlers don't run JavaScript, so if the tags only appear after rendering, your links may still preview badly on those sites — the Open Graph guide covers how to check.

Fallback

When there is no og:image

Plenty of pages have no preview image. Ask for a screenshot in the same visit with /scrape, and use it when image is null:

A preview image, always
import requests

res = requests.post(
    "https://urlpipe.dev/scrape",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "url": url,
        "operations": ["meta", "screenshot"],
        "screenshot_options": {"full_page": False, "format": "webp"},
        "sync": True,
    },
)
ops = res.json()["operations"]
meta = ops["meta"]["result"]
image = (meta["image"] or {}).get("url")  # or fall back to the screenshot's Base64 in ops["screenshot"]["result"]

The screenshot adds 1 credit. For a hosted link rather than Base64, call /screenshot on its own and read its X-Result-Url header: a no-key image URL, valid for 30 days, that goes straight into an <img>.

In practice

From response to preview card

A link preview needs four things: a title, a line of description, an image and a small icon. The response maps onto them directly, with sensible fallbacks when a field is null:

A preview card
const meta = await res.json()

const card = {
  title: meta.title ?? new URL(url).hostname,
  site: meta.site_name,
  description: meta.description ?? "",
  image: meta.image?.url,              // null → use a screenshot, see above
  icon: meta.favicon_url,
  byline: [meta.authors[0]?.name, meta.published_at?.slice(0, 10)].filter(Boolean).join(" · "),
}

Because every URL field comes back absolute, the card works wherever you render it — no need to remember the page URL and resolve /favicon.ico yourself. image.width and image.height let you reserve the space before the image loads, language tells you which way to set text direction or whether to translate, and feeds lets a reader app offer to subscribe.

At scale

Unfurling at volume

Link previews are read far more often than pages change. Results are stored and reused for 7 days by default, up to 30 with max_age, and a reused result is free — so the second person pasting the same link costs nothing. A burst of identical requests while the first is still running waits for it, also free. Requests are async by default; send sync: true when the preview is on the critical path.

Limits

What it does not do

  • No embed HTML. It returns the page's oEmbed endpoint, not a player or rich embed for YouTube or Twitter.
  • No custom fields. The shape is fixed; there is no schema to extend.
  • No guessing. A value the page doesn't declare is null — the one exception is the language, which is read from the text when the page declares none.
  • Public pages only. Private and intranet addresses are refused.

Pricing

What it costs

CallCreditsFree (calls/mo)Starter (calls/mo)Pro (calls/mo)Scale (calls/mo)
/meta1 credit1,00020,00055,000175,000

Cache hits and failed requests cost nothing. Paid plans are never cut off: past the allowance, extra credits are $1.50 per 1,000 credits. See every plan.

FAQ

Frequently asked questions

Which fields does the metadata API return?
title, description, site_name, type, language, locale, authors, published_at, modified_at, image, video, favicon_url, icons, logo_url, feeds, keywords, theme_color, generator, manifest_url, robots, open_graph, twitter, alternates, oembed_url and structured_data_types. Every key is present; a value the page doesn't declare is null or an empty list.
Does it read Open Graph tags added by JavaScript?
Yes. The page is rendered in headless Chrome before its metadata is read, so tags a single-page app sets from script are included.
What happens when og:title and the <title> disagree?
og:title wins, then twitter:title, then <title>, then the <h1>. Each field has a fixed order of precedence, so the answer is predictable — and the raw og: and twitter: tags are in the response too.
How much does it cost?
1 credit per URL, the same as /markdown and /html: no model is involved. A reused result is free.
What if the page has no og:image?
image comes back null. Request a screenshot in the same /scrape call and use it as the preview image instead.

Make your first request in five minutes.

Free plan, no card. Confirm your email and your API key is live — you'll be making real requests in minutes.