API reference
Two endpoints. HTTP 200 is returned even when individual pages failed — check statuses[] for the per-URL outcome.
Endpoint
POST https://livewebdata.com/api/v1/contents
Authorization: Bearer <api_key>
Content-Type: application/jsonRequest parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
urls | string[] | yes | — | The pages to fetch, 1–10 http or https URLs of at most 8,192 characters each. Processed in parallel; one entry per URL in results/statuses. Duplicates (same URL, ignoring #fragment) are fetched once; repeated positions cost 0 credits. |
countryCode | string | no | "us" | ISO-3166-1 alpha-2 code. The page is fetched through a connection in this country. |
text | boolean | object | no | true | Content options. Pass an object to set maxCharacters and verbosity. |
text.maxCharacters | integer | no | 100000 | Truncates text and markdown to this many characters (max 1,000,000); results[].textTruncated tells you when it did. The MCP tool defaults to 8,000 (max_characters). |
text.verbosity | "compact" | "full" | no | "compact" | compact = main content only. full = whole body incl. navigation and footer. |
html | boolean | no | false | Include the page HTML as served in results[].html. A response carries at most 24 MiB of JSON: past that, the remaining results come without html (warning RESPONSE_SIZE_LIMIT). |
livecrawlTimeout | integer (ms) | no | 30000 (60000 with render) | Maximum time for a live fetch, 1–90000 ms. A lower value is raised to 30000, the time the browser fallback needs, unless you send renderFallback: false (then any value, default 10000). With render or renderFallback: true at least 30000. |
render | boolean | no | false | Render every URL in a real browser, as a normal visitor sees it: 3 credits, 5 when the page needed a retry through another connection. Cannot be combined with renderFallback. |
renderFallback | boolean | no | on | Fetch normally first and render in a real browser only when the first attempt did not return the page, shows no or very little visible text, or was blocked. On by default: omit it. Send false for a plain fetch only (never a browser). A normal success costs its normal 1 or 2 credits, a rendered one only the render credits; statuses[].fallbackUsed tells you when it rendered. Cannot be combined with render: true. |
{
"urls": ["https://example.com/article"],
"countryCode": "de",
"text": { "maxCharacters": 50000, "verbosity": "compact" },
"livecrawlTimeout": 45000
}Unknown fields are rejected with 400 INVALID_REQUEST; the message names the field. So is a URL longer than 8,192 characters (urls.0: Each URL may be at most 8,192 characters.).
Response
| Field | Type | Description |
|---|---|---|
requestId | string | Id of this request, the same for every repeat with the same Idempotency-Key. Also returned as X-Request-Id. |
results[] | object[] | One entry per successfully fetched URL. |
results[].url | string | Final URL after redirects. |
results[].title | string | null | Page title. |
results[].author / publishedDate / image | string | null | Metadata when the page exposes it. |
results[].favicon | string | null | Favicon URL. |
results[].markdown | string | Main content as Markdown. |
results[].text | string | Plain text, tags removed, whitespace normalized. |
results[].html | string | Page HTML as served. Only with html: true, and only while the response stays within its 24 MiB size limit (else warning RESPONSE_SIZE_LIMIT). |
results[].textTruncated | boolean | true when text or markdown was cut at text.maxCharacters, or at the response size limit (warning RESPONSE_SIZE_LIMIT). Present whenever text is enabled. |
results[].warnings | { code, message }[] | Absent when empty. "PAGE_TRUNCATED" = only part of the page could be retrieved; content after that point is missing. "THIN_CONTENT" = very little text, the page probably needs render: true. "EXTRACTION_FALLBACK" = the page was too complex to convert in time; text is plain and markdown has no structure. "RESPONSE_SIZE_LIMIT" = the response reached its 24 MiB size limit: this result has no html, or no html and cut text/markdown. |
statuses[] | object[] | One entry per requested URL, success or error. |
statuses[].status | "success" | "error" | Outcome for this URL. |
statuses[].source | "livecrawled" | null | Always "livecrawled" on success: every request is fetched live. null on error. |
statuses[].fetchedAt | string | null | When the page was fetched (ISO 8601). null on error. |
statuses[].strategy | "fast" | "retry" | "browser" | Fetch strategy. "fast" = standard page (1 credit); "retry" = the first attempt did not return the page, a retry through another connection did (2 credits); "browser" = rendered in a real browser, as a normal visitor sees it (3 credits, 5 when it needed a retry through another connection). |
statuses[].credits | integer | Credits charged for this URL: 1 standard, 2 retried, 3 rendered, 5 rendered + retried, 0 on error and for a short page (thin). |
statuses[].fallbackUsed | boolean | Present (true) only when the browser fallback ran for this URL. With strategy "browser" the content is that render (fast → render); otherwise rendering was tried and did not help. |
statuses[].thin | boolean | Present (true) only for a short page: fewer than 500 characters of visible text even after rendering. Delivered as a success at 0 credits, with only that visible text. |
statuses[].error | { tag, message } | null | Per-URL error, see crawl errors. |
searchTime | integer (ms) | Total processing time. |
Browser fallback is on by default: a page the fast fetch cannot read (no or very little visible text, blocked, 401/403, a timeout) is rendered in a real browser and charged only the render credits. statuses[].fallbackUsed is true when that happened (strategy: "browser" = the content is the render). Send renderFallback: false for a fast fetch only. A page that still shows fewer than 500 characters of visible text after rendering is delivered as a success with statuses[].thin: true at 0 credits, holding only that visible text — it is not an error.
text.maxCharacters (default 100,000, max 1,000,000) cuts text and markdown; results[].textTruncated is true when it did. A response carries at most 24 MiB of JSON: from the result that would pass it on, results come without html, and if that is not enough, text and markdown are cut (textTruncated: true), each such result flagged RESPONSE_SIZE_LIMIT. The request never fails because of it. title and author are at most 2,000 characters each; an image or favicon URL longer than 4,096 characters is left out (favicon then falls back to /favicon.ico). The MCP tool get_page_contents has its own, smaller default of 8,000 (max_characters), because its result goes into an agent's context. results[].warnings is present only when there is something to know about a page:
| Code | Meaning | What to do |
|---|---|---|
PAGE_TRUNCATED | Only part of the page could be retrieved (size limit or an interrupted transfer); content after that point is missing. | Nothing to retry; the result is the part that arrived. |
THIN_CONTENT | Very little text next to scripts or an empty app root: the page probably loads its content with JavaScript. The URL is still charged as a success. | Retry with render: true. The default browser fallback renders a page with fewer than 500 characters of visible text by itself, not one with more. |
EXTRACTION_FALLBACK | The page structure was too complex to convert in time; text is plain and markdown carries no structure. | Use text. |
RESPONSE_SIZE_LIMIT | The response reached its 24 MiB size limit. This result and every later one come without html; when that was not enough, text and markdown were cut too (textTruncated: true). The URL is still a success and is charged. | Request fewer URLs at once, or without html. |
Response headers
| Header | Description |
|---|---|
X-Request-Id | Same as requestId in the body. |
X-Credits-Remaining | Credit balance after this request. |
X-RateLimit-Limit | URLs you can send at once (bucket size, = rateLimit.burst in GET /v1/account). The bucket refills at rateLimit.requestsPerSecond URLs per second. |
X-RateLimit-Remaining | URLs you can send right now. |
Retry-After | Seconds to wait: before retrying a 429, or before repeating a 202 with the same Idempotency-Key. |
Idempotency-Key | The request's Idempotency-Key: yours, or one the API created when you sent none. On every answer (200, 202, errors) except a 400 for a malformed key and a 413 for a body over 10 MiB (refused unread). Repeat a 202 with it. |
Errors
Per-URL errors
Returned inside statuses[].error with HTTP 200. These never cost credits.
| Tag | Meaning |
|---|---|
CRAWL_NOT_FOUND | The page returned 404/410. |
SOURCE_NOT_AVAILABLE | The target refused access (e.g. 403 or 429) after retries. |
CRAWL_LIVECRAWL_TIMEOUT | The live fetch did not finish within livecrawlTimeout. |
CRAWL_NOT_RENDERED | No usable content, e.g. the page needs JavaScript rendering (retry without renderFallback: false, or with render: true), or a rendered page shows no visible text. 0 credits. |
UNSUPPORTED_CONTENT_TYPE | The URL does not serve HTML or text. |
CRAWL_UNKNOWN_ERROR | Something else went wrong. Safe to retry. |
SOURCE_BLOCKED | Access denied by the site, or it answered with an interstitial page (HTTP 202) instead of the content. 0 credits. |
BROWSER_UNAVAILABLE | Browser rendering is temporarily unavailable; no credits were charged. |
RATE_LIMITED | Rendering limit for your plan reached; no credits were charged. |
RESTRICTED_TARGET | This URL is restricted. Other URLs in the same request are still processed. 0 credits. |
Request errors
Returned with a non-200 status and no results.
| HTTP | Tag | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | Invalid parameters. |
| 400 | TOO_MANY_URLS | More URLs than allowed per request (10). |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 402 | INSUFFICIENT_CREDITS | Not enough credits. Top up or upgrade. |
| 403 | KEY_SUSPENDED | The key or account is suspended or not yet approved. |
| 429 | RATE_LIMITED | Rate limit exceeded. Retry after the Retry-After header. |
| 409 | IDEMPOTENCY_CONFLICT | The Idempotency-Key was already used with a different request, or its result is no longer available (results are kept 15 minutes). Use a new key. |
| 413 | PAYLOAD_TOO_LARGE | The request body is larger than 10 MiB. Nothing was read or charged. |
| 451 | RESTRICTED_TARGET | All requested URLs are restricted. No credits charged. |
| 503 | SERVICE_UNAVAILABLE | Temporarily unavailable. Retry shortly. |
{
"error": {
"code": 402,
"tag": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Top up or upgrade your plan."
},
"requestId": "req_01J…"
}Long requests and Idempotency-Key
Most requests answer within seconds. A request still fetching after about 100 seconds (slow sites, rendering, a long livecrawlTimeout) answers 202 instead of making you wait on an open connection:
{
"status": "pending",
"requestId": "req_01J…",
"idempotencyKey": "lwda_3f1c2b7e-0d4a-4c1e-9a55-2b8f0e6c7d10",
"retryAfter": 1,
"message": "Still fetching. Send the same request again with the same Idempotency-Key header to get the result; it is not charged again."
}Send the same request body again with the header Idempotency-Key: <idempotencyKey> after Retry-After seconds. The repeat waits for the fetch and answers 200 with the result, or 202 again if it is still running. A repeat is never charged again. The SDKs and the MCP server do this for you.
Always send your own Idempotency-Key (8–128 characters of A–Z a–z 0–9 _ - : ., for example a UUID) on the first request, and keep it until you have the answer. A connection can break after the API received the request: a network error, a timeout on your side, a deploy. Send the same request again with the same key, and you get that fetch (its result, or 202 while it still runs) instead of a new one, so you pay once. Without your own key, a cut connection is a lost answer: the retry has no key, so it is a new request and charged again.
Every answer of POST /v1/contents (200, 202 and errors) carries the request's key in the Idempotency-Key response header: yours, or one the API created when you sent none. The only exceptions are the 400 for a malformed key and the 413 for a request body over 10 MiB, which is refused before it is read (nothing was charged, there is nothing to repeat). The SDKs create a key for each contents() call when you pass none, send it on every retry and put it on the error they raise (idempotencyKey / idempotency_key).
- Results are kept for 15 minutes. A result nobody collects stays charged; after 15 minutes a repeat answers
409 IDEMPOTENCY_CONFLICT, and a new key fetches the pages again. - A key is bound to its request body for 24 hours: reusing it with a different body answers
409 IDEMPOTENCY_CONFLICT. Use a new key for every new request.
Rate limits
Limits are per account and count URLs per second (a request with 3 URLs uses 3). When exceeded, the API returns 429 with a Retry-After header (seconds). Limits work as a bucket: X-RateLimit-Limit is how many URLs you can send at once (rateLimit.burst in GET /v1/account), X-RateLimit-Remaining how many right now, and the bucket refills at your plan's URLs per second (rateLimit.requestsPerSecond).
| Plan | Price / month | Rate limit (URLs/s) |
|---|---|---|
| Lite | $10 | 1 |
| Starter | $100 | 2 |
| Developer | $500 | 5 |
| Business | $900 | 10 |
| Enterprise | $2,250 | 20 |
| Custom | Custom | Custom |
Credits
| Request | Credits |
|---|---|
| Standard page (static / server-rendered) | 1 |
| Retried page (succeeded on a retry through another connection) | 2 |
| JS-rendered page | 3 |
| Rendered + retried page | 5 |
| Short page (under 500 characters of visible text; delivered as that text, a success) | 0 |
| Failed request (incl. access denied by the site) | 0 |
- Plan credits reset monthly.
- Top-up credits never expire.
When your balance is too low for the URLs in a request, the API returns 402 INSUFFICIENT_CREDITS. The current balance is returned in X-Credits-Remaining on every response.
Account
GET https://livewebdata.com/api/v1/account returns your plan, remaining credits and rate limit. It is free — call it before large jobs.
{
"plan": "starter",
"status": "active",
"credits": { "plan": 81234, "topup": 10000, "total": 91234 },
"rateLimit": { "requestsPerSecond": 2, "burst": 4 },
"periodEndsAt": "2026-11-01T00:00:00Z"
}