# Link Preview API - Open Graph, Meta Tags & URL Metadata (`tidytools/link-preview-metadata`) Actor

Get title, description, preview image, favicon, site name, author, dates and Open Graph / Twitter tags for any list of URLs. JavaScript pages too. $2 per 1,000 URLs.

- **URL**: https://apify.com/tidytools/link-preview-metadata.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** Developer tools, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 url previews

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Link Preview API do?

Give it URLs (as many as you like, in one run) and get the information you need to show a **rich link preview** or to catalogue pages:

- 🏷️ **Title and description** (Open Graph first, then Twitter Card tags, then the HTML title and meta description)
- 🖼️ **Preview image** (`og:image` / `twitter:image`, then schema.org JSON-LD) as an absolute URL
- 🌐 **Site name, page type, favicon, language, theme color, charset**
- ✍️ **Author, published and modified time, publisher and logo**, with a **JSON-LD fallback** when Open Graph tags are missing, and a `sources` object that says where each value came from (`og`, `twitter`, `html` or `jsonld`)
- 👥 **Social profiles**: Facebook, X, LinkedIn, Instagram, YouTube, TikTok, GitHub, Pinterest, Threads, Trustpilot and Discord links found on the page (share buttons and posts are ignored)
- 📰 **RSS/Atom feeds, oEmbed endpoint, web app manifest, all icons** (with sizes) and **email addresses** (`mailto:` links)
- 🔗 **Canonical URL, keywords, Twitter card type, JSON-LD types**, and optionally the raw JSON-LD
- 📦 **All Open Graph and Twitter tags** in one object

It works on protected sites too: pages are read over plain HTTP first (from a second network if a site blocks the first), with automatic fallback to a real browser when the request is refused. Choose **Page fetching: Browser** for pages whose preview tags are only added by JavaScript. When a page comes back without any preview, it is read again as a link-sharing crawler (`facebookexternalhit`), the way chat apps unfurl links. Requests identify themselves as TidyTools. Each URL you paste is read once, the way a chat app unfurls a link you share; nothing is crawled or followed, so `robots.txt` (a rule for crawlers) is not checked.

### Use cases

- **Link previews** in chat apps, CMSs, newsletters and bookmarking tools
- **Content curation**: turn a list of links into cards with image, title, author, date and summary
- **Lead research**: collect a company's social profiles, contact email and RSS feed from its home page
- **SEO and social checks**: find pages with missing preview images or descriptions
- **AI agents**: quickly understand what a URL is about without reading the whole page

### How much does it cost?

| Event | Price |
|---|---|
| URL preview | **$2.00 / 1,000 URLs** |

**Example:** 5,000 URLs that all return a preview cost 5,000 × $0.002 = **$10.00** on the Free plan.

**No start fee.** Higher Apify plans get volume discounts (see the *Pricing* tab). URLs that fail, are blocked or are not HTML pages are not charged; they carry an `errorType` (`not_found`, `blocked`, `timeout`…).

#### Control your cost

- **Charged:** one `URL preview` event per URL that returns a preview (`success: true`, `charged: true`).
- **Free** (`success: false`, `charged: false`, the `error` ends with "(not charged)"):
  - invalid lines (`errorType: "invalid_input"`), one row per line (an empty list, a private/local address such as `localhost` or `169.254.169.254` also gives a free `invalid_input` row and the run still ends normally), and duplicate URLs (read once),
  - sites with an invalid or expired HTTPS certificate (`errorType: "tls_error"`),
  - login walls (`login_wall`: the site sent its login page instead of the content, e.g. Instagram),
  - pages with no preview metadata (`no_metadata`: no Open Graph title, description or image, and the title is missing or only the site name on a deep link, e.g. "Reddit" or "TikTok - Make Your Day"),
  - failed, blocked, timed-out and non-HTML URLs (`blocked`: the site refuses automated visitors with HTTP 401/403 or a bot check; we do not get around that).
- **Before it starts**, the run logs its plan (number of URLs × price = the most it can cost) and compares it with your **maximum charge per run** (set in the run options). The plan is saved as `costPlan` in the `SUMMARY` record.
- **When the maximum is reached**, the run stops cleanly: the status message says so, `SUMMARY.status` is `LIMIT_REACHED` and `SUMMARY.notProcessed` lists how many URLs were not read (and the first 100 of them), so you can run them again.
- **If Apify restarts the run** (server migration or Resurrect), items already finished are skipped and not charged again (`SUMMARY.resumedSkipped`).

#### Price comparison (checked September 2026)

