# URL Metadata & Open Graph Extractor (`sfoln/url-metadata`) Actor

Extract metadata from any list of URLs: title, meta description, canonical, language, favicon, Open Graph and Twitter Card tags, JSON-LD types, feeds, H1, word and link counts. One fast HTTP fetch per URL, no browser. $1 per 1,000 URLs; failed and missing pages are free.

- **URL**: https://apify.com/sfoln/url-metadata.md
- **Developed by:** [SFOLN LLC](https://apify.com/sfoln) (community)
- **Categories:** Developer tools, SEO tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.90 / 1,000 url metadata records

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

**URL Metadata & Open Graph Extractor** turns a list of URLs into clean, structured page metadata: title, meta description, canonical URL, language, favicon, **Open Graph** and **Twitter Card** tags, **JSON-LD** schema types, publish/modify dates, author, robots and generator tags, the first H1, a visible word count, internal/external link counts and RSS/Atom feed links. Built for link previews, SEO audits, content inventories, lead enrichment and AI agents that need to know what a page is about without rendering it.

- **Price:** $1 per 1,000 URLs, charged only per analysed URL: invalid URLs, missing pages (404/410), dead domains, network failures and blocked pages are never charged.
- One fast HTTP fetch per URL (browser-like headers, up to 5 redirects followed): no headless browser, so it is cheap and quick.
- Stable, flat JSON output that works the same in the API, in integrations and in the Console.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `urls` | string\[] | – | Pages to analyse, one per line. `https://` is added when missing. Objects with a `url` key are accepted. |
| `maxUrls` | integer | 1000 | Safety cap for this run. |
| `timeoutSecs` | integer | 10 | Max time per page (3–30 s); slow pages are retried once. |
| `concurrency` | integer | 5 | Parallel fetches (1–10). |
| `includeRaw` | boolean | false | Attach low-level fetch details (detected charset) under `raw`. |

```json
{
  "urls": ["https://apify.com", "https://blog.apify.com"],
  "timeoutSecs": 10
}
```

### Output

One dataset record per URL:

```json
{
  "input": "https://apify.com",
  "url": "https://apify.com/",
  "finalUrl": "https://apify.com/",
  "redirected": false,
  "statusCode": 200,
  "contentType": "text/html; charset=utf-8",
  "title": "Apify: Full-stack web scraping and data extraction platform",
  "description": "Cloud platform for web scraping, browser automation, AI agents, and data for AI...",
  "canonical": "https://apify.com/",
  "language": "en",
  "favicon": "https://apify.com/favicon.ico",
  "ogTitle": "Apify: Full-stack web scraping and data extraction platform",
  "ogDescription": "Cloud platform for web scraping...",
  "ogImage": "https://apify.com/og-image.png",
  "ogType": "website",
  "ogSiteName": "Apify",
  "twitterCard": "summary_large_image",
  "twitterSite": "@apify",
  "jsonLdTypes": ["Organization", "WebSite"],
  "publishedTime": null,
  "modifiedTime": null,
  "author": null,
  "robots": "index, follow",
  "generator": null,
  "h1": "Build reliable web scrapers. Fast.",
  "h1Count": 1,
  "wordCount": 1432,
  "linksInternal": 118,
  "linksExternal": 21,
  "feeds": [],
  "pageBytes": 412339,
  "truncated": false,
  "responseTimeMs": 312,
  "status": "ok",
  "charged": true,
  "chargedEvent": "url-metadata",
  "source": "selfhosted",
  "fetchedAt": "2026-10-14T10:00:00.000Z"
}
```

| Field | Description |
| --- | --- |
| `finalUrl`, `redirected`, `statusCode`, `contentType` | Where the request ended up and how the server answered. |
| `title`, `description`, `canonical`, `language`, `favicon`, `keywords`, `themeColor` | Classic HTML/SEO tags. URLs are absolute. `favicon` falls back to the conventional `/favicon.ico` location when no icon link exists. |
| `ogTitle`, `ogDescription`, `ogImage`, `ogType`, `ogSiteName`, `ogUrl`, `ogLocale` | Open Graph tags. |
| `twitterCard`, `twitterTitle`, `twitterDescription`, `twitterImage`, `twitterSite`, `twitterCreator` | Twitter / X card tags. |
| `jsonLdTypes` | Distinct schema.org `@type` values in JSON-LD blocks (including `@graph`). |
| `publishedTime`, `modifiedTime`, `author` | From article/Dublin Core meta tags, falling back to JSON-LD `datePublished` / `dateModified` / `author`. Values are returned as written on the page. |
| `robots`, `xRobotsTag`, `generator` | Robots meta tag, `X-Robots-Tag` header and CMS generator tag. |
| `h1`, `h1Count` | First H1 text and the number of H1 elements. |
| `wordCount` | Estimate of visible words (scripts, styles and `<head>` excluded; CJK characters count as one word each). |
| `linksInternal`, `linksExternal` | Distinct link targets on the same host (ignoring `www.`) vs other hosts. |
| `feeds` | RSS, Atom and JSON Feed links: `{ url, type, title }`. |
| `pageBytes`, `truncated` | Bytes analysed; pages over 1 MB are cut at 1 MB and flagged. |

`status` is `ok` for every analysed page (non-HTML documents such as PDFs return `ok` with empty HTML fields and a warning), `not_found` when the page answered HTTP 404 or 410 (not charged), `error` for host names that do not resolve (`error.code` `dns_not_found`), network errors, timeouts and other HTTP errors, including non-standard statuses such as LinkedIn's 999 (not charged; a temporary DNS failure is retried once), `skipped_invalid` for malformed or non-http(s) URLs, ports other than 80/443, and URLs or redirects that lead to private/loopback addresses (`error.code` `blocked_host`, not charged) and `skipped_budget` when your cost cap was reached. Error messages quote at most ~120 characters of a target site's error page. The `OUTPUT` record in the run's key-value store contains totals.

### Pricing

Store discounts: the FREE and BRONZE tiers use the list prices below. SILVER receives 5% off; GOLD, PLATINUM and DIAMOND receive 10% off billable result events. The platform start fee is unchanged. Check the Actor's Store pricing table for the prices currently in effect.

| Event | Price | When charged |
| --- | --- | --- |
| URL metadata record (`url-metadata`) | **$0.001** per URL ($1 / 1,000) | once per URL whose page was fetched and analysed (`ok`) |

Apart from Apify's automatic Actor-start charge ($0.00005 per GB of run memory, a fraction of a cent per run) there is no start fee or subscription. Use **Maximum cost per run** to cap spend: once it is reached, the remaining URLs are returned with `status: "skipped_budget"` and are not charged. Accounts on the Apify free plan can process up to 10 URLs per run.

### Use with AI agents (MCP)

Use this Actor from Claude, ChatGPT, Cursor, VS Code or any other MCP client through the [Apify MCP server](https://mcp.apify.com). Add this server URL to your client and sign in with your Apify account when asked:

```text
https://mcp.apify.com?tools=sfoln/url-metadata
```

To run the MCP server locally instead, start `npx -y @apify/actors-mcp-server --tools sfoln/url-metadata` with your `APIFY_TOKEN` in the environment.

Example prompt:

> Get the title, description, Open Graph image and canonical URL of https://apify.com and https://blog.apify.com for a link preview.

Runs started by an agent are billed like any other run (see Pricing). Agents can cap what a run may spend with `maxTotalChargeUsd`.

### API usage

Run the Actor and get its dataset items in one call (the call waits up to 5 minutes for the run to finish):

```bash
curl -X POST "https://api.apify.com/v2/acts/sfoln~url-metadata/run-sync-get-dataset-items" \
  -H "Authorization: Bearer <APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://apify.com", "https://blog.apify.com"]}'
```

For large inputs, start the run asynchronously and download the dataset when it has finished:

```bash
## 1. Start a run: the response holds the run ID (data.id) and its dataset ID (data.defaultDatasetId)
curl -X POST "https://api.apify.com/v2/acts/sfoln~url-metadata/runs" \
  -H "Authorization: Bearer <APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://apify.com", "https://blog.apify.com"]}'
## 2. Repeat until data.status is SUCCEEDED (each call waits up to 60 s)
curl "https://api.apify.com/v2/actor-runs/<runId>?waitForFinish=60" -H "Authorization: Bearer <APIFY_TOKEN>"
## 3. Download the results (format=json, csv or xlsx)
curl "https://api.apify.com/v2/datasets/<defaultDatasetId>/items?format=json" -H "Authorization: Bearer <APIFY_TOKEN>"
```

Python ([apify-client](https://pypi.org/project/apify-client/)):

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("sfoln/url-metadata").call(run_input={"urls": ["https://apify.com", "https://blog.apify.com"]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

Instead of polling, you can attach a webhook to the run (see Integrations). The API tab of this page shows the same calls for JavaScript and the Apify CLI.

#### More API examples

Synchronous (small batches, returns the records directly):

```bash
curl -X POST "https://api.apify.com/v2/acts/sfoln~url-metadata/run-sync-get-dataset-items?token=<APIFY_TOKEN>&timeout=120" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://apify.com", "https://blog.apify.com"]}'
```

Asynchronous (large lists):

```bash
curl -X POST "https://api.apify.com/v2/acts/sfoln~url-metadata/runs?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" -d @urls.json
## → poll /v2/actor-runs/<runId> then GET /v2/datasets/<defaultDatasetId>/items?format=csv
```

Python:

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("sfoln/url-metadata").call(run_input={"urls": ["https://apify.com"]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["url"], item["title"], item["ogImage"])
```

### Integrations

- **Make, Zapier and n8n:** run `sfoln/url-metadata` with the Apify app (in n8n, the Apify node) and pass the dataset items to the next step of your workflow.
- **Google Sheets:** add the items to a sheet with the Google Sheets step of Make, Zapier or n8n, or download them as CSV or Excel from the run's Storage tab or the API (`format=csv` or `format=xlsx`).
- **Webhooks:** get a POST request when a run succeeds or fails: add a webhook in the Actor's Integrations tab, or pass `webhooks` when you start a run through the API.
- **Schedules:** run it hourly, daily or weekly with the same input from Apify Schedules; every run keeps its results in its own dataset.
- **More:** Slack, Google Drive, GitHub, LangChain, LlamaIndex and other [Apify integrations](https://docs.apify.com/platform/integrations).

### Related Actors

- [URL to Markdown & Article Extractor](https://apify.com/sfoln/url-to-markdown-summary)
- [Website to Markdown Crawler (llms.txt)](https://apify.com/sfoln/website-to-markdown)
- [AI Web Scraper – Any URL to Structured JSON](https://apify.com/sfoln/ai-web-scraper)
- [URL Safety Checker – Phishing & Malware](https://apify.com/sfoln/url-safety-check)
- [Domain WHOIS & DNS Lookup](https://apify.com/sfoln/domain-intelligence)
- [Website Change Monitor – Text Diff Alerts](https://apify.com/sfoln/website-change-monitor)
- [SEO Audit API – On-Page Checks for Any URL](https://apify.com/sfoln/onpage-seo-audit)

See [all our Actors](https://apify.com/sfoln) on Apify Store.

### FAQ

#### How much does it cost, and am I charged for failed items?

You pay per event at the prices under Pricing above, plus a run start fee of $0.00005 per GB of memory, with no subscription; platform usage is included. Errors and skipped items (`skipped_…` statuses) are never charged; a lookup that finds nothing is charged only where Pricing says so, and every record's `charged` field shows whether it was billed. Set **Maximum cost per run** to cap what a single run can spend.

#### How fresh is the data?

Each run queries its source while it runs, so you get what the source returns at that moment. Source-specific details, such as how often a provider refreshes its own database, are under Limits & notes.

#### What are the limits?

Per-run caps (for example the maximum number of items) are listed under Input, source limits under Limits & notes, and the free-plan limit under Pricing. For big batches, start the run asynchronously (see API usage) and read the dataset when it has finished.

#### Can I schedule runs or get results by webhook?

Yes. Create a schedule in Apify Console to run it hourly, daily or weekly with the same input, and add a webhook to get a POST request when a run finishes (see Integrations).

#### What does this Actor record about my runs?

This Actor records operational metrics about each run (item counts, duration, charges, error codes and a pseudonymous hash of your Apify user ID) to monitor reliability and costs. It never stores your input or results.

#### How do I get help?

Open an issue in the Actor's Issues tab and include the link to the run, so we can reproduce the problem. Feature requests are welcome there too.

### Limits & notes

- The page is fetched once with a desktop-browser User-Agent; JavaScript is **not** executed, so metadata injected client-side (some single-page apps) is not visible.
- At most 1 MB of each page is analysed; larger pages are flagged `truncated` and their word and link counts are lower bounds.
- Sites that block automated requests (HTTP 403, CAPTCHAs) return `status: "error"` and are not charged. Private, loopback, link-local and other non-public addresses are never fetched: the host of the URL and of every redirect (at most 5) is resolved and checked before it is requested, the connection goes to the address that was checked (no DNS rebinding), and only ports 80 and 443 are used.
- Data source: the target pages themselves, fetched live at run time by this Actor; nothing is cached or stored beyond your run's dataset.

# Actor input Schema

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

Pages to analyse, one per line. `https://` is added when the scheme is missing. Duplicates are removed. Objects with a `url` key are accepted too. Accounts on the Apify free plan can process up to 10 URLs per run.

## `maxUrls` (type: `integer`):

Safety cap on how many URLs are processed in this run.

## `timeoutSecs` (type: `integer`):

Maximum time to wait for one page (redirects included). Slow pages are retried once.

## `concurrency` (type: `integer`):

How many pages are fetched in parallel.

## `includeRaw` (type: `boolean`):

Attach low-level fetch details (detected charset) under `raw`.

## Actor input object example

```json
{
  "urls": [
    "https://apify.com",
    "https://blog.apify.com"
  ],
  "maxUrls": 1000,
  "timeoutSecs": 10,
  "concurrency": 5,
  "includeRaw": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (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://blog.apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sfoln/url-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 = { "urls": [
        "https://apify.com",
        "https://blog.apify.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sfoln/url-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 '{
  "urls": [
    "https://apify.com",
    "https://blog.apify.com"
  ]
}' |
apify call sfoln/url-metadata --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sfoln/url-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/oNEJQlPMPoy6j51fE/builds/hEaaHFXnDKO39lZhM/openapi.json
