{"openapi":"3.1.0","info":{"title":"Triatomine","description":"Adaptive multi-tier scraping API. Selectors that survive template drift, tier-priced from static-HTML cheap to Cloudflare-grade premium.","version":"0.1.0"},"paths":{"/signup":{"post":{"tags":["signup"],"summary":"Signup","description":"Sign up for a free-tier API key.\n\nFirst call with a given email creates the user + key and shows the key\nonce. Subsequent calls (e.g., user lost the email) refresh the\nverification token and re-send the link, but never re-issue the API key.","operationId":"signup_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/verify/{token}":{"get":{"tags":["signup"],"summary":"Verify","description":"Mark a user's email verified. Shows a success or error HTML page.","operationId":"verify_verify__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/health":{"get":{"summary":"Health","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health V1 Health Get"}}}}}}},"/errors":{"get":{"summary":"Human-readable error reference","description":"Authoritative documentation for every `code` value the API can return. The error envelope's `docs_url` field deep-links to anchors on this page.","operationId":"errors_docs_errors_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/v1/scrape":{"post":{"summary":"Scrape","description":"Scrape one URL synchronously through the tier cascade.\n\nRequires `X-API-Key` header. Rate limited per-(IP, key): 1 req/sec free tier,\n10 req/sec paid tiers. Returns within 30s or 408 timeout.\n\nTier-3 (stealth) and tier-4 (real Chrome) are forbidden on this endpoint —\nuse POST /v1/jobs for async tier-3+ work.","operationId":"scrape_v1_scrape_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScrapeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScrapeResponse"}}}},"400":{"description":"Tier 3+ requested on the sync endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"408":{"description":"Sync 30s timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-(IP, key) rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"451":{"description":"Host blocked by operator policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/jobs":{"post":{"summary":"Create Job","description":"Enqueue an async scrape. Returns 202 with job id and poll URL.\n\nUse this for tier-3 work that may exceed the 30s sync timeout, or any time\nyou'd rather poll than block. Subject to the same rate limit as /v1/scrape.","operationId":"create_job_v1_jobs_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScrapeRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCreateResponse"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-(IP, key) rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"451":{"description":"Host blocked by operator policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/batch":{"post":{"summary":"Create Batch","description":"Fan out one set of options across 1-100 URLs as independent async jobs.\n\nEach URL becomes its own Job row, pollable independently via `GET /v1/jobs/{id}`.\nCosts ONE rate-limit token per request (regardless of URL count) — the per-job\nfan-out doesn't multiply your bucket consumption. Per-job tier and metering\nwork the same as `POST /v1/jobs`.","operationId":"create_batch_v1_batch_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchScrapeRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchCreateResponse"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation failed (0 or >100 URLs, malformed URL, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Per-(IP, key) rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"451":{"description":"One or more hosts in the batch are blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/jobs/{job_id}":{"get":{"summary":"Get Job","description":"Poll the current state of an async job.\n\nAuth-only (no rate limit) — polling must be cheap. 404 on unknown id or any\njob that doesn't belong to the calling key (no cross-tenant info leak).","operationId":"get_job_v1_jobs__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Job not found, or not owned by this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"BatchCreateResponse":{"properties":{"batch_size":{"type":"integer","title":"Batch Size"},"items":{"items":{"$ref":"#/components/schemas/JobCreateResponse"},"type":"array","title":"Items"}},"type":"object","required":["batch_size","items"],"title":"BatchCreateResponse","description":"Returned from POST /v1/batch — one JobCreateResponse per submitted URL."},"BatchScrapeRequest":{"properties":{"urls":{"items":{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},"type":"array","maxItems":100,"minItems":1,"title":"Urls","description":"1 to 100 URLs to scrape, each as its own async job."},"force_tier":{"anyOf":[{"type":"integer","maximum":4.0,"minimum":1.0},{"type":"null"}],"title":"Force Tier"},"max_tier":{"anyOf":[{"type":"integer","maximum":4.0,"minimum":1.0},{"type":"null"}],"title":"Max Tier"},"expect_product":{"type":"boolean","title":"Expect Product","default":true},"use_llm":{"type":"boolean","title":"Use Llm","default":true}},"type":"object","required":["urls"],"title":"BatchScrapeRequest","description":"Body of POST /v1/batch — fan out one set of options across many URLs.\n\nAll URLs in a batch share the same tier / expect_product / use_llm settings.\nNeed different params per URL? Make multiple POSTs to /v1/jobs."},"ErrorDetail":{"properties":{"code":{"type":"string","enum":["invalid_request","validation_error","timeout","tier_unavailable","internal_error","rate_limited","auth_required","host_blocked","email_unverified"],"title":"Code"},"message":{"type":"string","title":"Message"},"retry_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Retry After"},"docs_url":{"type":"string","title":"Docs Url","default":"https://triatomine.com/docs/errors"}},"type":"object","required":["code","message"],"title":"ErrorDetail"},"ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/ErrorDetail"}},"type":"object","required":["error"],"title":"ErrorResponse"},"Failure":{"properties":{"kind":{"$ref":"#/components/schemas/FailureClass","description":"Stable public class. Use this for retry/upgrade logic."},"tier_attempted":{"type":"integer","title":"Tier Attempted","description":"The last tier the cascade ran (1=HTTP, 2=headless, 3=stealth, 4=real Chrome)."},"tier_path":{"items":{"type":"integer"},"type":"array","title":"Tier Path","description":"Every tier the cascade tried, in order."},"retryable":{"type":"boolean","title":"Retryable","description":"True if a retry (possibly with different params) might succeed."},"hint":{"type":"string","title":"Hint","description":"Human-readable next step for this failure class."},"detail":{"type":"string","title":"Detail","description":"Raw orchestrator quality reason (e.g. 'challenge:cloudflare_turnstile')."}},"type":"object","required":["kind","tier_attempted","tier_path","retryable","hint","detail"],"title":"Failure","description":"Structured per-scrape failure. Populated on `ScrapeResponse` when\n`success=False`. The `kind` is the stable, public taxonomy customers should\nbranch on; `detail` carries the raw orchestrator quality reason for debugging."},"FailureClass":{"type":"string","enum":["transport_error","anti_bot_block","no_product_signal","needs_higher_tier","tier_unavailable"],"title":"FailureClass"},"FetchInfo":{"properties":{"final_url":{"type":"string","title":"Final Url"},"status":{"type":"integer","title":"Status"},"elapsed_ms":{"type":"integer","title":"Elapsed Ms"},"tier":{"type":"integer","title":"Tier"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["final_url","status","elapsed_ms","tier"],"title":"FetchInfo","description":"Fetch metadata returned alongside the extracted product."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobCreateResponse":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["queued","running","succeeded","failed","timeout"],"title":"Status"},"poll_url":{"type":"string","title":"Poll Url"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","status","poll_url","created_at"],"title":"JobCreateResponse","description":"Returned from POST /v1/jobs — small ack with poll URL."},"JobResponse":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["queued","running","succeeded","failed","timeout"],"title":"Status"},"url":{"type":"string","title":"Url"},"request_params":{"additionalProperties":true,"type":"object","title":"Request Params"},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Result"},"error_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Reason"},"raw_html_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Raw Html Url","description":"Presigned URL to the raw HTML (24h TTL). Only populated on success."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"finished_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished At"}},"type":"object","required":["id","status","url","request_params","created_at"],"title":"JobResponse","description":"Full job state — returned from GET /v1/jobs/{id}."},"ScrapeRequest":{"properties":{"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url","description":"URL to scrape"},"force_tier":{"anyOf":[{"type":"integer","maximum":4.0,"minimum":1.0},{"type":"null"}],"title":"Force Tier","description":"Skip auto-escalation, run only this tier"},"max_tier":{"anyOf":[{"type":"integer","maximum":4.0,"minimum":1.0},{"type":"null"}],"title":"Max Tier","description":"Maximum tier to escalate to (1-4)"},"expect_product":{"type":"boolean","title":"Expect Product","description":"Require a Schema.org Product signal in the quality check","default":true},"use_llm":{"type":"boolean","title":"Use Llm","description":"Allow LLM extraction fallback on hard pages","default":true}},"type":"object","required":["url"],"title":"ScrapeRequest","description":"Body of POST /v1/scrape."},"ScrapeResponse":{"properties":{"url":{"type":"string","title":"Url"},"success":{"type":"boolean","title":"Success"},"tier_path":{"items":{"type":"integer"},"type":"array","title":"Tier Path"},"quality_reason":{"type":"string","title":"Quality Reason"},"fetch":{"$ref":"#/components/schemas/FetchInfo"},"product":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Product"},"failure":{"anyOf":[{"$ref":"#/components/schemas/Failure"},{"type":"null"}],"description":"Populated when success=False. Replaces the old error_reason string."}},"type":"object","required":["url","success","tier_path","quality_reason","fetch"],"title":"ScrapeResponse","description":"Body of 200 response from POST /v1/scrape."},"SignupRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"SignupRequest"},"SignupResponse":{"properties":{"message":{"type":"string","title":"Message"},"api_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key"},"key_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key Prefix"},"email":{"type":"string","title":"Email"},"verification_required":{"type":"boolean","title":"Verification Required","default":true}},"type":"object","required":["message","email"],"title":"SignupResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}