Records now or a job later
Both routes take the same body, run the same request and produce the same records. They differ in when you get the records back.
Wait for the response
POST /records/{site} runs the request while the connection is open and answers with every record it produced, up to 500. Ask for more with maxItems and the API refuses with 413 and the message sync POST /records/amazon is capped at 500 items (requested 5000); use POST /jobs/amazon for larger runs. Leave maxItems out and the source default of 100 records applies.
The response carries records, count, stats and a jobId: the run is a job like any other, so GET /jobs/{id} shows it afterwards. 200 means the run finished. 499 means the job was cancelled before it finished and 500 means the run failed; both come with ok: false, the records written so far and error filled in.
Submit a job
POST /jobs/{site} answers 202 at once with the job's id and status. Poll GET /jobs/{id}, then read the records in pages from GET /jobs/{id}/records, or ask for a webhook. A job takes up to 5,000 records per submission; a larger maxItems is clamped, never refused, and the value in effect is the maxItems field of the job.
Side by side
| POST /records/{site} | POST /jobs/{site} | |
|---|---|---|
| Reply | After the run finishes | At once, with 202 and the job |
| Records per request | Up to 500 | Up to 5,000, or the largest run your key allows |
| Where the records arrive | In the response body | GET /jobs/{id}/records, in pages or as NDJSON |
| The connection | Stays open for the whole run | Closes as soon as the job is accepted |
idempotency-key | Supported | Supported |
webhookUrl | Supported | Supported |
| When the run fails | 499 or 500, with the records written so far | The job ends failed or cancelled; its records stay readable |
| Use it for | Lookups, tests, small batches | Large runs and background work |
The record envelope
Every record starts with the same fields, whatever the source, followed by the fields of that source.
| Field | Type | Meaning |
|---|---|---|
source | string | The source the record came from, for example amazon. |
type | string | The shape of the record. Set by the source; otherwise the event name without a -detail or -full suffix. |
event | string | The name the record was billed under. Always present. |
id | string or null | The identifier the source uses for the item, or null when it has none. |
url | string | The page the record was read from. |
collectedAt | string | When the record was collected, as an ISO 8601 UTC timestamp. |
status | string | "not_found" on a record that reports the source's answer that an item you looked up does not exist. Absent on a found record, except on property sources, where it is the listing's own status. |
error | object | Present only on a record we could not complete, as { stage, reason }. Absent on every other record. |
{ "source": "amazon", "type": "product", "event": "product-detail", "id": "B0GG9BY46M", "url": "https://www.amazon.com/dp/B0GG9BY46M", "collectedAt": "2026-09-20T20:57:01.663Z"}Example data. A real Amazon record carries these fields plus the ones listed on the Amazon page.
Why billing follows event, not type
A source can produce records of one type under more than one event, and the events can carry different prices. event is the exact name each record was billed under, so it is the field to reconcile spend against; type tells you the shape of the data, nothing more.
In the price list, Amazon bills product at $4.80 and product-detail at $4.80 per 1,000 records, and a record from either can carry type: "product".
GET /usage reports the month's records per site/event under events, and stats.counts on a job does the same for one run. Multiply each count by its price per 1,000 to check a bill. Pricing and billing has the full list.
Items that do not exist
When you look up one item, for example a product by its id or a property by its URL, and the source answers that it does not exist or was removed, you get a record for it with status: "not_found". It carries the id you asked for (as id and under the source's own id field), the url it was looked up at and nothing else: no other field is filled in. The source answered your question, so the record is billed under its event at the same price as a found one, and it counts towards maxItems. A search or listing that finds nothing returns no record at all.
| Field | On a not-found record |
|---|---|
source | The source you asked. |
| The source's own id field | The id you asked for, under the name the source uses for it. |
url | The page the item was looked up at. |
status | "not_found" |
id | The id you asked for. |
type and event | The same as on a found record of that kind. The event sets the price. |
collectedAt | When the source gave its answer. |
| Every other field | Absent. |
Records with error
When we cannot complete an item, because the source blocked the request, timed out, or answered in a way we could not read, the record is delivered with an error field and is not charged. Such records do not count towards maxItems either, so a run that asked for 100 can return 100 complete records plus the ones that failed. Requests that returned nothing usable produce no record at all; they appear in stats.failures, one entry per request with a reason. Neither is ever billed: you never pay when we fail to get an answer.
Next step
For runs above 500 records, continue with Jobs: polling, paging, cancelling and how long records are kept.