| Actor | Price per 1,000 URLs (free plan) | URLs per run |
|---|---|---|
| **Link Preview & URL Metadata Extractor (this Actor)** | **$2**, no start fee | Up to 100,000 |
| njoylab/url-summary-scraper | $10 | 1 |
| easyapi/url-metadata-crawler | $3 + $0.09 start fee + platform usage | 1 |
| technicaldost/link-preview-api | $2 + $0.001 start fee; the max charge per run must be set to at least $1 | — |
| ntriqpro/metascraper-actor | $1 + $0.01 start fee + platform usage; the max charge per run must be set to at least $0.10 | — |
| dataquarry/website-seo-metadata-checker | Free (SEO-focused meta tag checks) | — |

"—" = not stated in the listing we checked. "Platform usage" means you also pay the Apify compute the run uses; with this Actor it is included in the event price.

**vs Apify Web Fetch:** Web Fetch returns the whole page (Markdown, HTML or links) for an agent to read. This Actor returns small, structured preview fields (title, image, author, dates, logo, social profiles, feeds) with a `sources` object, which is cheaper in tokens when you only need to know what a link is.

### How to use it

1. Paste your links into **URLs or domains**, one per line: full URLs or bare domains like `stripe.com`. A bad line never stops the run: it becomes a row with `errorType: "invalid_input"` (not charged). To upload or link a text or CSV file of links, use the **URL list files or request list** field (every `http(s)` URL in the file is read; a file of bare domains is read one per line).
2. Click **Start**.
3. Export the results as JSON, CSV or Excel, or call the Actor from the API.

#### Input example

This is the input the form is prefilled with; a run with it takes about a minute and costs only cents:

```json
{
    "urlList": [
        "https://github.com/apify/crawlee",
        "https://www.bbc.com/news"
    ]
}
```

#### Output example (real result, September 2026)

A news article whose Open Graph tags have no dates: `publishedTime`, `modifiedTime`, `publisher` and `logo` come from its JSON-LD, and the X profile from its `twitter:site` tag.

```json
{
    "input": "https://www.theverge.com/tech/1001797/nothings-headphone-1-pro-review",
    "inputUrl": "https://www.theverge.com/tech/1001797/nothings-headphone-1-pro-review",
    "inputIndex": 0,
    "url": "https://www.theverge.com/tech/1001797/nothings-headphone-1-pro-review",
    "success": true,
    "title": "The Nothing Headphone 1 Pro put you in the studio",
    "description": "Nothing’s new flagship headphones surprised me.",
    "image": "https://platform.theverge.com/wp-content/uploads/sites/2/2026/09/268785_Nothing_pro_1_headphones_JHIGGINS_1.jpg?quality=90&strip=all&...",
    "siteName": "The Verge",
    "type": "article",
    "author": "John Higgins",
    "publishedTime": "2026-09-29T01:00:00+00:00",
    "modifiedTime": "2026-09-29T01:00:00+00:00",
    "publisher": "The Verge",
    "logo": "https://platform.theverge.com/wp-content/uploads/sites/2/2025/01/verge-placeholder_f212b3.png?...",
    "lang": "en-US",
    "twitterCard": "summary_large_image",
    "structuredDataTypes": ["NewsArticle", "BreadcrumbList"],
    "socialProfiles": {
        "tiktok": "https://www.tiktok.com/@verge",
        "youtube": "https://www.youtube.com/theverge",
        "instagram": "https://www.instagram.com/verge",
        "facebook": "https://www.facebook.com/verge",
        "threads": "https://www.threads.net/@verge",
        "x": "https://x.com/verge"
    },
    "feeds": [{ "url": "https://www.theverge.com/rss/index.xml", "type": "application/rss+xml", "title": "The Verge" }],
    "oembedUrl": null,
    "icons": [{ "href": "https://www.theverge.com/static-assets/icons/apple-touch-icon.png", "rel": "apple-touch-icon", "sizes": "180x180" }, "..."],
    "charset": "utf-8",
    "emails": [],
    "sources": { "title": "og", "description": "og", "image": "og", "author": "html", "publishedTime": "jsonld", "modifiedTime": "jsonld", "siteName": "og", "publisher": "jsonld", "logo": "jsonld" },
    "httpStatus": 200,
    "mode": "fast",
    "via": "backend",
    "charged": true
}
```

A login wall (real result, September 2026). It is not charged:

```json
{
    "input": "https://www.instagram.com/nasa/",
    "inputUrl": "https://www.instagram.com/nasa/",
    "url": "https://www.instagram.com/nasa/",
    "finalUrl": "https://www.instagram.com/accounts/login/",
    "title": "Login • Instagram",
    "success": false,
    "errorType": "login_wall",
    "error": "the site sent a login page instead of the content (https://www.instagram.com/accounts/login/) (not charged)",
    "charged": false
}
```

A product page also returns its oEmbed endpoint and contact email, e.g. Allbirds: `"oembedUrl": "https://www.allbirds.com/products/mens-tree-runners.oembed"`, `"emails": ["help@allbirds.com"]`.

