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

# Look up an item

> One public post, video, short, image, carousel, or Meta Ad Library ad, returned inside the request.

`GET /v1/items/lookup` answers with the item itself, not a job to poll. It is the light path next to [import](/guides/import-a-url): metadata and captions only, no media bytes, no reference created in your library.

<CodeGroup>
  ```bash curl theme={null}
  curl -G https://api.refmatter.com/v1/items/lookup \
    -H "Authorization: Bearer $REFMATTER_API_KEY" \
    --data-urlencode "url=https://www.tiktok.com/@creator/video/7437269251355921707" \
    --data-urlencode "include=transcript" \
    --data-urlencode "maxAge=1h"
  ```

  ```ts TypeScript theme={null}
  const item = await refmatter.items.lookup(
    'https://www.tiktok.com/@creator/video/7437269251355921707',
    { include: 'transcript', maxAge: '1h' },
  );
  ```
</CodeGroup>

| Parameter | Required | Values                                                                    |
| --------- | -------- | ------------------------------------------------------------------------- |
| `url`     | yes      | One absolute `http` or `https` link to a single post, video, short, or ad |
| `include` | no       | `transcript`                                                              |
| `maxAge`  | no       | `0`, or whole minutes, hours, or days up to `365d`. Default `1h`          |

URL-encode the `url` value, as `--data-urlencode` does. An unencoded `&` in the link ends the value there, and the rest, such as `&t=10s`, arrives as a parameter of its own. Parameter names are case-sensitive. An unknown one, such as `t` or `maxage`, is refused with `invalid_input`, `details.field` naming it and `details.reason` set to `unknown_parameter`.

