> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keenable.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Keenable search and fetch work without an API key. To add them to code, use the official SDK for the project's language: https://docs.keenable.ai/sdks.md
> Read an API key from the KEENABLE_API_KEY environment variable. Never ask a user to paste a key into chat; send them to https://docs.keenable.ai/api-keys.md
> On 402 with an API key (code insufficient_credits), stop and ask a human to add credits; don't retry. On the pay-per-request paths a 402 is the payment offer: pay it and retry, see https://docs.keenable.ai/pay-per-request.md. On 429, wait the number of seconds in Retry-After, then retry.
> Every error, with its cause and fix: https://docs.keenable.ai/errors.md

# Time Machine

> Search and read the web as it was at a past moment (experimental)

> [!WARNING]
>
> The Time Machine API is experimental and can change before general availability.

*Examples and coverage verified against the production API on 2026-10-26.*

Search and read the web as Keenable captured it at a past moment, given by `query_time`. Every result is a version of the page captured at or before `query_time`, so nothing published or edited later can appear. Use it to backtest agents and forecasts, to research what was known on a given date, or to build training datasets that can't see the future.

| Endpoint | What it does |
| - | - |
| `GET` or `POST https://api.keenable.ai/v1/time_machine/search` | Searches the web as of `query_time` |
| `GET https://api.keenable.ai/v1/time_machine/fetch` | Reads one page as it was at `query_time` |

## Access

