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

# Extractions — the URL-first path

> Already know the catalog page? Skip the fan-out: one URL, one pass, one signed receipt. discover → extract → refresh → monitor, never touching a session.

<Warning>
  **Not yet available — planned.** The entire URL-first surface on this page —
  `POST /v1/discover`, `POST /v1/extractions` (single and batch), every
  `GET /v1/extractions/…` read, `…/refresh`, and `GET /v1/receipts/{id}` — is specced
  but **not yet served**: these routes answer `501 not_implemented`. Nothing on this page can
  be called today. This page is the contract the surface ships against; the
  [changelog](/changelog) announces when it goes live. Sessions
  ([quickstart](/quickstart)) are the live path.
</Warning>

Many integrators don't have a question — they have a **link**: a specific catalog URL
whose rows they want, on demand and on a schedule. `/v1/extractions` is that path. No
sentence to phrase, no merchant resolution to pay for, no fan-out: one known URL goes
straight to the extraction agent and comes back as a catalog under a signed receipt.

The URL-first ladder is `discover → extract → refresh → monitor`, and it never touches
a session.

## Step 0 (optional): discover — \$0

Have a site but not the exact catalog link? `POST /v1/discover` runs deterministic
candidate discovery — free:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/discover \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://powellsbooks.com"}'
```

```json theme={null}
{
  "data": [
    { "url": "https://powellsbooks.com/used/poetry", "kind": "collection", "signals": ["paginated", "priced-items"] },
    { "url": "https://powellsbooks.com/sitemap.xml", "kind": "sitemap-derived", "signals": ["product-urls"] }
  ]
}
```

Candidates come back best-first. Like every customer-supplied URL on this API,
discover targets must resolve to public hosts — private, loopback, and link-local
ranges are refused with [invalid\_param](/errors/invalid_param).

## Create an extraction

```bash theme={null}
curl https://api.fiveninelabs.com/v1/extractions \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://powellsbooks.com/used/poetry",
    "entity": "book",
    "guidance": "used poetry section only; include condition and price",
    "max_spend_usd": 1
  }'
```

| Field                     | Required | Meaning                                                                                                                                                                                 |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`                     | yes\*    | The catalog page. Https, public hosts only.                                                                                                                                             |
| `entity`                  | yes      | What one row **is** (`"book"`, `"ticket listing"`, `"espresso machine"`). Drives extraction targeting and the completeness gate.                                                        |
| `guidance`                | no       | Free-text steering, ≤ 2,000 chars. Treated as **data for the request composer, never as instructions to the agent** — it cannot smuggle credentials, identity, or commands into a pass. |
| `max_spend_usd`           | yes      | The reservation and ceiling, per extraction.                                                                                                                                            |
| `urls`                    | no\*     | Batch form — see below. `url` and `urls` are mutually exclusive.                                                                                                                        |
| `webhook_url`, `metadata` | no       | Per-run delivery + your own KV (≤ 16 string keys, echoed everywhere).                                                                                                                   |

The response is `201` with the extraction object — or the live SSE stream if you send
`Accept: text/event-stream`, exactly as with sessions. Then the same read verbs:

```
GET  /v1/extractions/{id}            status, spend, claim, receipt id
GET  /v1/extractions/{id}/events     the pass's event log (same stable subset)
GET  /v1/extractions/{id}/stream     SSE, resumable
GET  /v1/extractions/{id}/result     long-poll ?timeout= · ?format=ndjson
GET  /v1/receipts/{id}               the signed receipt behind receipt_id
```

## Terminal statuses — the claim is stated

`delivered | abstained | error | stopped` (open enum). On `delivered`, the object
carries the receipt's **claim**:

* `"complete"` — the agent covered the catalog and says so.
* `"partial"` — it delivered what it could and *says which part* — never dressed up
  as complete.

An `abstained` extraction means the page isn't an extractable catalog, or the
meaningful data is gated — stated, and **\$0 is billed** beyond what discovery
measurably spent. An honest "no" instead of an empty "yes".

```json theme={null}
{
  "id": "ext_6b1st",
  "object": "extraction",
  "status": "delivered",
  "url": "https://powellsbooks.com/used/poetry",
  "entity": "book",
  "claim": "complete",
  "records": 214,
  "spend_usd": 0.48,
  "receipt_id": "rcpt_p11xa",
  "created_at": "2026-08-26T14:32:07Z"
}
```

## Batches — up to 50 URLs

Same entity and guidance across a list of URLs, one extraction per URL:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/extractions \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "urls": ["https://a.example/catalog", "https://b.example/shop/all"],
    "entity": "espresso machine",
    "max_spend_usd": 1
  }'
```

Budget honesty: `max_spend_usd` reserves **per URL** — the batch's worst case is
N × the cap, and the batch response states it as `total_reserved_usd` before anything
runs. Validation is all-or-nothing: one invalid URL fails the whole POST with
[invalid\_param](/errors/invalid_param) naming it; there is no partial batch creation.

## What extractions deliberately are not

* They never fan out. One URL, one pass. If you want merchant discovery from a
  question, that's a [session](/quickstart).
* The request cannot carry instructions to the agent. Customers state intent
  (`entity`, `guidance`); the actual agent request is composed through a fail-closed
  chain — off-enum values collapse to absence, and identity fields are never filled.

## The second ask is cheaper than the first

`POST /v1/extractions/{id}/refresh` re-runs the same URL at marginal cost, serves
from the record at \$0 when your `max_age_s` is satisfied, and diffs against the
previous delivery. A monitor is that refresh on a cadence with a webhook. Both are
covered in [Refresh and monitors](/guides/refresh-and-monitors).