A lookup takes 1–6 seconds live, under 0.2 seconds from the store, and never more than 20 seconds. YouTube is read from its watch page in about a second; on the rare page that cannot be read, a slower engine answers instead (about 12 seconds). `include=transcript` on an item with captions usually refetches them live even within `maxAge` (see [Transcripts](#transcripts)), so it takes live time and can come close to the 20-second limit. Call [`GET /v1/resolve`](/guides/resolve-a-url) first if you want to know for free whether a link is supported.

## The item

```json theme={null}
{
  "object": "item",
  "id": "tiktok:7437269251355921707",
  "platform": "tiktok",
  "kind": "video",
  "url": "https://www.tiktok.com/@creator/video/7437269251355921707",
  "author": {
    "object": "account",
    "id": "tiktok:6812345678901234567",
    "platform": "tiktok",
    "handle": "creator",
    "name": "Display Name",
    "url": "https://www.tiktok.com/@creator"
  },
  "title": null,
  "text": "caption text #tag",
  "publishedAt": "2026-09-01T10:00:00.000Z",
  "durationMs": 15000,
  "stats": {
    "views": 120000,
    "likes": 5300,
    "comments": 210,
    "shares": 95,
    "saves": 400
  },
  "thumbnails": [
    {
      "url": "https://…",
      "width": 720,
      "height": 1280,
      "expiresAt": "2026-09-18T10:00:00.000Z"
    }
  ],
  "transcript": null,
  "unavailableFields": ["title", "transcript"],
  "ad": null,
  "tiktok": {
    "authorSecUid": "MS4w…",
    "authorVerified": false,
    "reposts": 12,
    "hashtags": ["tag"],
    "music": {
      "id": "7000000000000000003",
      "title": "original sound",
      "authorName": "creator",
      "original": true
    },
    "isAd": false,
    "aiGenerated": false,
    "width": 720,
    "height": 1280,
    "originalLanguage": "en-US",
    "captionLanguages": ["en-US"]
  },
  "instagram": null,
  "youtube": null,
  "observedAt": "2026-09-17T12:00:03.120Z",
  "cached": false,
  "usage": { "creditsCharged": 1, "lines": [{ "part": "fetch", "credits": 1 }] }
}
```

Two discriminators tell you what to read:

* `platform` names the one platform block that is filled. For a `tiktok` item, `tiktok` is an object and `instagram` and `youtube` are `null`. For `meta_ad_library` all three are `null`.
* `ad` is filled if and only if `kind` is `ad`.

`kind` is `video`, `short`, `image`, `carousel`, or `ad`.

## Core and blocks

A field is in the common core only when at least two platforms publish it with the same meaning. Everything else lives in the platform block, so nothing is squeezed into a field it does not fit.

| Block       | What it adds                                                                                                                                                            |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tiktok`    | `authorSecUid`, `authorVerified`, `reposts`, `hashtags`, `music`, `isAd`, `aiGenerated`, `width`, `height`, `originalLanguage`, `captionLanguages`                      |
| `instagram` | `mediaId`, `productType`, `carouselCount`, `likesHidden`, `hasAudio`, `captionEdited`, `authorVerified`, `width`, `height`, `accessibilityCaption`                      |
| `youtube`   | `availability`, `liveState`, `captionTracks`, `categories`, `tags`, `channelFollowerCount`, `commentCountApproximate`                                                   |
| `ad`        | `archiveId`, `deeplinkId`, `pageId`, `pageName`, `byline`, `active`, `startedAt`, `endedAt`, `platforms`, `displayFormat`, `collationId`, `collationCount`, `creatives` |

An Ad Library ad reports `publishedAt: null` and every `stats` value `null`: the library publishes a run window, not a publication date or engagement. The run start is `ad.startedAt`, and `ad.deeplinkId` is the id carried by the URL you sent, so you can join the link you had to the item you got.

## Unknown values

An unknown value is `null`, never `0` and never invented. `unavailableFields` lists, in a fixed order, every core field whose value in this response is unknown:

```json theme={null}
"stats": { "views": null, "likes": 12, "comments": 0, "shares": null, "saves": null },
"unavailableFields": ["stats.views", "stats.shares", "stats.saves"]
```

`stats.comments` is `0` because the source reported zero comments, so it is not listed. `transcript` is listed only when you asked for one and there is none.

## Freshness and maxAge

Refmatter keeps a canonical record of every item it has seen, shared by everyone who asks for it. When it already holds an observation of the item confirmed no longer ago than `maxAge`, the lookup answers from that record without touching the platform, and `observedAt` is the time the observation was last confirmed.

| `maxAge` | Behavior                                                     |
| -------- | ------------------------------------------------------------ |
| `0`      | Always fetches live                                          |
| `30m`    | Serves a stored observation confirmed in the last 30 minutes |
| `1h`     | The default                                                  |
| `7d`     | Up to `365d` is accepted                                     |

An import writes the same observation a lookup does, so a URL your workspace imported five minutes ago is served from the store. A live fetch that finds the item unchanged renews the confirmation time without rewriting history; observations are immutable, and a changed count records a new one.

Thumbnail URLs on TikTok, Instagram, and Meta are signed and expire, so they are never stored. A store hit for those platforms returns `thumbnails: []` and lists `thumbnails` in `unavailableFields`. YouTube thumbnail URLs are stable and are kept. When you need a thumbnail you can rely on, ask with `maxAge=0`.

## Credits

One credit buys one item for your workspace for the length of your `maxAge`. `usage.lines` lists only charged parts, and `usage.creditsCharged` is their sum.

At a zero balance a lookup that would charge answers `402 insufficient_credits` before the platform is contacted; a repeat within your `maxAge` is free and keeps answering from the store.

| Situation                                                                              | Credits | `cached` |
| -------------------------------------------------------------------------------------- | ------- | -------- |
| First lookup of an item by your workspace, live or from the store                      | 1       | `false`  |
| Repeat within `maxAge` of your last charge for that item, served from the store        | 0       | `true`   |
| Repeat within `maxAge` of your last charge that needs a live fetch (transcript, stale) | 0       | `false`  |
| `maxAge=0`: a fresh read every time                                                    | 1       | `false`  |
| Every error: 400, 401, 403, 404, 410, 422, 429, 500, 502, 503, 504                     | 0       | —        |
| A fetch that succeeded but could not be written                                        | 0       | `false`  |

Consequences worth knowing:

* The window is yours. Once your workspace paid for an item, nothing another workspace or an import does to it costs you again within your `maxAge`: if someone refreshed the item, you are served the newer counts free.
* The window is measured from your last charge for the item, with the `maxAge` of the request you are making. A request with a shorter `maxAge` than the time since that charge pays again.
* Five concurrent repeats of the same stored item charge at most once.
* A lookup does not store transcripts, so `include=transcript` on an item with captions reads the platform again each time. When that read is free for you, it runs at most once a minute per item: a sooner repeat answers `429 rate_limited` with `retryAfterSeconds`. A read you pay for is never refused this way.

## Transcripts

`include=transcript` returns timed text built from the platform's own captions. There is no speech recognition, and a transcript is never charged on its own: one that turns out to be empty adds nothing to the price of the item.

```json theme={null}
"transcript": {
  "language": "en",
  "source": "captions",
  "segments": [{ "startMs": 1200, "endMs": 3360, "text": "All right, so here we are" }],
  "text": "All right, so here we are …"
}
```

`source` is `captions` for a human-authored track or `automatic_captions` for the platform's speech recognition. Instagram and Meta Ad Library publish no captions, so they always answer `transcript: null`. When the stored facts say the item has no caption track, the store answers `null` without a fetch. When the item has captions and none is stored, the lookup refetches them live, free for a workspace that already paid for that observation.

A lookup does not keep the transcript. Use [import](/guides/transcripts) when you want a transcript stored with a reference.

## Headers

| Header                        | On                                                  | Value                                     |
| ----------------------------- | --------------------------------------------------- | ----------------------------------------- |
| `Refmatter-Credits-Charged`   | every response, errors and access refusals included | Integer, `0` whenever nothing was charged |
| `Refmatter-Credits-Remaining` | every response that named a workspace               | The workspace balance after this response |
| `Server-Timing`               | successful responses only                           | `fetch;dur=…, persist;dur=…, total;dur=…` |
| `Retry-After`                 | `429`                                               | Whole seconds                             |
| `Cache-Control`               | successful responses                                | `no-store`                                |

`Server-Timing` omits a phase that did not run, so a store hit carries `total` alone. A fetch whose result could not be written carries `persist;dur=…;desc="failed"`, and the item comes back with `creditsCharged: 0`.

## Rate limits

Every platform request waits for source capacity first. A lookup waits up to 1.5 seconds for it; if none frees up it answers `429 rate_limited` without contacting the platform, free, with an honest wait:

```json theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "The operation is temporarily rate limited.",
    "retryable": true,
    "requestId": "01a0b40c-c701-70a9-add2-3e584ed47951",
    "details": { "retryAfterSeconds": 3 }
  }
}
```

`Retry-After` carries the same number. Wait it out rather than retrying at once: it is computed from real slot, pace, and cooldown state, so after a platform challenge it can be minutes or hours rather than seconds.

## Errors

Every one of these is free and carries `Refmatter-Credits-Charged: 0`.

| Code                                             | HTTP | Means                                                                   |
| ------------------------------------------------ | ---- | ----------------------------------------------------------------------- |
| `invalid_input`                                  | 400  | Missing or bad `url`, unknown `include` or parameter, invalid `maxAge`  |
| `unauthenticated` / `invalid_credential`         | 401  | No credential, or a key that is invalid, revoked, or expired            |
| `policy_blocked`                                 | 403  | Policy does not allow this object for the workspace                     |
| `not_found`                                      | 404  | A method other than `GET`, or a workspace your credential cannot reach  |
| `unavailable_removed`                            | 410  | The item is gone, or no longer in the Ad Library                        |
| `unsupported_object`                             | 422  | Not an item link: account, short link, unsupported object, unknown host |
| `unavailable_private` / `unavailable_restricted` | 422  | The platform will not serve it publicly                                 |
| `rate_limited`                                   | 429  | No source capacity; see `details.retryAfterSeconds`                     |
| `source_changed`                                 | 502  | The platform no longer matches the supported contract                   |
| `network_transport`                              | 502  | Upstream transport failed                                               |
| `route_disabled` / `route_degraded`              | 503  | Route closed for the workspace, or a dependency is down                 |
| `network_timeout`                                | 504  | The 20 second budget ran out                                            |

`unsupported_object` names what you sent and what to do instead:

```json theme={null}
{
  "code": "unsupported_object",
  "message": "This source object type is not supported.",
  "details": {
    "platform": "instagram",
    "target": "account",
    "hint": "This link names an account. Pass a link to one post, video or ad."
  }
}
```

The full vocabulary is in [Errors](/guides/errors).

## From an agent

The MCP server exposes the same two operations. `lookup_item` states its price and latency in its own description, and `resolve_url` is free, so an agent can check a link before spending anything.

```text theme={null}
resolve_url  { "url": "https://www.tiktok.com/@creator" }
→ target "account", supported false, reason "account_not_supported"

lookup_item  { "url": "https://www.tiktok.com/@creator/video/7437269251355921707",
               "include": "transcript", "maxAge": "1h" }
→ the item, 1 credit live or on a first store hit
```

See [MCP server](/mcp/overview) for setup.

## Not in this endpoint

* Media bytes. A lookup never downloads video or audio; use [import](/guides/import-a-url) for that, and for a reference you can annotate and keep.
* Short share links (`vm.tiktok.com`, `vt.tiktok.com`, `tiktok.com/t/…`, `instagram.com/share/…`). Open them once and pass the URL they land on.
* TikTok photo posts. The TikTok route serves videos; images and carousels are served through Instagram.
* Accounts, lists, and search. Lookup answers one item at a time.