Fields the page does not provide are `null` (or empty lists). Every row keeps your original line in `input` / `inputUrl` (and its position in `inputIndex`), so you can match results back to a spreadsheet; `url` is the address that was read (bare domains get `https://`). When the same URL appears more than once it is read and charged once, and the row lists every original line in `inputs`. `via` tells you where the page was read from: `backend` (our servers), `direct` (Apify's network), `direct-unfurl` (Apify's network as a link-sharing crawler) or `browser`. A `SUMMARY` record in the key-value store has the run `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `NO_RESULTS`, `LIMIT_REACHED`), failure counts by `errorType`, `invalidInputs`, `duplicatesMerged`, `costPlan` and, when the run stopped at your limit, `notProcessed`.

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/link-preview-metadata) to Claude, Cursor or any MCP client, then ask e.g. "Get the title, image, author and social profiles for these 20 links."

```json
{ "urlList": ["https://github.com/apify/crawlee", "stripe.com"] }
```

Failed URLs are not charged and carry an `errorType`.

### Use it from code

```bash
curl -X POST "https://api.apify.com/v2/acts/tidytools~link-preview-metadata/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urlList":["https://github.com/apify/crawlee","stripe.com"]}'
```

Use `urlList` (a plain list of strings). The older `urls` field (`[{"url": "https://…"}]`) still works, but Apify rejects the whole request with HTTP 400 if one of its entries is a bare domain or a blank line; `urlList` accepts anything and reports bad lines as rows instead.

**Schedules and integrations:** run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send the results to Zapier, Make, n8n, Google Sheets, Slack and other apps with Apify integrations. Results can be exported as JSON, CSV, Excel or XML.

### Advanced settings

- **Plain HTTP requests from**: *Auto* (recommended) reads pages from our servers and retries from Apify's network when a site blocks data-center requests, before falling back to a browser.
- **Read social links as a link-sharing crawler**: *Auto* (recommended) reads pages with our own user agent (`TidyToolsFetch/1.0`; in browser mode a real browser's user agent with `TidyTools/1.0` added) and, when a page has no preview or shows a login page, asks again as `facebookexternalhit` from Apify's network. You can also send `facebookexternalhit` or `Twitterbot` first, or turn the retry off. No extra charge.
- **Time limit per URL**: default 120 seconds, all retries included. Slower URLs get `errorType: "timeout"` and are not charged. 0 = no limit.
- **HTTP 429 (Too Many Requests)**: a URL that is rate-limited on every route is read once more after a short wait, inside the time limit. If it still answers 429, the row has `errorType: "rate_limited"` and is not charged.
- **Proxy**: optional Apify Proxy for requests sent from Apify's network (billed to your Apify account).

### Limitations

- Only public `http`/`https` pages. Sites that show a login page instead of a preview (e.g. many Instagram profiles) return a free `login_wall` row.
- Some sites show their login page to data-center networks whatever the user agent. Instagram profiles are inconsistent: in our September 2026 test they returned the login page from Apify's network (reported as `login_wall`, not charged), while a test run in October 2026 got the public profile preview (title, picture); expect either result. We have not tested this with a residential proxy.
- Only HTML pages are previewed: PDFs, images and JSON endpoints return a free `unsupported` row ("not an HTML page"). Sites that refuse automated visitors on every route (HTTP 401/403 or a bot check, e.g. some Chinese portals) return a free `blocked` row that says so; the Actor identifies itself honestly and does not bypass bot protection.
- The image URL of some sites (Instagram, X) is a signed or size-limited CDN link that can expire; download it soon if you need to keep it. A TikTok profile returned a full preview through the crawler retry.
- Values come from what the page declares (meta tags, links and JSON-LD); pages without them have fewer fields.
- Social profiles are the links the page shows; a site that links to someone else's profile (e.g. an embedded post author) can appear too.

### Responsible use

- The Actor reads only the public metadata of a page you give it (title, description, image, and the author or byline the page itself publishes). It does not log in and does not build profiles of people.
- An author byline is the name the publisher shows on the page; keep it only when you need it (for example to credit an article) and follow the data-protection laws that apply to you.

### FAQ

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**Which Open Graph and meta tags does it read, and how is the OG image found?** Title and description come from Open Graph first, then the Twitter Card tags, then the HTML title and meta description. The preview image comes from `og:image` / `twitter:image`, then schema.org JSON-LD, and is returned as an absolute URL. All Open Graph and Twitter tags are also returned in one object, and a `sources` object says where each value came from (`og`, `twitter`, `html` or `jsonld`).

**Does it work on JavaScript-heavy or protected sites?** Yes. Pages are read over plain HTTP first (from a second network if a site blocks the first), with automatic fallback to a real browser when the request is refused (preview tags that only JavaScript adds need *Browser*); **Page fetching (HTTP or browser)** lets you choose Auto, HTTP only or Browser. When a page has no preview or shows a login page, **Read social links as a link-sharing crawler** reads it again as `facebookexternalhit` at no extra charge, but some sites (e.g. Instagram profiles) still return a login page, reported as `login_wall` and not charged.

**Does it check robots.txt?** No. `robots.txt` is a rule for crawlers, and this Actor does not crawl: it reads only the URLs you paste, once each, like a chat app showing a link preview, and it never follows links. Every request names TidyTools in its user agent, except the optional link-sharing crawler retry described above.

**What is charged and what is free?** You pay $2.00 per 1,000 URLs, one `URL preview` event per URL that returns a preview, with no start fee. Invalid lines, duplicate URLs, login walls, pages with no preview metadata (`no_metadata`), and failed, blocked, timed-out, bad-certificate or non-HTML URLs (PDFs, images) are free; every row has `charged: true` or `false`. A page with only an HTML title (e.g. example.com) still counts as a preview and is charged; a page with nothing usable is `no_metadata` and free.

**Can I use it as a link preview API or run it on a schedule?** Yes. Call it from code through the Apify API (`run-sync-get-dataset-items`) with your Apify API token and a plain `urlList` of URLs or bare domains, or connect it to AI agents through Apify's MCP server. You can also run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send results to Zapier, Make, n8n, Google Sheets or Slack.

### Related Actors

More tools by TidyTools that work well with this one:

- [Sitemap URL Extractor](https://apify.com/tidytools/sitemap-url-extractor): list every URL a site publishes in its sitemaps
- [SEO Audit Crawler](https://apify.com/tidytools/seo-audit-crawler): audit titles, links, speed and structured data across a whole site
- [Broken Link Checker](https://apify.com/tidytools/broken-link-checker): find 404 and dead links on any website
- [Bulk URL Status Checker](https://apify.com/tidytools/bulk-url-status-checker): check HTTP status and redirects for a list of URLs

### Support

Open an issue in the **Issues** tab with the URL. Issues are checked regularly.

# Actor input Schema

## `urlList` (type: `array`):

Main input (fill this, or `urls`). Pages to read metadata from (articles, products, videos, any web page). One per line: full URLs or bare domains like stripe.com. Up to 100,000 per run. Invalid lines become a result row with errorType "invalid_input" (not charged) instead of failing the run.

## `urls` (type: `array`):

Older URL field, kept for existing API calls. Use it to upload or link a .txt/.csv file of URLs (every http(s) URL in the file is read; a file with bare domains is read one per line). Full URLs only here: bare domains such as stripe.com go in "URLs or domains" above.

## `mode` (type: `string`):

"Auto" reads the page's HTML directly and uses a real browser only for sites that block simple requests. Values: auto = Auto (recommended); fast = Fast (HTTP only); browser = Browser (JavaScript rendering).

## `includeJsonLd` (type: `boolean`):

Add the page's schema.org JSON-LD objects to each result. Preview fields already use JSON-LD as a fallback when Open Graph tags are missing.

## `maxConcurrency` (type: `integer`):

How many URLs are processed at the same time.

## `httpVia` (type: `string`):

Some sites block requests from data centers. Auto retries from a second network before falling back to a real browser. If both are refused, Auto also tries our second server (Oracle, different IP) before a real browser.

## `userAgent` (type: `string`):

Some sites (e.g. Instagram, TikTok) give their full Open Graph tags only to link-sharing crawlers. The crawler request is sent from Apify's network (or your proxy). No extra charge.

## `pageTimeoutSecs` (type: `integer`):

A URL that takes longer (all retries included) gets errorType "timeout" and is not charged. 0 = no limit.

## `proxyConfiguration` (type: `object`):

Only used for requests sent from Apify's network. Apify proxy usage is billed to your Apify account.

## Actor input object example

```json
{
  "urlList": [
    "https://github.com/apify/crawlee",
    "https://www.bbc.com/news"
  ],
  "mode": "auto",
  "includeJsonLd": false,
  "maxConcurrency": 10,
  "httpVia": "auto",
  "userAgent": "auto",
  "pageTimeoutSecs": 120
}
```

# Actor output Schema

## `previews` (type: `string`):

No description

## `full` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urlList": [
        "https://github.com/apify/crawlee",
        "https://www.bbc.com/news"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/link-preview-metadata").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urlList": [
        "https://github.com/apify/crawlee",
        "https://www.bbc.com/news",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidytools/link-preview-metadata").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urlList": [
    "https://github.com/apify/crawlee",
    "https://www.bbc.com/news"
  ]
}' |
apify call tidytools/link-preview-metadata --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tidytools/link-preview-metadata"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ACySefAmKCzI7W4KG/builds/mYQKWEsvV4dQ3TEfW/openapi.json
