# Link Preview API \[$0.99/1K💰] | OG + JSON-LD (`ahmed_jasarevic/link-preview-api`) Actor

Turn any URL into 42 metadata fields - Open Graph, Twitter Cards, JSON-LD, oEmbed, feeds, favicon, canonical and publish dates - over plain HTTP. Batch runs, a Standby REST API and MCP, with SSRF protection built in.

- **URL**: https://apify.com/ahmed_jasarevic/link-preview-api.md
- **Developed by:** [Ahmed Jasarevic](https://apify.com/ahmed_jasarevic) (community)
- **Features:** Limited permissions, Single-tenant, Server interface
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.94 / 1,000 link previews

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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

## Link Preview API

Turn any URL into 49 structured metadata fields — Open Graph, Twitter Cards, JSON-LD, oEmbed, favicons, feeds, hreflang, canonical and publish dates — as a batch Apify run, a Standby REST API, or a call from an AI agent over MCP.

- **49 fields, not 10.** Title, description, images, site name, type, canonical, favicon, apple-touch icon, manifest, RSS/Atom feeds, hreflang alternates, language, author, publisher, published and modified times, robots, theme colour, Twitter card details, Schema.org types, JSON-LD and oEmbed.
- **Two ways to call it.** Batch it as a normal Apify run, or point your app at the Standby HTTP API — `GET /v1/preview?url=` for one URL, `POST /v1/unfurl` for up to 100.
- **SSRF protection built in.** Cloud metadata hosts, `localhost`, `.local` and private IP ranges are refused by default, including after DNS resolution and across redirects. Gate it off with `allowPrivateNetwork` if you genuinely need internal URLs.
- **No third-party API key.** No Microlink, no OpenGraph.io, no per-request vendor bill.
- **About 2× cheaper** than the verified Apify alternative at `$0.99` per 1,000 previews on the FREE tier, against `$2.00` per 1,000.

### Main Use Cases

- **Bulk link unfurling** — enrich up to 100 URLs per `POST /v1/unfurl` call, or a whole list per Apify run
- **Slack and Discord-style preview cards** — map `title`, `description`, `image`, `siteName` and `favicon` into your own markup
- **Open Graph auditing** — check whether your own pages expose complete `og:` tags, canonical URLs and images
- **JSON-LD and Schema.org extraction** — pull structured product, article, recipe and organisation data
- **CRM and lead enrichment** — attach page titles, descriptions and images to company or contact records
- **RSS feed and hreflang detection** — discover feeds and language alternates from any page
- **URL metadata for AI agents** — expose a metadata endpoint agents can call via MCP instead of fetching raw HTML

### How It Works

1. Each URL is validated and screened against the SSRF guard. Blocked hosts and private addresses are refused before any request is made, and re-checked after DNS resolution and at each redirect hop.
2. The page is fetched over HTTP with retries, a configurable timeout, and redirect following. Response HTML is size-capped.
3. Metadata is parsed in priority order: Open Graph, then Twitter Card tags, then HTML fallbacks (`<title>`, meta description, canonical link, favicon), then platform-specific oEmbed endpoints when `fetchOembed` is on.
4. `includeJsonLd` extracts every `application/ld+json` block, with `schemaTypes` giving the detected Schema.org types.
5. `includeRawTags` adds the raw meta tags map for auditing work.
6. An optional TTL cache (`cacheTtlSecs`, `useKvCache`) avoids refetching unchanged URLs.

Two dataset views are available: `overview` for a quick scan and `details` for the full record.

> **Honest limitation:** this Actor does **not** execute client-side JavaScript. It reads the HTML returned by the initial HTTP response plus oEmbed endpoints. Client-rendered single-page apps often return minimal metadata unless they also expose server-rendered `og:` tags or an oEmbed endpoint. That is the same trade-off the popular open-source unfurl libraries make.

### Fetch Link Metadata Without a Paid Preview API or a Third-Party Key

Commercial preview APIs solve the hard parts — proxy tiers, JavaScript rendering, edge caching — and charge per request accordingly. If you only need the metadata tags a page already publishes, that is the expensive part you are paying for.

This Actor reads the published tags directly, over plain HTTP, with no vendor key and no per-request bill. For the common case — building preview cards, auditing your own pages, enriching records — that is enough. The trade-off is documented above: no client-side JavaScript rendering.

### Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `urls` | array | no | — | URLs to preview. Bare strings or `{ "url": "..." }` objects are both accepted. |
| `maxItems` | integer | no | `0` | Limit how many URLs are processed. `0` (or any non-positive value) processes all of them. |
| `maxConcurrency` | integer | no | `10` | Parallel fetches. |
| `requestTimeoutSecs` | integer | no | `20` | Per-request timeout in seconds. |
| `maxRetries` | integer | no | `1` | Retries per URL on transient failures. |
| `includeJsonLd` | boolean | no | `true` | Extract JSON-LD blocks and detected Schema.org types. |
| `includeRawTags` | boolean | no | `false` | Include the raw meta tags map. Useful for audits; increases row size. |
| `fetchOembed` | boolean | no | `true` | Fall back to platform oEmbed endpoints when a page has weak or no tags. |
| `cacheTtlSecs` | integer | no | `3600` | Cache time-to-live in seconds. |
| `useKvCache` | boolean | no | `false` | Persist the cache in a key-value store so it survives across runs. |
| `allowPrivateNetwork` | boolean | no | `false` | Permit `localhost`, `.local` and private IP targets. Off by default for SSRF safety. |

### Output

One row per URL (49 fields):

`url`, `finalUrl`, `domain`, `ok`, `status`, `error`, `contentType`, `responseTimeMs`, `attempts`, `truncated`, `htmlBytes`, `charset`, `title`, `description`, `image`, `images`, `siteName`, `type`, `canonical`, `favicon`, `appleTouchIcon`, `manifestUrl`, `feedUrl`, `feeds`, `hreflang`, `language`, `author`, `publisher`, `publishedTime`, `modifiedTime`, `robots`, `themeColor`, `twitterCard`, `twitterSite`, `twitterCreator`, `openGraph`, `twitter`, `schemaTypes`, `jsonLd`, `metaTags`, `oembed`, `oembedRaw`, `oembedEndpoint`, `redirectChain`, `isShortened`, `cleanUrl`, `excerpt`, `cacheHit`, `fetchedAt`

Every URL produces a row even when it fails. Check `ok` first: on failure `error` explains why and `status` carries the HTTP status where one was received. A blocked-by-SSRF URL returns `ok: false` with an error such as `Private addresses are not allowed` — that is a deliberate refusal, not a network fault.

### Example Input

```json
{
  "urls": [
    "https://github.com",
    "https://www.theverge.com/2026/1/28/some-article",
    "https://news.ycombinator.com"
  ],
  "includeJsonLd": true,
  "fetchOembed": true,
  "includeRawTags": false,
  "cacheTtlSecs": 3600
}
```

### Example Output

```json
{
  "url": "https://www.theverge.com/2026/1/28/some-article",
  "finalUrl": "https://www.theverge.com/2026/1/28/some-article",
  "domain": "www.theverge.com",
  "ok": true,
  "status": 200,
  "error": null,
  "contentType": "text/html; charset=utf-8",
  "responseTimeMs": 412,
  "attempts": 1,
  "truncated": false,
  "htmlBytes": 184213,
  "charset": "utf-8",
  "title": "The chip supply question nobody wants to answer",
  "description": "Fabrication capacity, not design, is now the bottleneck for advanced packaging — and the industry has been slow to say so.",
  "image": "https://www.theverge.com/hero-chip.jpg",
  "images": [
    { "url": "https://www.theverge.com/hero-chip.jpg", "width": 1200, "height": 628, "alt": "Advanced packaging wafer" },
    { "url": "https://www.theverge.com/hero-chip-thumb.jpg", "width": 600, "height": 314, "alt": null }
  ],
  "siteName": "The Verge",
  "type": "article",
  "canonical": "https://www.theverge.com/2026/1/28/some-article",
  "favicon": "https://www.theverge.com/favicon.ico",
  "appleTouchIcon": "https://www.theverge.com/apple-touch-icon.png",
  "manifestUrl": "https://www.theverge.com/manifest.json",
  "feedUrl": "https://www.theverge.com/rss/index.xml",
  "feeds": [
    { "url": "https://www.theverge.com/rss/index.xml", "type": "application/rss+xml", "title": "The Verge" }
  ],
  "hreflang": [
    { "lang": "en-US", "href": "https://www.theverge.com/2026/1/28/some-article" },
    { "lang": "de", "href": "https://www.theverge.com/de/2026/1/28/some-article" }
  ],
  "language": "en-US",
  "author": "A. Reporter",
  "publisher": "Vox Media",
  "publishedTime": "2026-01-28T14:02:11Z",
  "modifiedTime": "2026-01-28T16:40:03Z",
  "robots": "max-image-preview:large, noarchive",
  "themeColor": "#000000",
  "twitterCard": "summary_large_image",
  "twitterSite": "@theverge",
  "twitterCreator": "@areporter",
  "openGraph": {
    "og:title": "The chip supply question nobody wants to answer",
    "og:description": "Fabrication capacity, not design, is now the bottleneck for advanced packaging.",
    "og:image": "https://www.theverge.com/hero-chip.jpg",
    "og:type": "article",
    "og:site_name": "The Verge"
  },
  "twitter": {
    "twitter:card": "summary_large_image",
    "twitter:site": "@theverge",
    "twitter:title": "The chip supply question nobody wants to answer"
  },
  "schemaTypes": ["NewsArticle", "Article"],
  "jsonLd": [
    {
      "@context": "https://schema.org",
      "@type": "NewsArticle",
      "headline": "The chip supply question nobody wants to answer",
      "datePublished": "2026-01-28T14:02:11Z",
      "author": { "@type": "Person", "name": "A. Reporter" },
      "publisher": { "@type": "Organization", "name": "Vox Media" }
    }
  ],
  "metaTags": null,
  "oembed": null,
  "oembedRaw": null,
  "oembedEndpoint": null,
  "redirectChain": [],
  "isShortened": false,
  "cleanUrl": "https://www.theverge.com/2026/1/28/some-article",
  "excerpt": "Fabrication capacity, not design, is now the bottleneck for advanced packaging — and the industry has been slow to say so.",
  "cacheHit": false,
  "fetchedAt": "2026-02-11T11:22:08.415Z"
}
```

### Standby REST API

This Actor runs a container web server, so you can call it directly over HTTP without starting an Apify run:

```
GET  /              health and service info (JSON)
GET  /health        plain-text liveness probe -> ok
GET  /v1/preview?url=<encoded-url>     one URL
POST /v1/unfurl     {"urls": [...]}    up to 100 URLs per request
```

The batch endpoint caps at **100 URLs per request**. CORS preflight (`OPTIONS`) is handled, so browser clients can call it. The full OpenAPI description is in `.actor/web_server_openapi.json` and renders as the **Endpoints** tab in Apify Console.

### Pricing

Pay-per-event at `$0.99` per 1,000 previews on the FREE tier, with a low per-run starting cost. Each URL fetched counts as one event, including ones that fail or are blocked — blocked requests are refused locally but are still counted.

### Integrations & Automation

- **Standby REST API** — point any app at the container URL for single or batch (100-URL) unfurling.
- **Apify API** — schedule batch enrichment of large URL lists and read the dataset.
- **Schedules** — re-audit your own pages daily for new pages, broken images or regressed tags.
- **Webhooks** — fire downstream enrichment when a batch completes.
- **MCP** — callable by AI agents through the Apify MCP server, so an agent can resolve a URL to metadata with one tool call.

### Related Actors

- `ahmed_jasarevic/preply-scraper` — extract structured profile data from tutor URLs after resolving their preview metadata
- `ahmed_jasarevic/goodreads-scraper` — extract book metadata and review text from Goodreads URLs

### FAQ

#### Why not just use Microlink or OpenGraph.io?

Those solve JavaScript rendering and proxy routing, which cost extra per request. If you only need the metadata a page already publishes, this Actor gives you the same fields over plain HTTP with no vendor account or API key, at roughly half the per-preview price of the comparable Apify Actor. If you genuinely need rendered metadata for client-rendered SPAs, those tools are the better fit — this Actor does not run JavaScript.

#### Why is my link preview empty for some sites?

The most common cause is a client-rendered single-page app: the initial HTML response is an empty shell, so there are no `og:` tags to read. Check `ok`, `status` and `htmlBytes` — a tiny `htmlBytes` with `ok: true` is the signature of that case. Enabling `fetchOembed` helps on platforms that publish an oEmbed endpoint.

#### What is the difference between `image` and `images`?

`image` is the single best image (Open Graph preferred, then Twitter Card). `images` is the full array with `width`, `height` and `alt` for each. Use `image` for a card and `images` when you want to pick a size.

#### Why did a URL fail with "Private addresses are not allowed"?

The SSRF guard refused it. `localhost`, `.local`, private and link-local IP ranges, and cloud metadata hostnames are blocked by default, and the check runs again after DNS resolution and after each redirect. This is intentional — it prevents a user-supplied URL from reaching internal infrastructure. Set `allowPrivateNetwork: true` only if you are deliberately previewing internal hosts.

#### What does `cacheHit` mean, and when should I use `useKvCache`?

`cacheHit: true` means the result came from the cache rather than a fresh fetch. `useKvCache: false` keeps the cache in memory for the duration of the run; setting it to `true` persists the cache to a key-value store so repeated runs reuse metadata until `cacheTtlSecs` expires.

#### Can I extract JSON-LD and Schema.org types?

Yes — `includeJsonLd` defaults to `true`. `jsonLd` holds each parsed `application/ld+json` block and `schemaTypes` gives the detected Schema.org types such as `NewsArticle` or `Product`.

#### Do I need an API key?

No. There is no third-party account or key involved.

#### What are alternatives to this actor?

Commercial APIs such as Microlink and OpenGraph.io (which add rendering and proxies), free-tier tools like `api.linkmetadata.com` (rate-limited to 20 requests per 10 seconds per IP), and open-source libraries such as `jacktuck/unfurl` — which also does not execute JavaScript. On the Apify Store the comparable Actor is `technicaldost/link-preview-api`, which returns roughly a third as many fields and costs about twice as much per preview.

### For AI Agents & LLM Apps

**Purpose:** returns one row per URL with normalized Open Graph, Twitter Card, JSON-LD, oEmbed, feed, hreflang and icon metadata, plus fetch diagnostics.

**Minimal working input** — smallest input that produces a valid run:

```json
{ "urls": ["https://apify.com"] }
```

**Batch audit variant:**

```json
{
  "urls": ["https://example.com/a", "https://example.com/b", "https://example.com/c"],
  "maxItems": 100,
  "includeJsonLd": true,
  "includeRawTags": true,
  "fetchOembed": true,
  "maxConcurrency": 5
}
```

**Output fields** — `url, finalUrl, domain, ok, status, error, contentType, responseTimeMs, attempts, truncated, htmlBytes, charset, title, description, image, images, siteName, type, canonical, favicon, appleTouchIcon, manifestUrl, feedUrl, feeds, hreflang, language, author, publisher, publishedTime, modifiedTime, robots, themeColor, twitterCard, twitterSite, twitterCreator, openGraph, twitter, schemaTypes, jsonLd, metaTags, oembed, oembedRaw, oembedEndpoint, redirectChain, isShortened, cleanUrl, excerpt, cacheHit, fetchedAt`

**Behaviors an agent should know**

- **`urls` accepts both bare strings and `{ "url": "..." }` objects.** Either form works.
- **`maxItems` defaults to `0`, which means process ALL URLs.** It only truncates when it is greater than zero. Setting `maxItems: 1` does not mean "one URL" — it means "the first one".
- **Every URL produces a row, including failures.** Check `ok` before trusting the metadata fields; on failure `error` gives the reason and `status` may be `null` when no response was received.
- **A blocked URL is not a bug.** SSRF refusals return `ok: false` with an error such as `Private addresses are not allowed`. Do not retry these.
- **No JavaScript is executed.** Client-rendered SPAs return minimal metadata. Diagnose with `htmlBytes` (very small means an empty shell) and `ok: true`.
- **`includeRawTags` defaults to `false`.** Turn it on for tag auditing; it materially increases row size and billed dataset size.
- **Use `image` for the primary preview image;** `images` is the full array with dimensions and alt text.
- `fetchOembed` (default `true`) adds a network round trip per weak-tag URL, so it slows runs on pages with no tags.
- The Standby batch endpoint `POST /v1/unfurl` accepts **at most 100 URLs per request**; `GET /v1/preview?url=` handles one.
- Set `allowPrivateNetwork: true` only when you intentionally need internal URLs — it disables the SSRF guard.
- Two dataset views exist: `overview` and `details`.

**Billing:** pay-per-event — `$0.99` per 1,000 previews on the FREE tier. Every URL processed counts, including failures and SSRF blocks.

### SEO Keywords

link preview api, open graph api, url metadata api, link unfurl, unfurl url api, open graph extractor, twitter card metadata, json-ld extractor, oembed api, extract title description image from url, bulk link preview, link preview javascript, url unfurling node, fetch favicon url, schema.org extraction api, rss feed detection api, hreflang extraction, slack unfurl bot api, discord link unfurl, link preview microservice, ssrf protection url fetcher, open graph alternative, microlink alternative, unfurl.js alternative, url metadata for ai agents

# Actor input Schema

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

URLs to preview. Accepts plain URL strings or a list of { "url": "..." } objects (batch runs only - ignored in Standby mode). Leave empty to only use the HTTP endpoints.

## `maxItems` (type: `integer`):

Maximum number of URLs to preview in this run. Set 0 to process all URLs from the list.

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

How many URLs are fetched in parallel. Higher values are faster but hammer the target sites harder.

## `requestTimeoutSecs` (type: `integer`):

Seconds to wait for a single URL before giving up.

## `maxRetries` (type: `integer`):

Extra attempts for failed requests (network errors, 429, 5xx).

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

Return parsed schema.org (JSON-LD) objects in the jsonLd field.

## `includeRawTags` (type: `boolean`):

Return every <meta> name/property and content in the metaTags field. Larger records.

## `fetchOembed` (type: `boolean`):

When the page has no title or image, query the site's oEmbed endpoint (YouTube, X, Spotify, ...) to fill the gaps.

## `cacheTtlSecs` (type: `integer`):

Seconds to cache previews of the same URL. 0 disables caching. Cached results are still charged but are returned instantly.

## `useKvCache` (type: `boolean`):

Store the cache in the key-value store so it survives across runs. Slower per request, cheaper for repeated URLs.

## `allowPrivateNetwork` (type: `boolean`):

Allow previewing http://localhost and private/internal IPs. Keep off unless you trust every URL in the input (SSRF protection).

## Actor input object example

```json
{
  "urls": [
    "https://apify.com",
    "https://example.com/blog/post?utm_source=x"
  ],
  "maxItems": 0,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 20,
  "maxRetries": 1,
  "includeJsonLd": true,
  "includeRawTags": false,
  "fetchOembed": true,
  "cacheTtlSecs": 3600,
  "useKvCache": false,
  "allowPrivateNetwork": false
}
```

# Actor output Schema

## `results` (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 = {
    "urls": [
        "https://apify.com",
        "https://example.com/blog/post?utm_source=x"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/link-preview-api").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 = { "urls": [
        "https://apify.com",
        "https://example.com/blog/post?utm_source=x",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/link-preview-api").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 '{
  "urls": [
    "https://apify.com",
    "https://example.com/blog/post?utm_source=x"
  ]
}' |
apify call ahmed_jasarevic/link-preview-api --silent --output-dataset

```

## MCP server setup

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

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/sfPUWF92pJzCXodSy/builds/bP7rNx90cGqFJanbJ/openapi.json
