Production Reliability
Retries, timeouts, and backoff in depth.
Every error the Tabstack API returns, what triggers it, and how to handle it.
When something goes wrong with a Tabstack call, you hit one of two layers. The first is the HTTP layer: the API returns a status code and a JSON error body. That layer is canonical - it’s what the wire actually carries, regardless of which language you’re in. The second is the SDK layer: the TypeScript and Python SDKs read that status code and raise a typed exception (BadRequestError, RateLimitError, and so on) so you can branch on the failure with instanceof / except instead of inspecting numbers.
The SDK classes are a thin, typed wrapper over the HTTP statuses. This page consolidates every error the API can return, the typed class each one maps to, and the separate class of failures the /automate and /research streams produce after a successful connection.
Every HTTP error returns a JSON object with a single error field, nothing else.
{ "error": "json schema is required"}That’s the whole contract. The string is human-readable and endpoint-specific, but the shape never changes. The SDKs surface this string as the exception’s message.
This is the master list. Every status the API returns across /extract, /generate, /automate, /extract/markdown, and /research is here. The table gives the common cause; endpoint-specific message variants follow underneath.
| Status | Meaning | When it fires |
|---|---|---|
| 400 | Bad Request | The request body is malformed, or a required parameter is missing or invalid. |
| 401 | Unauthorized | API key is missing, invalid, or expired. |
| 403 | Forbidden | Your key doesn’t have access to the requested resource. |
| 404 | Not Found | The endpoint or resource doesn’t exist. |
| 408 | Request Timeout | The server timed out waiting for the request. No dedicated SDK class — surfaces as the generic APIError / APIStatusError. Auto-retried by the SDK. |
| 409 | Conflict | The request conflicts with the current state of a resource. |
| 422 | Unprocessable Entity | The URL is malformed or points to an inaccessible/private resource (e.g. localhost, 127.0.0.1, private IPs). |
| 429 | Too Many Requests | You’ve exceeded your plan’s rate limit or quota. Auto-retried by the SDK. |
| 500 | Internal Server Error | Server-side failure: the target URL failed to fetch, the page was too large, or generation/extraction failed. |
| 503 | Service Unavailable | A backing service (today, /automate) isn’t configured or is temporarily unavailable. |
The same status carries different error strings depending on which endpoint you called. Expand a status to see exact strings where the generated API reference publishes them, or the documented trigger when the reference groups several variants together.
| Endpoint | error message / trigger |
|---|---|
/extract/json | url is required |
/extract/json | json schema is required |
/extract/json | json schema must be a valid object |
/extract/json | invalid JSON request body |
/generate/json | Missing or malformed url, json_schema, or instructions |
/automate | task is required |
/automate | invalid URL format |
/automate | maxIterations must be between 1 and 100 |
/extract/markdown | Missing or malformed url |
/research | Missing or malformed query or mode |
| Endpoint | error message |
|---|---|
| All endpoints | Unauthorized - Invalid token |
| Endpoint | error message / trigger |
|---|---|
/extract/json | url is invalid |
/extract/markdown | url is invalid |
/extract/markdown | access to internal resources is not allowed |
| Endpoint | error message / trigger |
|---|---|
/extract/json | failed to fetch URL |
/extract/json | web page is too large |
/extract/json | failed to generate JSON |
/generate/json | Page too large, fetch failed, or the AI transformation failed |
/automate | failed to call automate server |
/extract/markdown | failed to fetch URL |
/extract/markdown | failed to convert HTML to Markdown |
| Endpoint | error message |
|---|---|
/automate | automate service not available |
Both SDKs translate each HTTP status into a typed class. Catch these instead of comparing status codes by hand.
| HTTP status | SDK class | Notes |
|---|---|---|
| 400 | BadRequestError | |
| 401 | AuthenticationError | |
| 403 | PermissionDeniedError | |
| 404 | NotFoundError | |
| 409 | ConflictError | |
| 422 | UnprocessableEntityError | |
| 429 | RateLimitError | |
| 500+ | InternalServerError | Covers 503 as well. |
| any other 4xx | APIError (TS) / APIStatusError (Python) | Statuses without a dedicated class — e.g. 408, 410 — surface as the generic status-error base, with the code on .status (TS) / .status_code (Python). |
| none | APIConnectionError | Network connectivity, DNS, or firewall failures. The request never reached the API. |
| none | APIConnectionTimeoutError (TS) / APITimeoutError (Python) | The connection timed out. In Python this is a subclass of APIConnectionError. |
Only the statuses with a dedicated row above get their own class. Any other status code is not silently dropped — it still raises, but as the generic base (APIError in TS, APIStatusError in Python) rather than a named subclass. Branch on .status / .status_code for those.
Every status class descends from a common base, TabstackError (the root) and APIError, so catching TabstackError is the catch-all when you don’t care which specific failure occurred. The Python hierarchy adds two intermediate classes the TypeScript SDK doesn’t expose: APIStatusError (the base for all status-code errors, carrying status_code, message, and body) and APIResponseValidationError.
The naming difference worth remembering: the connection-timeout class is APIConnectionTimeoutError in TypeScript and APITimeoutError in Python.
/automate and /research)The streaming endpoints are different. /automate and /research both stream Server-Sent Events, and most failures there happen after a successful HTTP connection, inside the stream, while the agent is working. (HTTP errors still apply for failures before the stream opens — a missing task/query, a bad token, a 429, or a 503 — and those raise the typed classes above.)
/automateFailures surface in two places inside the stream:
When the agent gives up, the canonical final state arrives in the complete event with success: false and a structured error payload of { code, message }. The code is a typed enum you can branch on:
| Code | Meaning |
|---|---|
TASK_ABORTED | The task was terminated early. |
MAX_ITERATIONS | The agent hit its maxIterations limit. |
MAX_ERRORS | The agent accumulated too many errors. |
TASK_FAILED | The task failed to complete. |
A task:aborted event also fires earlier in the stream, but treat complete as authoritative. It carries the final state.
A top-level error event fires only if the task runner itself crashes, distinct from the agent-level aborts above, which are an orderly “I couldn’t finish.” Its payload is { success: false, error: { code, message, timestamp } }, where timestamp is an ISO-8601 string. This event is a full member of the typed AutomateEvent union, so an exhaustive switch narrows it.
for await (const event of stream) { switch (event.event) { case "complete": if (!event.data.success) { // Agent-level abort: TASK_ABORTED | MAX_ITERATIONS | MAX_ERRORS | TASK_FAILED console.error( `Aborted (${event.data.error?.code}):`, event.data.error?.message, ); } break; case "error": // The runner itself crashed console.error( `Runner error (${event.data.error.code}):`, event.data.error.message, ); break; }}For the full event flow and consumption patterns, see Automate Events.
/research/research reports task-level failures with a single error event inside the stream. Its payload is { error: { message, name, stack? }, activity?, iteration?, message, timestamp }, where activity tells you which phase failed. There is no agent-level complete failure path as in /automate — if the run fails, the error event is the signal. If you only listen for complete, failures produce no output.
for await (const event of stream) { if (event.event === "error") { // Task-level failure: phase is in event.data.activity throw new Error(event.data.error.message); }}For the full event flow, see Autonomous Research.
The SDKs retry transient failures automatically, twice, with exponential backoff. The retried set is:
| Status | Error |
|---|---|
| 408 | Request Timeout |
| 409 | Conflict |
| 429 | Rate Limit |
| 500+ | Server errors |
| none | Network / connection failures |
Everything else (400, 401, 403, 404, 422) fails fast and is not retried, because retrying won’t help: you have to fix the request first. Retries are transparent; you don’t need to write retry logic for the cases above.
You can tune this. Set maxRetries: 0 (TypeScript) or max_retries=0 (Python) on the client to disable it when you run your own retry orchestration, or override per request. See Production Reliability for retry, timeout, and backoff details.
One pattern covers nearly everything: try the call, branch on the typed class, handle each failure mode for what it is. Auth failures need a config fix, rate limits need backoff, server errors are worth a retry.
import Tabstack, { BadRequestError, AuthenticationError, UnprocessableEntityError, RateLimitError, InternalServerError,} from "@tabstack/sdk";
const client = new Tabstack();
try { const result = await client.extract.json({ url, json_schema }); return result;} catch (err) { if (err instanceof BadRequestError) { throw new Error(`Bad request: ${err.message}`); } if (err instanceof AuthenticationError) { throw new Error("Authentication failed. Check your API key."); } if (err instanceof UnprocessableEntityError) { throw new Error(`Invalid URL: ${err.message}`); } if (err instanceof RateLimitError) { throw new Error("Rate limit hit. Back off and retry."); } if (err instanceof InternalServerError) { throw new Error(`Server error: ${err.message}`); } throw err;}from tabstack import Tabstackfrom tabstack import ( BadRequestError, AuthenticationError, UnprocessableEntityError, RateLimitError, InternalServerError,)
client = Tabstack()
try: result = client.extract.json(url=url, json_schema=json_schema)except BadRequestError as e: print(f"Bad request: {e}")except AuthenticationError: print("Authentication failed. Check your API key.")except UnprocessableEntityError as e: print(f"Invalid URL: {e}")except RateLimitError: print("Rate limit hit. Back off and retry.")except InternalServerError as e: print(f"Server error: {e}")Handle the specific classes first and fall back to TabstackError for anything you didn’t anticipate. For the full hierarchy and richer patterns (retry with backoff, batch error tracking, logging), see the SDK error-handling guides for TypeScript and Python.
Production Reliability
Retries, timeouts, and backoff in depth.
Automate Events
The full SSE stream and its error events.
API Reference