# Wayback Machine Snapshots: Archived URL History & Dead Links (`yadroo/wayback-machine-snapshots`) Actor

Every archived capture of a URL from the Internet Archive Wayback Machine: capture time in UTC, HTTP status, MIME type, size, content digest, replay link and raw-file link. Also finds the copy closest to a date and lists the archived URLs of a whole site.

- **URL**: https://apify.com/yadroo/wayback-machine-snapshots.md
- **Developed by:** [Samat Makatov](https://apify.com/yadroo) (community)
- **Categories:** SEO tools, Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 archived captures

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

Look up any address in the Internet Archive's Wayback Machine capture index and get one row per stored capture:
capture time in UTC, the HTTP status the crawler saw, content type, stored size, the content hash, a link to the
archived copy and a raw link that returns the stored file itself. The same actor answers two neighbouring questions:
which single archived copy sits closest to a date you name (the one a dead footnote needs), and which addresses under
a site, path or domain are in the archive at all. No API key, no account, no proxy and no browser — it reads the
archive's public index and returns links, never the archived bytes.

> **Need it tailored to your workflow?** Missing a field, a filter, another source or a different output format? [Open an issue](https://apify.com/yadroo/wayback-machine-snapshots/issues) and describe what you need — we read every request and adapt the actor.

### Use cases

- **Repair a reference list.** Send 80 URLs in `closest` mode with the date you read them, get one citable archived
  copy per URL and a `found: false` row for the few the archive never stored.
- **Date a wording change.** Run `captures` with `changedOnly` on a pricing, policy or terms page: hundreds of
  captures collapse to the handful of moments the stored content really changed.
- **Recover files a site removed.** `siteUrls` with `mimeTypes` set to `application/pdf`, `audio/` or `image/` lists
  the archived documents, recordings or pictures under a path, each with a raw-file link.
- **Audit a migrated domain.** `siteUrls` with `matchScope: "domain"` shows which addresses — subdomains included —
  the archive holds for a host, which is the list of URLs an SEO migration has to account for.
- **Prove a page existed on a date.** One capture row with its UTC time, status and hash is evidence you can link to.
- **Check a domain's age and early look.** `captures` with `collapseBy: "year"` puts the oldest capture in the first
  row and one copy per year after it.
- **Watch your own pages.** `sinceHours` plus `onlyNew` on a schedule reports only captures stored since the last run.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `mode` | string (enum) | `captures` | Which question to answer: `captures` (one row per stored capture of each URL), `closest` (one row per URL: the capture nearest `targetDate`), `siteUrls` (one row per archived address under a site). See [Modes](#modes). |
| `urls` | string\[] | required (prefill `https://www.bbc.com/news`, `https://en.wikipedia.org/wiki/Internet_Archive`) | The addresses to look up, at most 100 per run. `https://example.com/page`, `example.com/page` and `http://example.com/page` are all accepted. A trailing slash matters for `siteUrls` with `prefix` scope. |
| `targetDate` | string | prefill `2016-03-01` | `closest` only: the moment you want a copy from. `2016`, `2016-03`, `2016-03-01` or `2016-03-01T12:00:00Z`. Empty = the newest capture. |
| `matchScope` | string (enum) | `prefix` | `siteUrls` only: `prefix` (everything under the path), `host` (that host), `domain` (host plus subdomains). See [Site scope](#site-scope). |
| `fromDate` | string | prefill `2024-01-01` | Earliest capture to return (UTC). `2024`, `2024-01` or `2024-01-01`; a year or month alone starts at its first moment. |
| `toDate` | string | prefill `2024-12-31` | Latest capture to return. Same forms; a year, month or day alone covers the whole period. Both empty = the full history back to 1996. |
| `sinceHours` | number | empty | A moving window in hours (1–8760). While it is set it replaces `fromDate`, and the run says so in its status. |
| `onlyNew` | boolean | `false` | Remember the captures this run wrote and skip them next time (keyed by archived address + capture timestamp). Made for schedules. |
| `statusCodes` | string\[] | prefill `200` | Keep only captures whose stored status is one of these. Empty = every capture. See [Status values](#status-values). |
| `mimeTypes` | string\[] | empty | Keep only these content types; a family prefix such as `audio/` keeps the whole family. Empty = every type. See [Content types](#content-types). |
| `collapseBy` | string (enum) | `day` | Thin the history inside the index: `none`, `hour`, `day`, `month`, `year`, or `digest` (drop a capture repeating the previous hash). See [Thinning](#thinning). |
| `changedOnly` | boolean | `false` | `captures` only: keep a capture only when its content hash differs from the capture before it. The first capture is always kept. |
| `newestFirst` | boolean | `false` | Ask for the end of the history instead of the beginning, so a small row cap shows the most recent captures. |
| `maxItemsPerUrl` | number | `25` | At most this many rows per URL (1–2000). Sent to the index with the query. |
| `maxItems` | number | `50` | Hard cap on the rows the run writes, and therefore on what it costs (1–2000). |
| `fields` | string\[] | empty | Keep only these columns, **in this order**. `queryUrl`, `originalUrl`, `found`, `sourceMode`, `url` and `fetchedAt` are always kept — the ones you do not list come first, and one you do list stays where you put it. A name that is not a column is reported in the run's status message. |

`mode`, `matchScope` and `collapseBy` are strict dropdowns: the platform refuses a value that is not on the list with
HTTP 400 before the run starts, and names the allowed values. `statusCodes` and `mimeTypes` are free text, so they
forgive how you type them — see their dictionaries below.

### Reference

#### Modes

| `mode` | One row is | Uses | Index requests per URL |
|---|---|---|---|
| `captures` | one stored capture of the URL you asked for | `fromDate`, `toDate`, `sinceHours`, `collapseBy`, `changedOnly`, `newestFirst`, `statusCodes`, `mimeTypes` | 1 |
| `closest` | the capture nearest `targetDate`, one per URL | `targetDate`, `statusCodes`, `mimeTypes` | 2 (1 without `targetDate`) |
| `siteUrls` | one archived address under the site | `matchScope`, `fromDate`, `toDate`, `statusCodes`, `mimeTypes` | 1 |

In `closest` mode add `statusCodes: ["200"]` when you need a copy with content — the nearest capture is otherwise
sometimes a redirect or an error page.

`maxItemsPerUrl` caps the rows one URL may contribute; `maxItems` caps the whole run. With 20 URLs and
`maxItemsPerUrl: 25` the run writes at most 500 rows — set `maxItems` to what you are willing to pay for.

#### Site scope

- `prefix` — the path you typed and everything under it: `https://www.irs.gov/pub/irs-pdf/` returns that folder.
- `host` — every archived address on exactly that host, whatever path you typed.
- `domain` — the host and all of its subdomains, which is how forgotten `old.` and `files.` hosts turn up.

#### Status values

The status is what the crawler received at capture time: `200` content, `301`/`302` a redirect, `404` a page already
gone, `403` a refusal, `503` an outage. Deduplicated revisit records carry no status: the index stores `-` for them
and this actor writes an empty `statusCode` column — add `-` to the list when you want those rows too.

Any three-digit code is accepted as typed. Words are read as well (`ok` → `200`, `not found` → `404`, `gone` → `410`,
`forbidden` → `403`, `revisit` → `-`), and case, spaces, underscores and hyphens do not matter. Anything else is
refused before the run does any work, with the closest values named: `statusCodes: ["20O"]` comes back as
`Unknown statusCodes "20O" (did you mean "200"?)`. Nothing is silently substituted.

#### Content types

Common values in the index: `text/html`, `text/plain`, `application/pdf`, `application/json`, `application/zip`,
`image/jpeg`, `image/png`, `image/gif`, `image/svg+xml`, `audio/mpeg`, `audio/x-wav`, `video/mp4`, `text/css`,
`application/javascript`, `warc/revisit` (a deduplicated record).

A value ending in `/` is a family: `audio/` keeps every audio type, `image/` every picture, `application/` every
document or data file. Short names are read too (`pdf`, `html`, `jpg`, `mp3`, `mp4`, `csv`, `zip`, `audio`, `image`,
`video`), as are case and spacing variants. A value whose type is not a real content type is refused with a
suggestion: `aplication/pdf` comes back as `Unknown mimeTypes "aplication/pdf" (did you mean "application/pdf"?)`.

#### Thinning

`collapseBy` is applied by the index before rows are counted, so it changes which captures you see, not how many rows
you pay for. `day` (the default) keeps at most one capture per calendar day — a news front page archived 40 times a
day becomes 365 rows a year instead of ~15000. `hour`, `month` and `year` work the same way at their period, `none`
returns the complete record, and `digest` drops a capture whose content hash equals the one before it.

`changedOnly` is our own comparison on top of that: it scans up to 20× your `maxItemsPerUrl` (at most 2000 captures)
of the URL's history and keeps only the captures whose hash differs from the previous one.

### Examples

**Which copies of a news page exist for 2024** — one capture per month, content only.

```json
{
  "mode": "captures",
  "urls": ["https://www.bbc.com/news"],
  "fromDate": "2024-01-01",
  "toDate": "2024-12-31",
  "collapseBy": "month",
  "statusCodes": ["200"],
  "maxItemsPerUrl": 12,
  "maxItems": 12
}
```

**An archived copy of each dead link, near the date it was read** — one row per URL, `found: false` where nothing exists.

```json
{
  "mode": "closest",
  "urls": ["https://www.bbc.com/news", "https://en.wikipedia.org/wiki/Internet_Archive", "https://www.nasa.gov/"],
  "targetDate": "2016-03-01",
  "maxItems": 5
}
```

**Archived PDF files under a path**, each with a raw-file link that returns the stored document.

```json
{
  "mode": "siteUrls",
  "urls": ["https://www.irs.gov/pub/irs-pdf/"],
  "matchScope": "prefix",
  "mimeTypes": ["application/pdf"],
  "maxItemsPerUrl": 20,
  "maxItems": 20
}
```

**When did this page change** — only captures whose stored content hash differs from the one before.

```json
{
  "mode": "captures",
  "urls": ["https://en.wikipedia.org/wiki/Internet_Archive"],
  "fromDate": "2023-01-01",
  "toDate": "2024-12-31",
  "collapseBy": "day",
  "changedOnly": true,
  "statusCodes": ["200"],
  "maxItemsPerUrl": 15,
  "maxItems": 15
}
```

**Archived addresses of a whole domain**, subdomains included.

```json
{
  "mode": "siteUrls",
  "urls": ["https://news.ycombinator.com/"],
  "matchScope": "domain",
  "statusCodes": ["200"],
  "maxItemsPerUrl": 25,
  "maxItems": 25
}
```

**New captures of a page in the last 90 days**, newest first — schedule it with `onlyNew: true`.

```json
{
  "mode": "captures",
  "urls": ["https://www.gov.uk/"],
  "sinceHours": 2160,
  "newestFirst": true,
  "collapseBy": "day",
  "maxItemsPerUrl": 15,
  "maxItems": 15
}
```

### Output

One real row from a cloud run of 06.10.2026 (`captures` mode, the 2024 window above):

```json
{
  "queryUrl": "https://www.bbc.com/news",
  "originalUrl": "https://www.bbc.com/news",
  "urlKey": "com,bbc)/news",
  "timestamp": "20240101001625",
  "capturedAt": "2024-01-01T00:16:25Z",
  "statusCode": 200,
  "mimeType": "text/html",
  "digest": "MNUDJY5ILQTZ7FVLKOYEJ3NUGOS6Q5BT",
  "lengthBytes": 50543,
  "url": "https://web.archive.org/web/20240101001625/https://www.bbc.com/news",
  "rawUrl": "https://web.archive.org/web/20240101001625id_/https://www.bbc.com/news",
  "found": true,
  "sourceMode": "captures",
  "fetchedAt": "2026-10-06T21:50:40.024Z"
}
```

| Field | Type | Meaning |
|---|---|---|
| `queryUrl` | string | The address you sent, unchanged |
| `originalUrl` | string | The address as the archive stores it (its canonical form, often with the old `:80` port) |
| `urlKey` | string | The index's sort key for that address (`com,bbc)/news`) |
| `timestamp` | string | The archive's own 14-digit capture time, `YYYYMMDDhhmmss` UTC |
| `capturedAt` | string | The same moment as ISO-8601 UTC |
| `statusCode` | number | HTTP status at capture time; **empty** for a deduplicated revisit record (the index stores `-`) |
| `mimeType` | string | Content type at capture time; `warc/revisit` on a deduplicated record |
| `digest` | string | Base-32 SHA-1 of the stored body: equal digests mean byte-identical copies |
| `lengthBytes` | number | Size of the stored record in bytes (compressed), not the page's own size |
| `url` | string | Link to the archived copy as the Wayback Machine replays it |
| `rawUrl` | string | Link to the stored bytes themselves (`id_` form) — the one to use for a PDF, image or audio file |
| `changedFromPrevious` | boolean | `captures` mode: does this capture's hash differ from the previous capture's? **Empty on the first capture** of the slice |
| `captureCount` | number | `siteUrls` mode: how many captures the index reports for that address — **usually empty**, the index returns no count today |
| `targetDate` | string | `closest` mode: the date you asked for, as you wrote it |
| `offsetDays` | number | `closest` mode: days between the capture and your date; negative = the capture is older |
| `notFoundReason` | string | Only on a `found: false` row: why there is no capture (nothing archived, or the filters matched none) |
| `found` | boolean | `true` on a capture row, `false` on a marker row for an address the archive has nothing for |
| `sourceMode` | string | Which question produced the row: `captures`, `closest` or `siteUrls` |
| `fetchedAt` | string | When this run read the index, ISO-8601 UTC |

Columns a mode cannot fill are left out of its rows instead of being written empty, so a `captures` row has no
`offsetDays` and a `closest` row no `changedFromPrevious`.

Dataset views: **Archived captures** (the wide table), **History of one page** (timeline of one address with hashes
and the changed flag), **Closest copy per URL** (one line per URL with the distance from your date).

### Use it from code / agents

```bash
curl -X POST "https://api.apify.com/v2/acts/yadroo~wayback-machine-snapshots/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"closest","urls":["https://www.bbc.com/news"],"targetDate":"2016-03-01","maxItems":5}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/wayback-machine-snapshots').call({
    mode: 'captures',
    urls: ['https://en.wikipedia.org/wiki/Internet_Archive'],
    fromDate: '2024-01-01',
    toDate: '2024-12-31',
    maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/wayback-machine-snapshots").call(run_input={
    "mode": "siteUrls",
    "urls": ["https://www.irs.gov/pub/irs-pdf/"],
    "mimeTypes": ["application/pdf"],
    "maxItems": 20,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

The run's key-value store holds a `SUMMARY` record with the mode, how many URLs were looked up, how many index
requests it took, how many URLs came back as not found, how many rows were skipped as already delivered, and whether
the run stopped at your cost cap or before the run timeout.

MCP: add `https://mcp.apify.com` to Claude / Cursor / any MCP client and call the `yadroo/wayback-machine-snapshots`
tool with the same JSON input. For "find me a readable copy of this dead link", send
`{"mode":"closest","urls":["<the dead URL>"],"targetDate":"<when you saw it>","maxItems":1}` and read `url` (the
replay page) or `rawUrl` (the stored file) from the single row; `found: false` means the archive has nothing and
`notFoundReason` says why.

### Pricing

Pay per event: **$0.001 per run start + $0.001 per dataset row**. Every run pays the start event, including runs that
write no rows, and every row written to the dataset is charged — a `found: false` marker row for an address the
archive does not hold is a row and is billed, because it is the answer you asked for.

- The default input (2 URLs, one capture per day of 2024, `maxItems: 50`) → 50 rows: 0.001 + 50 × 0.001 = **$0.051**.
- A reference list of 80 URLs in `closest` mode → 80 rows: 0.001 + 80 × 0.001 = **$0.081**.
- One archived PDF folder, `maxItems: 20` → 20 rows: 0.001 + 20 × 0.001 = **$0.021**.

`maxItems` is the cost switch: the run stops as soon as it is reached. The run also respects the **Maximum cost per
run** you set on the platform — when that cap is reached the run stops, keeps and delivers the rows it already paid
for, and ends with `Stopped at your spending limit: N rows delivered`.

### Limits & FAQ

- **Measured speed (06.10.2026, Apify cloud, 256 MB).** The actor keeps **one request per second** to
  `web.archive.org` through a single token bucket, retries included, and the index itself answers in 1–5 s.
  - `captures` and `siteUrls` need **one index request per URL**: the default input (2 URLs, 50 rows) finished in
    **10–20 s**, a 25-row whole-domain listing of `news.ycombinator.com` in **32 s**. Roughly 25–40 URLs a minute, so
    the 100-URL maximum fits the default 900 s run timeout comfortably.
  - `closest` needs **two requests per URL** (the last capture before your date and the first one after it) and the
    second one is slow on a heavily archived page. A 30-URL run took **59 index requests in 268 s** — about 9 s per
    URL. Keep `closest` batches at **60 URLs or fewer** per run, or raise the run timeout; a batch that does not fit
    stops early, keeps every row it has read and says so.
- **The archive's rate limit is not published as a number.** The Internet Archive says it has put protections in
  place against high-volume automated traffic and answers `429` when a client is over the line. 59 requests in a row
  at one per second were served without a single `429` or block on 06.10.2026, which is the pace this actor keeps.
  It honours `Retry-After` and retries a `429`, a `5xx` or the archive's "Temporarily Offline" page with growing
  waits; when the archive keeps refusing three URLs in a row the run stops and says so instead of hammering it —
  start it again in a few minutes. A larger run simply takes longer; it is never faster than one request per second.
- **A wide scope plus a filter can be more than one index query answers.** `siteUrls` with `host` or `domain` scope
  over a large site, combined with a content-type or status filter, makes the index scan millions of entries and it
  can answer with a gateway timeout. That URL comes back as a failed lookup naming the fix: narrow the scope to
  `prefix`, or add a `fromDate`/`toDate` window.
- **A run can stop before its timeout.** Near the run timeout, and at your maximum cost per run, the actor stops
  starting new work, keeps every row it has read and ends successfully with a status that says why (`Stopped before
  the run timeout: N rows saved`, `Stopped at your spending limit: N rows delivered`). Rows are written as each URL
  finishes, so an interrupted run never loses what it read. Verified in the cloud: a 40-URL `closest` run with a
  60 s run timeout ended SUCCEEDED with its rows and that status.
- **Coverage is what the archive's crawlers saw.** A small or regional site may have a handful of captures a year, or
  none; pages the archive excludes, or pages that were blocked when the crawler came, are missing. A capture can also
  be a redirect, a cookie wall or an error page — read `statusCode` and `lengthBytes` before quoting it.
- **Some addresses are refused by the index itself.** For a few addresses the index answers `403` (excluded or
  withheld) or `404`. That URL gets a row with `found: false` and the reason instead of disappearing from the
  dataset, so a reference list always comes back complete. It is a billed row like any other.
- **`digest` is a byte hash, not an editorial diff.** A rotated advert or a changed banner changes the hash, so treat
  `changedFromPrevious` as "worth looking at", not as proof of an edit.
- **One index query answers at most about 10000 rows.** A whole-domain export is therefore done in slices: by year
  with `fromDate`/`toDate`, or by path with `prefix` scope.
- **`captureCount` is usually empty.** The index accepts the request for a per-address capture count but returns no
  value for it today; we ask for it anyway and leave the column empty rather than spend an extra request per address.
- **It never writes to the archive.** Saving a new capture ("Save Page Now") needs an archive account and is out of
  scope, as are downloading, rendering or re-publishing archived content: every row is a link plus metadata.
- **Empty results vs errors.** A URL with no matching capture always produces a row with `found: false` and a reason,
  so a list of 80 references can be checked in one pass; an unreadable input (a date we cannot parse, a filter value
  we cannot match, more than 100 URLs) fails the run before any request, naming the one thing to change.

Questions or problems: open an issue on the Issues tab of this actor.

***

Made by **Yadroo**. Sibling actors: [domain-intel](https://apify.com/yadroo/domain-intel),
[rss-to-json](https://apify.com/yadroo/rss-to-json), [wikipedia-search](https://apify.com/yadroo/wikipedia-search),
[github-repo-intel](https://apify.com/yadroo/github-repo-intel),
[data-gov-datasets](https://apify.com/yadroo/data-gov-datasets).

# Actor input Schema

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

Three different questions, one per run. `captures` answers "when was this page archived and did it change" - one row per stored capture. `closest` answers "give me one working archived copy near this date", which is what repairing a dead link or a footnote needs: one row per URL, including a row marked `found: false` for URLs the archive never stored. `siteUrls` answers "which addresses under this site are in the archive at all" - one row per archived URL.

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

The addresses to look up, at most 100 per run. Write them as people write them: `https://example.com/page`, `example.com/page` and `http://example.com/page` are all accepted, because the archive indexes a canonical form of the address and the scheme rarely matters. A trailing slash does matter for `siteUrls` with `prefix` scope - `https://www.irs.gov/pub/irs-pdf/` means everything under that folder. More than 100 URLs is refused with a clear message instead of a half-finished run.

## `targetDate` (type: `string`):

Used by `closest`. The moment you want a copy from: `2016`, `2016-03`, `2016-03-01` or a full `2016-03-01T12:00:00Z` are all read. Leave it empty to get the newest capture the archive holds. The answer is the nearest capture in either direction, and `offsetDays` says how far it landed from your date - a page archived twice in ten years can answer a 2016 question with a 2011 capture, so always read that column before quoting the copy.

## `matchScope` (type: `string`):

Used by `siteUrls`. `prefix` keeps the path you typed and walks down from it, `host` ignores the path and returns the whole host, `domain` adds every subdomain, which is how you find the forgotten `old.` and `files.` hosts of an organisation. A big site holds millions of archived addresses, so a wide scope needs `maxItemsPerUrl` and the MIME or status filters to stay useful.

## `fromDate` (type: `string`):

Earliest capture to return, as `2024`, `2024-01` or `2024-01-01` (UTC). The Wayback Machine's first captures are from 1996, so leaving this empty really does mean three decades of history for an old site.

## `toDate` (type: `string`):

Latest capture to return, same forms as above. A date alone covers the whole day. Leave both dates empty for the full history.

## `sinceHours` (type: `number`):

A moving window for watching a page: `24` for the last day, `168` for the last week. It replaces `Captures from` while it is set, so a scheduled run always asks for the fresh part of the history instead of re-reading everything. Nothing in the window is a normal answer and ends the run with a status that says so - the archive crawls a small site a few times a year, not daily.

## `onlyNew` (type: `boolean`):

Remember which captures this run wrote in the actor's key-value store and write only unseen ones next time, keyed by URL and capture timestamp. Made for a schedule: with `sinceHours` it turns the actor into a watcher that reports what the archive stored since the last run. The memory belongs to the actor or task that runs, so two tasks watching two sites never shadow each other.

## `statusCodes` (type: `array`):

Status of the page at the moment it was archived: `200` for the ones that really hold content, `301` or `302` for redirects, `404` for pages that were already gone when the crawler came. Several values are combined with OR; leave the list empty to keep every capture. The archive stores a dash instead of a status for deduplicated revisit records, so add `-` when you want those too - they appear as an empty `statusCode` column.

## `mimeTypes` (type: `array`):

Filter by what was archived: `text/html` for pages, `application/pdf` for documents, `image/jpeg`, `audio/mpeg`, `video/mp4`. A family prefix also works - `audio/` keeps every audio type - which is how you pull a set of lost recordings or every PDF a site once published. Empty means every type.

## `collapseBy` (type: `string`):

A busy news front page is archived dozens of times a day, so the default keeps one capture per day and a year of history fits in 365 rows instead of thousands. `month` or `year` gives a quick long-range picture, `none` the complete record, `digest` drops a capture whose content hash equals the previous one. Thinning happens inside the archive's index before the row cap is applied, so it changes which captures you see, not how many rows you pay for.

## `changedOnly` (type: `boolean`):

Compare the content hash of each capture with the previous capture of the same URL and keep only the ones that differ, so a page archived 400 times with 12 real edits gives 12 rows. The first capture of a URL is always kept as the starting point and its `changedFromPrevious` column stays empty. A changed hash means the stored bytes differ - a new advert or a rotated banner counts as a change, so treat the column as "worth looking at", not as an editorial diff.

## `newestFirst` (type: `boolean`):

The archive's index is ordered oldest first, which is what you want for "when did this page appear" questions. Switch this on and the run asks for the end of the history instead, so a small row cap shows the most recent captures - the usual choice for monitoring and for checking whether a live page is archived at all.

## `maxItemsPerUrl` (type: `number`):

Keeps one heavily archived address in a list of twenty from eating the whole run. The cap is sent to the archive with the query, so a thin URL costs one request either way.

## `maxItems` (type: `number`):

Hard cap on the rows this run writes, and therefore on what it costs. The run stops as soon as it is reached and says so in its status message. The archive's index hands out at most about 10000 rows for one query, so a whole-domain export is done in slices - by year with `Captures from` / `Captures up to`, or by path.

## `fields` (type: `array`):

Keep only the named columns, in the order given - for example `originalUrl, capturedAt, rawUrl`. Empty = every column. `queryUrl`, `originalUrl`, `found`, `sourceMode`, `url` and `fetchedAt` are always present; name one of them here and it moves to the position you gave it. Unknown names are reported in the status message instead of being dropped in silence.

## Actor input object example

```json
{
  "mode": "captures",
  "urls": [
    "https://www.bbc.com/news",
    "https://en.wikipedia.org/wiki/Internet_Archive"
  ],
  "targetDate": "2016-03-01",
  "matchScope": "prefix",
  "fromDate": "2024-01-01",
  "toDate": "2024-12-31",
  "onlyNew": false,
  "statusCodes": [
    "200"
  ],
  "collapseBy": "day",
  "changedOnly": false,
  "newestFirst": false,
  "maxItemsPerUrl": 25,
  "maxItems": 50
}
```

# 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://www.bbc.com/news",
        "https://en.wikipedia.org/wiki/Internet_Archive"
    ],
    "targetDate": "2016-03-01",
    "fromDate": "2024-01-01",
    "toDate": "2024-12-31",
    "statusCodes": [
        "200"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/wayback-machine-snapshots").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://www.bbc.com/news",
        "https://en.wikipedia.org/wiki/Internet_Archive",
    ],
    "targetDate": "2016-03-01",
    "fromDate": "2024-01-01",
    "toDate": "2024-12-31",
    "statusCodes": ["200"],
}

# Run the Actor and wait for it to finish
run = client.actor("yadroo/wayback-machine-snapshots").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://www.bbc.com/news",
    "https://en.wikipedia.org/wiki/Internet_Archive"
  ],
  "targetDate": "2016-03-01",
  "fromDate": "2024-01-01",
  "toDate": "2024-12-31",
  "statusCodes": [
    "200"
  ]
}' |
apify call yadroo/wayback-machine-snapshots --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yadroo/wayback-machine-snapshots"
        }
    }
}
```

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/hEY2vK1FHvEoeTgPS/builds/N8pB3EXmuM5MNZCk6/openapi.json
