Skip to content

API reference

Two endpoints. HTTP 200 is returned even when individual pages failed — check statuses[] for the per-URL outcome.

Endpoint

Request
POST https://livewebdata.com/api/v1/contents
Authorization: Bearer <api_key>
Content-Type: application/json

Request parameters

ParameterTypeRequiredDefaultDescription
urlsstring[]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.
countryCodestringno"us"ISO-3166-1 alpha-2 code. The page is fetched through a connection in this country.
textboolean | objectnotrueContent options. Pass an object to set maxCharacters and verbosity.
text.maxCharactersintegerno100000Truncates 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.
htmlbooleannofalseInclude 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).
livecrawlTimeoutinteger (ms)no30000 (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.
renderbooleannofalseRender 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.
renderFallbackbooleannoonFetch 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.
Example body
{
  "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

FieldTypeDescription
requestIdstringId 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[].urlstringFinal URL after redirects.
results[].titlestring | nullPage title.
results[].author / publishedDate / imagestring | nullMetadata when the page exposes it.
results[].faviconstring | nullFavicon URL.
results[].markdownstringMain content as Markdown.
results[].textstringPlain text, tags removed, whitespace normalized.
results[].htmlstringPage 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[].textTruncatedbooleantrue 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" | nullAlways "livecrawled" on success: every request is fetched live. null on error.
statuses[].fetchedAtstring | nullWhen 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[].creditsintegerCredits charged for this URL: 1 standard, 2 retried, 3 rendered, 5 rendered + retried, 0 on error and for a short page (thin).
statuses[].fallbackUsedbooleanPresent (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[].thinbooleanPresent (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 } | nullPer-URL error, see crawl errors.
searchTimeinteger (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:

CodeMeaningWhat to do
PAGE_TRUNCATEDOnly 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_CONTENTVery 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_FALLBACKThe page structure was too complex to convert in time; text is plain and markdown carries no structure.Use text.
RESPONSE_SIZE_LIMITThe 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

HeaderDescription
X-Request-IdSame as requestId in the body.
X-Credits-RemainingCredit balance after this request.
X-RateLimit-LimitURLs you can send at once (bucket size, = rateLimit.burst in GET /v1/account). The bucket refills at rateLimit.requestsPerSecond URLs per second.
X-RateLimit-RemainingURLs you can send right now.
Retry-AfterSeconds to wait: before retrying a 429, or before repeating a 202 with the same Idempotency-Key.
Idempotency-KeyThe 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.

TagMeaning
CRAWL_NOT_FOUNDThe page returned 404/410.
SOURCE_NOT_AVAILABLEThe target refused access (e.g. 403 or 429) after retries.
CRAWL_LIVECRAWL_TIMEOUTThe live fetch did not finish within livecrawlTimeout.
CRAWL_NOT_RENDEREDNo 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_TYPEThe URL does not serve HTML or text.
CRAWL_UNKNOWN_ERRORSomething else went wrong. Safe to retry.
SOURCE_BLOCKEDAccess denied by the site, or it answered with an interstitial page (HTTP 202) instead of the content. 0 credits.
BROWSER_UNAVAILABLEBrowser rendering is temporarily unavailable; no credits were charged.
RATE_LIMITEDRendering limit for your plan reached; no credits were charged.
RESTRICTED_TARGETThis 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.

HTTPTagMeaning
400INVALID_REQUESTInvalid parameters.
400TOO_MANY_URLSMore URLs than allowed per request (10).
401UNAUTHORIZEDMissing or invalid API key.
402INSUFFICIENT_CREDITSNot enough credits. Top up or upgrade.
403KEY_SUSPENDEDThe key or account is suspended or not yet approved.
429RATE_LIMITEDRate limit exceeded. Retry after the Retry-After header.
409IDEMPOTENCY_CONFLICTThe 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.
413PAYLOAD_TOO_LARGEThe request body is larger than 10 MiB. Nothing was read or charged.
451RESTRICTED_TARGETAll requested URLs are restricted. No credits charged.
503SERVICE_UNAVAILABLETemporarily unavailable. Retry shortly.
Error body
{
  "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:

202 Accepted
{
  "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).

PlanPrice / monthRate limit (URLs/s)
Lite$101
Starter$1002
Developer$5005
Business$90010
Enterprise$2,25020
CustomCustomCustom

Credits

RequestCredits
Standard page (static / server-rendered)1
Retried page (succeeded on a retry through another connection)2
JS-rendered page3
Rendered + retried page5
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.

GET /v1/account
{
  "plan": "starter",
  "status": "active",
  "credits": { "plan": 81234, "topup": 10000, "total": 91234 },
  "rateLimit": { "requestsPerSecond": 2, "burst": 4 },
  "periodEndsAt": "2026-11-01T00:00:00Z"
}