Common patterns
The examples below use the Extract base URL. Use the generated OpenAPI document for route-specific statuses and schemas.
Polling a Run
Runs are asynchronous. After creating a Run, poll until it reaches a terminal
state such as completed, failed, or stopped.
import time
import requests
def wait_for_run(run_id, *, timeout_seconds=3600):
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
response = requests.get(
f"https://api.extralt.com/v1/extract/runs/{run_id}",
headers=HEADERS,
)
response.raise_for_status()
run = response.json()
if run["status"] in ("completed", "failed", "stopped"):
return run
time.sleep(10)
raise TimeoutError(f"Run {run_id} did not finish before the client timeout")Use a client-side timeout and a bounded polling interval. Completion time depends on source coverage, crawl scope, and queue state.
Handling errors
Check the status before decoding a success payload. API errors use the stable
{ "error": { "code", "message", "request_id" } } envelope; retain the
request ID when contacting support.
response = requests.post(
url,
headers={**HEADERS, "Idempotency-Key": idempotency_key},
json=payload,
)
if response.status_code == 429:
# Apply bounded backoff, then retry only if the operation is safe to repeat.
pass
elif response.status_code >= 400:
raise RuntimeError(
f"Extralt request failed ({response.status_code}): {response.text}"
)
result = response.json()For create and restart requests, retry the same payload with the original
Idempotency-Key. This returns the original result instead of duplicating
work. A key reused with a different payload returns 409 Conflict.
Pagination
List responses use cursor pagination. Pass next_cursor as the cursor query
parameter while has_more is true.
def get_all_captures(run_id):
rows = []
cursor = None
while True:
params = {"run_id": run_id}
if cursor is not None:
params["cursor"] = cursor
response = requests.get(
"https://api.extralt.com/v1/extract/captures",
headers=HEADERS,
params=params,
)
response.raise_for_status()
page = response.json()
rows.extend(page["data"])
if not page["has_more"]:
return rows
cursor = page["next_cursor"]Do not use a Capture id from another filter scope as a cursor. Treat cursors as opaque and preserve the same filters across pages.
Rate-limit handling
A 429 response is authoritative. Honor Retry-After when present, then retry
with exponential backoff, random jitter, and a maximum attempt count. Do not
build against an observed request rate as if it were a permanent quota;
operational limits can change.
Reads are generally safe to retry. Retry a mutation only with the same
Idempotency-Key and payload, or when the operation is otherwise documented as
idempotent.
Bulk Capture export
Use the Capture export route instead of paginating when you need all Captures from one Run or Import as JSONL or Parquet. Stream the response to disk rather than loading the entire file in memory. See Working with Captures.