* Send your API key in the `X-API-Key` header.
* Time Machine must be turned on for your organization. If it isn't, every call returns `403` with `Feature not enabled`. To turn it on, go to [keenable.ai/time-machine](https://keenable.ai/time-machine) or ask your Keenable contact.
* Each search uses credits like a regular search, and each fetch like a regular fetch. If your organization has no credits left, calls return `402` with `Insufficient credits`.
* Most responses carry an `X-Request-Id` header. Include its value when you [report a problem](https://docs.keenable.ai/errors.md#report-a-problem).

## Quick start

Set your API key in the environment, then run the examples below.

```bash theme={"system"}
export KEENABLE_API_KEY=...   # your API key
```

**Search**: what was known about the WeWork IPO on 1 September 2019, a month before it was withdrawn.

```bash theme={"system"}
curl -s --get "https://api.keenable.ai/v1/time_machine/search" \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  --data-urlencode "query=WeWork IPO" \
  --data-urlencode "query_time=2019-09-01" \
  --data-urlencode "max_results=3" \
  --data-urlencode "snippet_max_length=200"
```

```json theme={"system"}
{
  "query": "WeWork IPO",
  "query_time": "2019-09-01T00:00:00.000Z",
  "results": [
    {
      "title": "WeWork Takes Key Step Toward I.P.O., Citing Heady Growth and Huge Losses",
      "url": "https://nytimes.com/2019/08/14/business/dealbook/wework-ipo.html",
      "snippet": "WeWork would be the second-largest “unicorn” — a privately held start-up valued at $1 billion or more — to go public this year after Uber.\n\nThe offering is being led by JPMorgan and Goldman Sachs.",
      "published_at": "2019-08-14T00:00:00Z",
      "acquired_at": "2019-08-20T03:49:44Z"
    },
    {
      "title": "WeWork files for IPO amid investor skepticism over deep losses",
      "url": "https://theguardian.com/technology/2019/aug/14/wework-files-for-ipo-amid-investor-skepticism-over-deep-losses",
      "snippet": "sleepy world of office rentals by adding a hipster aesthetic and beer taps to office life, became the latest Silicon Valley “unicorn” to file for an initial public offering (IPO) on Wednesday,",
      "published_at": "2019-08-14T13:10:38Z",
      "acquired_at": "2019-08-21T14:33:06Z"
    }
  ]
}
```

*(The response contains 3 results; only 2 are shown here.)*

**Search with filters**, sent as a `POST` with a JSON body:

```bash theme={"system"}
curl -s -X POST "https://api.keenable.ai/v1/time_machine/search" \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "Uber IPO", "query_time": "2019-06-01T00:00:00Z", "site": "techcrunch.com",
       "published_after": "2019-04-01", "max_results": 3, "snippet_max_length": 200}'
```

The response has the same shape as the `GET` search above.

**Fetch**: one page as it was on 1 September 2019.

```bash theme={"system"}
curl -s --get "https://api.keenable.ai/v1/time_machine/fetch" \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  --data-urlencode "url=https://www.businessinsider.com/wework-ipo-we-company-history-founder-story-timeline-adam-neumann-2019-8" \
  --data-urlencode "query_time=2019-09-01" \
  --data-urlencode "max_chars=300"
```

```json theme={"system"}
{
  "url": "https://businessinsider.com/wework-ipo-we-company-history-founder-story-timeline-adam-neumann-2019-8",
  "query_time": "2019-09-01T00:00:00.000Z",
  "acquired_at": "2019-08-23T12:47:36Z",
  "published_at": "2019-08-14T00:00:00Z",
  "title": "WeWork history, from Manhattan building to $47 billion valuation, IPO",
  "content": "WeWork’s $47 billion parent company, The We Company,  publicly filed its IPO paperwork on Wednesday. WeWork started as one office space in the SoHo neighborhood of New York City. Now it has nearly 800 locations open or coming soon in 124 cities around the world. The We Company is composed of WeWork\n\n..._This content has been truncated to stay below 300 characters_..."
}
```

Other `query_time` values return other versions of the same URL:

| `query_time` | Result |
| - | - |
| `2019-06-01` | `404`: no version had been captured yet |
| `2019-09-01` | The version captured on 2019-08-23, shown above |
| `2019-11-01` | The version captured on 2019-09-20, titled *"WeWork history, from single Manhattan building to global company, IPO"* |
| `2026-10-01` | The version captured on 2026-09-16, titled *"WeWork history, from single Manhattan building to pushing out its CEO"* |

## `query_time`

Both endpoints require `query_time`. The response echoes it in UTC ISO 8601 form, for example `2019-09-01T00:00:00.000Z`.

| Form | Example | Meaning |
| - | - | - |
| ISO 8601 with a time zone | `2019-09-01T12:00:00Z`, `2019-09-01T08:00:00-04:00` | That instant |
| ISO 8601 without a time zone | `2019-09-01T12:00` | That instant, in UTC |
| Date | `2019-09-01` | Midnight UTC at the start of that day |
| Unix epoch | `1567296000` (seconds) or `1567296000000` (milliseconds) | That instant |
| Relative | `30d` | 30 days before now. Units: `min`, `h`, `d`, `w`, `mo` (30 days), `y` (365 days) |

* `query_time` must not be in the future, and `now` is not accepted. For current results, use the regular [search API](https://docs.keenable.ai/api-reference/search.md).
* In a JSON body, `query_time` must be a string, so quote epochs: `"1567296000"`.
* In a `GET` query string, encode the `+` of a time zone offset as `%2B`. curl's `--data-urlencode` does this for you.

## Search

`GET` or `POST /v1/time_machine/search`. `GET` takes the parameters in the query string. `POST` takes the same parameters as a JSON body.

### Request

**query** `string` *required*

What to search for, in words. A URL is rejected; to read a URL, use [fetch](#fetch).

**query\_time** `string` *required*

See [`query_time`](#query_time).

**site** `string`

Only results from this domain, e.g. `techcrunch.com`. A leading `www.` is optional.

**published\_after** `string`

Only pages published after this date. Accepts a date, an ISO 8601 timestamp, a Unix epoch, or a relative offset counted back from `query_time` (`7d` means 7 days before `query_time`).

**published\_before** `string`

Only pages published before this date. Same formats as `published_after`.

**max\_results** `integer` (default: 10)

How many results to return, from `1` to `50`.

**snippet\_max\_length** `integer`

Maximum snippet length in characters, 180–10,000. Without it, snippets are typically 500–3,500 characters.

**session\_id** `string`

Your own ID for grouping related requests. You can send it as the `X-Keenable-Session-Id` header instead.

Any other parameter returns `400` with `Unknown parameter(s)` and the list of supported parameters. The `site:` operator inside `query` also works. The `acquired_after:` and `acquired_before:` operators are rejected. The `published_after:` and `published_before:` operators inside `query` take only absolute dates. A relative one, such as `published_after:7d`, is rejected, because there it would count back from now; use the `published_after` parameter, which counts back from `query_time`.

### Response

**query** `string`

Your query.

**query\_time** `string`

The `query_time` used, in UTC ISO 8601.

**results** `array`

Results, most relevant first.

**result**

**title** `string`

The page's title, from the captured version.

**url** `string`

The page's URL, normalized (for example, without `www.`).

**snippet** `string`

A relevant passage from the captured version.

**published\_at** `string`

The page's publication date in UTC ISO 8601. Absent when unknown.

**acquired\_at** `string`

When Keenable captured the version this result shows, in UTC ISO 8601. Always at or before `query_time`.

**warning** `string`

Present only when the API adjusted your query. For example, a query made only of `site:` operators is searched with the site's name as the search term.

**usage** `object`

What this call was billed: `sku` (`search.time_machine`), `amount` (1), `credits` (credits charged) and `paid` (`true` when the credits came from your purchased balance, `false` when they came from your plan's free credits).

## Fetch

`GET /v1/time_machine/fetch`, `GET` only. Returns the latest version of the page that Keenable stored at or before `query_time`. It never fetches the live page. Keenable doesn't have every older version of every page.

### Request

**url** `string` *required*

The page's URL. `www.` and non-`www.` forms return the same page, but `http://` and `https://` are different URLs.

**query\_time** `string` *required*

See [`query_time`](#query_time).

**max\_chars** `integer` (default: 50000)

Maximum content length, as a positive integer. Longer content is cut to `max_chars` characters, and a truncation note of about 70 characters is appended.

**session\_id** `string`

Your own ID for grouping related requests. You can send it as the `X-Keenable-Session-Id` header instead.

### Response

**url** `string`

The page's URL, normalized.

**query\_time** `string`

The `query_time` used, in UTC ISO 8601.

**acquired\_at** `string`

When Keenable captured this version, in UTC ISO 8601.

**published\_at** `string`

The page's publication date in this version, in UTC ISO 8601. Absent when unknown.

**title** `string`

The page's title in this version. Absent when unknown.

**content** `string`

The page's text in this version.

**usage** `object`

What this call was billed: `sku` (`fetch.time_machine`), `amount` (1), `credits` and `paid`, as in search.

If no version was captured at or before `query_time`, fetch returns `404` and the response suggests trying a later `query_time`.

## Errors

Errors are JSON: `{"error": "<title>", "message": "<details>"}`. `message` is sometimes absent. A call that returns an error is not billed, and its response carries no `usage`.

| Status | `error` | When |
| - | - | - |
| 400 | `Missing required parameter` | `query`, `url` or `query_time` is missing, or `query_time` is `now`. |
| 400 | `Invalid parameter` | `query_time` is malformed, in the future, or not a string; a value is out of range; or `query` contains a rejected operator. |
| 400 | `Bad request` | An unknown parameter, a URL as a search query, an unparseable fetch URL, or a malformed `published_after` or `published_before`. |
| 401 | `Missing authentication` or `Authentication failed` | The API key is missing or invalid. |
| 402 | `Insufficient credits` | Your organization has no credits left. |
| 403 | `Feature not enabled` | Time Machine is not enabled for your organization. |
| 403 | `Upstream forbidden` | Fetch: the stored version is a bot-check page, not the page itself. |
| 404 | `Not found` | Fetch: no version was captured at or before `query_time`. Also returned for `POST` to fetch. |
| 422 | `Unprocessable entity` | Fetch: the stored version has no usable text, for example an empty page or a login wall. |
| 429 | `Rate limit exceeded` | Your organization's requests-per-second limit was exceeded. |
| 5xx | Varies | A transient failure. Retry with backoff. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.