{"openapi":"3.1.0","info":{"title":"on-page.ai API","version":"1.0.0-preview","description":"Public REST + MCP surface at api.on-page.ai. This spec is the canonical contract — every response shape, error body, and outbound webhook event is generated from the runtime Zod validators.","contact":{"name":"on-page.ai","url":"https://on-page.ai"},"license":{"name":"Proprietary","identifier":"LicenseRef-proprietary"}},"servers":[{"url":"https://api.on-page.ai"}],"tags":[{"name":"NLP","description":"Classification + entity extraction."},{"name":"Scan","description":"URL + keyword analysis."},{"name":"Jobs","description":"Status + terminal result reads."},{"name":"Credits","description":"Ledger balance."},{"name":"Webhooks","description":"Test delivery + outbound event schemas."}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"api-key","description":"Bearer token issued in the dashboard. See /dashboard/api-keys. Required scopes per endpoint are listed on each operation."}},"schemas":{"AcceptedJobResponse":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["queued"]},"request_id":{"type":"string"},"estimated":{"type":"object","properties":{"queue_position":{"type":"integer","minimum":0}},"required":["queue_position"]}},"required":["job_id","status","request_id"],"description":"Response to a successful `POST /v1/classify|scan|scan/lite|scan/deep` job-create call. Returned as HTTP 202."},"JobStatusResponse":{"type":"object","properties":{"job_id":{"type":"string"},"type":{"type":"string","enum":["classify","scan"],"description":"Job type supplied on creation and echoed in status/result reads."},"status":{"type":"string","enum":["queued","waiting","running","processing","completed","failed","cancelled"],"description":"Job lifecycle state. Terminal states are `completed`, `failed`, and `cancelled`."},"progress":{"type":["integer","null"],"minimum":0,"maximum":100},"created_at":{"type":"string"},"started_at":{"type":["string","null"]},"completed_at":{"type":["string","null"]},"region":{"type":["string","null"],"enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK",null],"description":"Canonical scan region. `null` for classify jobs and for legacy scan rows whose stored region/market values fail the strict 27-region check.","example":"UK"},"locale":{"type":["string","null"],"description":"BCP-47 locale that was sent (or filled by Phase-2 admission) when the job was created. `null` for classify jobs and for legacy scan rows that did not include a locale."},"result":{"type":"null"},"error":{"type":["object","null"],"properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["job_id","type","status","progress","created_at","started_at","completed_at","region","locale","result","error"],"description":"Lightweight status view. Poll this endpoint (or subscribe to webhooks) to observe job progression. When `status` is terminal, fetch `/v1/jobs/:job_id/result` for the payload. `region` / `locale` echo the canonical scan-region fields; `null` for classify jobs and for legacy scan rows whose stored values fail validation (no fabrication)."},"JobResultResponse":{"oneOf":[{"type":"object","properties":{"meta":{"type":"object","properties":{"report_date":{"type":"string"},"target_keyword":{"type":"string"},"location":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"categories":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"score":{"type":"number"}},"required":["category","score"]},"maxItems":3}},"required":["meta","categories"],"description":"Terminal `customer_v1` payload for a completed classify job."},{"type":"object","properties":{"meta":{"type":"object","properties":{"report_date":{"type":["string","null"]},"target_keyword":{"type":["string","null"]},"location":{"type":["string","null"]},"url":{"type":["string","null"]}},"required":["report_date","target_keyword","location","url"],"additionalProperties":false},"on_page_optimization":{"type":"object","properties":{"score":{"type":["integer","null"],"minimum":0,"maximum":100},"grade":{"type":"string","enum":["Excellent","Strong","Good","Needs Work","Under-optimized","Unavailable"]},"confidence":{"type":"number","minimum":0,"maximum":1},"summary":{"type":"string"},"focus_areas":{"type":"array","items":{"type":"string"}},"algorithm_version":{"type":"string","enum":["onpage-optimization-score-v1.1"]}},"required":["score","grade","confidence","summary","focus_areas","algorithm_version"],"additionalProperties":false,"description":"Customer-facing On-Page Optimization Score, computed from scan data already produced by the job. The proprietary algorithm exposes only the final score, grade, confidence, summary, and high-level focus areas."},"benchmarks":{"type":"object","additionalProperties":{}},"entity_coverage":{"type":"object","additionalProperties":{}},"topic_and_classification":{"type":"object","additionalProperties":{}},"internal_linking":{"type":"object","additionalProperties":{}},"competitor_term_coverage":{"type":"object","additionalProperties":{}},"serp_speed_benchmark":{"type":"object","properties":{"status":{"type":"string","enum":["ok","disabled","skipped_not_deep","skipped_no_target_url","skipped_budget_exhausted","timeout","internal_error"]},"what_this_measures":{"type":"string"},"measurement_type":{"type":"string","enum":["single_run_lab"]},"device_profile":{"type":"string","enum":["mobile_viewport_unthrottled_lab"]},"network_profile":{"type":"string","enum":["unthrottled"]},"cpu_profile":{"type":"string","enum":["unthrottled"]},"benchmark_version":{"type":"string"},"web_vitals_version":{"type":"string"},"target":{"type":"object","additionalProperties":{}},"competitors":{"type":"object","additionalProperties":{}}},"required":["status","what_this_measures","measurement_type","device_profile","network_profile","cpu_profile","benchmark_version"],"additionalProperties":{},"description":"Deep Scan only. Head-to-head loading speed and visual stability of the target page and the top 3 organic competitor pages in the same SERP, measured under identical conditions. Lite and Standard scans never include this field. Deep scans MAY include it (optional — present when the benchmark produced a payload); when present, the `status` value indicates whether the run completed (`ok`) or was short-circuited at an entry gate (`disabled`, `skipped_not_deep`, `skipped_no_target_url`, `skipped_budget_exhausted`, `timeout`, `internal_error`). Clients must check for presence on Deep results and tolerate a present-with-skip-status response."},"schema_version":{"type":"string","enum":["onpage-report-customer-v1"]}},"required":["meta","on_page_optimization","benchmarks","entity_coverage","topic_and_classification","internal_linking","competitor_term_coverage","schema_version"],"additionalProperties":{},"description":"Terminal `customer_v1` payload for a completed Standard or Deep scan job. Fields live at the top level (no wrapper). `entity_coverage` includes related-entity density scores comparing the analyzed URL against the competitor cohort. The analysis body is additive — future fields will not break existing consumers. `schema_version` is a literal (`onpage-report-customer-v1`) so the jobResultResponseSchema union can discriminate on it."},{"type":"object","properties":{"meta":{"type":"object","properties":{"report_date":{"type":["string","null"]},"target_keyword":{"type":["string","null"]},"location":{"type":["string","null"]},"url":{"type":["string","null"]}},"required":["report_date","target_keyword","location","url"],"additionalProperties":false},"on_page_optimization":{"type":"object","properties":{"score":{"type":["integer","null"],"minimum":0,"maximum":100},"grade":{"type":"string","enum":["Excellent","Strong","Good","Needs Work","Under-optimized","Unavailable"]},"confidence":{"type":"number","minimum":0,"maximum":1},"summary":{"type":"string"},"focus_areas":{"type":"array","items":{"type":"string"}},"algorithm_version":{"type":"string","enum":["onpage-optimization-score-v1.1"]}},"required":["score","grade","confidence","summary","focus_areas","algorithm_version"],"additionalProperties":false,"description":"Customer-facing On-Page Optimization Score, computed from scan data already produced by the job. The proprietary algorithm exposes only the final score, grade, confidence, summary, and high-level focus areas."},"benchmarks":{"type":"object","additionalProperties":{}},"entity_coverage":{"type":"object","properties":{"your_url_related_entity_density_score":{"type":["number","null"],"description":"Weighted related-entity signal per 1,000 main-content words for the analyzed URL."},"competitor_related_entity_density_score":{"type":["number","null"],"description":"Average weighted related-entity signal per 1,000 main-content words across the competitor cohort."},"natural_language_entities":{"type":"array","items":{"type":"object","additionalProperties":{}}},"highly_related_terms":{"type":"array","items":{"type":"object","additionalProperties":{}}},"keyword_variations":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["your_url_related_entity_density_score","competitor_related_entity_density_score","natural_language_entities","highly_related_terms","keyword_variations"],"additionalProperties":false},"competitor_term_coverage":{"type":"object","additionalProperties":{}},"schema_version":{"type":"string","enum":["onpage-report-customer-v1-lite"]}},"required":["meta","on_page_optimization","benchmarks","entity_coverage","competitor_term_coverage","schema_version"],"additionalProperties":false,"description":"Terminal `customer_v1_lite` payload for a completed Lite scan job. Lite omits `topic_and_classification`, `internal_linking`, and the `related_category_entities` / `specific_category_entities` branches of `entity_coverage` because the Lite pipeline does not run NLP classification or internal-link discovery. Schema: `docs/customer-report-v1-lite/onpage-report-customer-v1-lite.schema.json`."}],"description":"Terminal payload for a completed job. The shape varies by job `type` (and for scans, by `depth`) but carries no top-level `type` discriminator — match against the endpoint you called. Scan shapes can be further distinguished by the literal `schema_version` field. The `legacy` format remains runtime-supported for existing callers but is intentionally undocumented."},"CreditsResponse":{"type":"object","properties":{"available_credits":{"type":"number","minimum":0},"reserved_credits":{"type":"number","minimum":0},"available_millicredits":{"type":"integer","minimum":0},"reserved_millicredits":{"type":"integer","minimum":0}},"required":["available_credits","reserved_credits","available_millicredits","reserved_millicredits"],"description":"Current ledger snapshot for the caller's org. Use `available_credits` and `reserved_credits`; the millicredit fields remain for existing integrations."},"RegionsResponse":{"type":"object","properties":{"defaultRegion":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"On-Page region code (mostly ISO-3166 alpha-2, with `UK` as the Google convention — mapped to Google `gb` / proxy `GB`). See GET /v1/regions.","example":"UK"},"regions":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"On-Page region code (mostly ISO-3166 alpha-2, with `UK` as the Google convention — mapped to Google `gb` / proxy `GB`). See GET /v1/regions.","example":"UK"},"name":{"type":"string"},"flagEmoji":{"type":"string"},"googleDomain":{"type":"string"},"googleGl":{"type":"string"},"defaultLocale":{"type":"string"}},"required":["code","name","flagEmoji","googleDomain","googleGl","defaultLocale"],"additionalProperties":false,"description":"Public metadata for a single supported region. `googleDomain` is informational only — crawl requests use `google.com` with the `gl` parameter rather than the country-specific domain."}}},"required":["defaultRegion","regions"],"additionalProperties":false,"description":"List of supported scan regions for this API. Static — `Cache-Control: public, max-age=86400`. Use the `code` from any entry as `region` in `POST /v1/scan` (and the lite/deep variants) and as `region` on the MCP scan tools."},"WebhookTestAccepted":{"type":"object","properties":{"delivery_id":{"type":"string"},"status":{"type":"string","enum":["queued"]},"signing_secret":{"type":"string"}},"required":["delivery_id","status","signing_secret"],"description":"202 response from `POST /v1/webhooks/test`. The delivery is asynchronous. `signing_secret` is the per-delivery secret integrators verify `OnPage-Signature` against."},"ValidationError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["VALIDATION_ERROR"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Request body failed schema validation (400)."},"AuthError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["INVALID_API_KEY"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Missing, malformed, or revoked API key (401)."},"ScopeError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["SCOPE_FORBIDDEN"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"API key is missing the scope required for this route (403)."},"InsufficientCredits":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["INSUFFICIENT_CREDITS"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"The caller's credit balance cannot cover the reservation (402)."},"IdempotencyConflict":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["IDEMPOTENCY_CONFLICT"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Same Idempotency-Key replayed with a different request body (409)."},"UrlNotAllowed":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["URL_NOT_ALLOWED"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"URL rejected by the SSRF / fetch-safety policy (422)."},"JobNotFound":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["NOT_FOUND"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Job ID is unknown to this org (404)."},"JobResultConflict":{"oneOf":[{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["RESULT_NOT_READY"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Job is not yet in a terminal state; poll status or retry after backoff (409)."},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["UNSUPPORTED_FORMAT"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Requested `response_format` is incompatible with the job (409). Emitted when a Lite scan is asked for `customer_v1` — use `customer_v1_lite` or omit the parameter."}],"description":"`GET /v1/jobs/{jobId}/result` 409 body. Branches on `error.code`: `RESULT_NOT_READY` when the job is still running, `UNSUPPORTED_FORMAT` when the requested `response_format` is incompatible with the job."},"InternalError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"requestId":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Unexpected server-side failure (500)."},"TooManyRequestsBody":{"oneOf":[{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["RATE_LIMITED"]},"message":{"type":"string"}},"required":["code","message"]},"retry_after_seconds":{"type":"number","minimum":0},"limit":{"type":"integer","exclusiveMinimum":0}},"required":["error","retry_after_seconds","limit"],"description":"Burst-rate limit hit. `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers accompany this body."},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["ORG_ACTIVE_LIMIT_REACHED"]},"message":{"type":"string"}},"required":["code","message"]},"limit":{"type":"integer","exclusiveMinimum":0},"observed":{"type":"integer","minimum":0}},"required":["error","limit","observed"],"description":"Org has reached its cap on actively-running jobs (combined across classify / scan). `X-Concurrency-Limit` carries `limit`; `X-Concurrency-Remaining: 0`. No `Retry-After` — retry after any active job completes (observe via webhooks or GET /v1/jobs/:id polling)."},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["ORG_QUEUED_LIMIT_REACHED"]},"message":{"type":"string"}},"required":["code","message"]},"limit":{"type":"integer","exclusiveMinimum":0},"observed":{"type":"integer","minimum":0}},"required":["error","limit","observed"],"description":"Org has too many jobs waiting in queue (combined across all job types). `X-Concurrency-Limit` carries `limit`; `X-Concurrency-Remaining: 0`. No `Retry-After` — retry after the queue drains."},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["SCAN_QUEUE_SATURATED"]},"message":{"type":"string"}},"required":["code","message"]},"retry_after_seconds":{"type":"number","minimum":0},"estimated_wait_seconds":{"type":"integer","minimum":0}},"required":["error","retry_after_seconds","estimated_wait_seconds"],"description":"Global scan queue saturated. `Retry-After` is always set for this variant; `estimated_wait_seconds` is advisory."}],"description":"429 body. Clients branch on `error.code`. Not every variant carries `Retry-After`; see the per-variant descriptions."},"JobCompletedWebhookBody":{"oneOf":[{"type":"object","properties":{"job_id":{"type":"string"},"api_key_id":{"type":"string"},"request_id":{"type":"string"},"type":{"type":"string","enum":["classify"]},"status":{"type":"string","enum":["completed"]},"completed_at":{"type":"string"},"timings":{"type":"object","properties":{"queue_wait_ms":{"type":"integer","minimum":0},"execution_ms":{"type":"integer","minimum":0},"total_wall_ms":{"type":"integer","minimum":0}},"required":["queue_wait_ms","execution_ms","total_wall_ms"]}},"required":["job_id","request_id","type","status","completed_at","timings"],"additionalProperties":false},{"type":"object","properties":{"job_id":{"type":"string"},"api_key_id":{"type":"string"},"request_id":{"type":"string"},"type":{"type":"string","enum":["scan"]},"callback_metadata":{"type":"object","additionalProperties":{}},"status":{"type":"string","enum":["completed"]},"completed_at":{"type":"string"},"timings":{"type":"object","properties":{"queue_wait_ms":{"type":"integer","minimum":0},"execution_ms":{"type":"integer","minimum":0},"total_wall_ms":{"type":"integer","minimum":0}},"required":["queue_wait_ms","execution_ms","total_wall_ms"]}},"required":["job_id","request_id","type","status","completed_at","timings"]}],"description":"Outbound delivery body for the `job.completed` event. `callback_metadata` only appears on scan jobs that received one on creation."},"JobFailedWebhookBody":{"oneOf":[{"type":"object","properties":{"job_id":{"type":"string"},"api_key_id":{"type":"string"},"request_id":{"type":"string"},"type":{"type":"string","enum":["classify"]},"status":{"type":"string","enum":["failed"]},"failed_at":{"type":"string"},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]},"attempt":{"type":"integer","exclusiveMinimum":0},"will_retry":{"type":"boolean"}},"required":["job_id","request_id","type","status","failed_at","error","attempt","will_retry"],"additionalProperties":false},{"type":"object","properties":{"job_id":{"type":"string"},"api_key_id":{"type":"string"},"request_id":{"type":"string"},"type":{"type":"string","enum":["scan"]},"callback_metadata":{"type":"object","additionalProperties":{}},"status":{"type":"string","enum":["failed"]},"failed_at":{"type":"string"},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]},"attempt":{"type":"integer","exclusiveMinimum":0},"will_retry":{"type":"boolean"}},"required":["job_id","request_id","type","status","failed_at","error","attempt","will_retry"]}],"description":"Outbound delivery body for the `job.failed` event. `attempt` is one-based; `will_retry` is false on terminal emissions."},"BillingTopupSucceededWebhookBody":{"type":"object","properties":{"tpaId":{"type":"string"},"paymentIntentId":{"type":"string"},"grantedMillicredits":{"type":"integer","minimum":0},"bonusMillicredits":{"type":"integer","minimum":0},"amountCents":{"type":"integer","minimum":0},"taxAmountCents":{"type":"integer","minimum":0}},"required":["tpaId","paymentIntentId","grantedMillicredits","bonusMillicredits","amountCents","taxAmountCents"],"additionalProperties":{},"description":"Outbound delivery body for `billing.topup_succeeded`. Field names are camelCase (historical). Additive: extra observability keys may appear."},"BillingTopupFailedWebhookBody":{"type":"object","properties":{"tpaId":{"type":"string"},"reason":{"type":"string"}},"required":["tpaId","reason"],"additionalProperties":{},"description":"Outbound delivery body for `billing.topup_failed`. Minimum fields are `tpaId` + `reason`; additional per-reason context (e.g. `pmStatus`, `paymentIntentId`, `nextAttemptAt`) may appear."},"BillingRefundFailedWebhookBody":{"type":"object","properties":{"refundId":{"type":"string"},"orgId":{"type":"string"},"packPurchaseId":{"type":["string","null"]},"amountCents":{"type":"integer"},"reason":{"type":["string","null"]}},"required":["refundId","orgId","amountCents","reason"],"description":"Outbound delivery body for `billing.refund.failed`."},"BillingDisputeCreatedWebhookBody":{"type":"object","properties":{"stripeDisputeId":{"type":"string"},"stripeChargeId":{"type":["string","null"]},"stripePaymentIntentId":{"type":["string","null"]},"amountCents":{"type":"integer"},"reason":{"type":["string","null"]},"status":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["stripeDisputeId","stripeChargeId","stripePaymentIntentId","amountCents","reason","status","createdAt"],"description":"Outbound delivery body for `billing.dispute.created`."},"BillingEarlyFraudWarningCreatedWebhookBody":{"type":"object","properties":{"stripeEarlyFraudWarningId":{"type":"string"},"stripeChargeId":{"type":["string","null"]},"stripePaymentIntentId":{"type":["string","null"]},"fraudType":{"type":["string","null"]},"actionable":{"type":["boolean","null"]}},"required":["stripeEarlyFraudWarningId","stripeChargeId","stripePaymentIntentId","fraudType","actionable"],"description":"Outbound delivery body for `billing.early_fraud_warning.created`."}},"parameters":{}},"paths":{"/v1/classify":{"post":{"operationId":"createClassifyJob","summary":"Submit text or a URL for topical classification.","tags":["NLP"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,255}$"},"required":false,"name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1},"url":{"type":"string","maxLength":2048,"format":"uri"},"languageHint":{"type":"string","minLength":2,"maxLength":16},"webhook":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"type":"string"},"default":["job.completed","job.failed"]}},"required":["url"]}},"description":"Provide at least one of `text` or `url`. Both may be supplied; if both are present, `text` takes precedence."}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptedJobResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCredits"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"409":{"description":"Idempotency conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyConflict"}}}},"422":{"description":"URL not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlNotAllowed"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/scan":{"post":{"operationId":"createScanJob","summary":"Standard SEO scan for a URL + keyword.","description":"Standard 2-credit SEO scan. Returns benchmarks, related-entity density scores, NLP-classified entities, internal-link recommendations, topical-authority questions, the competitor × entity cohort matrix, and a content brief — the full `customer_v1` shape.\n### Supported `region` values\n\nPass any `code` from this table as `region` to target that country's Google SERP. The full list is also available at `GET /v1/regions`. `googleDomain` is informational only — crawls hit `google.com` with the `gl` parameter rather than the country-specific domain.\n\n| code | name | google domain | gl | default locale |\n|------|------|---------------|----|----------------|\n| `US` | 🇺🇸 United States | `google.com` | `us` | `en-US` |\n| `CA` | 🇨🇦 Canada | `google.ca` | `ca` | `en-CA` |\n| `UK` | 🇬🇧 United Kingdom | `google.co.uk` | `gb` | `en-GB` |\n| `AU` | 🇦🇺 Australia | `google.com.au` | `au` | `en-AU` |\n| `NZ` | 🇳🇿 New Zealand | `google.co.nz` | `nz` | `en-NZ` |\n| `ES` | 🇪🇸 Spain | `google.es` | `es` | `es-ES` |\n| `DE` | 🇩🇪 Germany | `google.de` | `de` | `de-DE` |\n| `IT` | 🇮🇹 Italy | `google.it` | `it` | `it-IT` |\n| `FR` | 🇫🇷 France | `google.fr` | `fr` | `fr-FR` |\n| `IE` | 🇮🇪 Ireland | `google.ie` | `ie` | `en-IE` |\n| `NL` | 🇳🇱 Netherlands | `google.nl` | `nl` | `nl-NL` |\n| `CH` | 🇨🇭 Switzerland | `google.ch` | `ch` | `de-CH` |\n| `SE` | 🇸🇪 Sweden | `google.se` | `se` | `sv-SE` |\n| `NO` | 🇳🇴 Norway | `google.no` | `no` | `nb-NO` |\n| `DK` | 🇩🇰 Denmark | `google.dk` | `dk` | `da-DK` |\n| `FI` | 🇫🇮 Finland | `google.fi` | `fi` | `fi-FI` |\n| `ZA` | 🇿🇦 South Africa | `google.co.za` | `za` | `en-ZA` |\n| `MX` | 🇲🇽 Mexico | `google.com.mx` | `mx` | `es-MX` |\n| `BR` | 🇧🇷 Brazil | `google.com.br` | `br` | `pt-BR` |\n| `CO` | 🇨🇴 Colombia | `google.com.co` | `co` | `es-CO` |\n| `IN` | 🇮🇳 India | `google.co.in` | `in` | `en-IN` |\n| `SG` | 🇸🇬 Singapore | `google.com.sg` | `sg` | `en-SG` |\n| `MY` | 🇲🇾 Malaysia | `google.com.my` | `my` | `ms-MY` |\n| `JP` | 🇯🇵 Japan | `google.co.jp` | `jp` | `ja-JP` |\n| `KE` | 🇰🇪 Kenya | `google.co.ke` | `ke` | `en-KE` |\n| `AE` | 🇦🇪 UAE | `google.ae` | `ae` | `ar-AE` |\n| `HK` | 🇭🇰 Hong Kong | `google.com.hk` | `hk` | `zh-HK` |\n","tags":["Scan"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,255}$"},"required":false,"name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"keyword":{"type":"string","minLength":1,"maxLength":150,"description":"Primary search keyword. Whitespace is normalized. Must be a concise search query: max 150 characters."},"region":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"Region code. Case-insensitive and trimmed on input. Use canonical uppercase codes in response bodies. See GET /v1/regions.","example":"uk"},"market":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"DEPRECATED: use `region`. Accepted for backward compatibility; the API emits `Deprecation: true` + `Link` response headers when this field is used. Must match `region` if both are sent.","example":"uk","deprecated":true},"locale":{"type":"string","description":"BCP-47 locale tag. Canonicalized via Intl.Locale (`en_us` → `en-US`; `zh_hant_hk` → `zh-Hant-HK`). Max 16 chars.","example":"en-GB"},"compatibility_mode":{"type":"boolean","description":"Optional. Defaults to false. When true, allows budgeted browser rendering for difficult competitor pages. The target URL already receives automatic recovery when needed."},"webhook":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"type":"string"},"default":["job.completed","job.failed"]}},"required":["url"]},"callbackMetadata":{"type":"object","additionalProperties":{}}},"required":["keyword"],"description":"Scan a URL + keyword combo. `region` selects the Google SERP country (27 supported — see GET /v1/regions). `market` is a deprecated alias for `region`; sending both with different values returns `REGION_MISMATCH`. `compatibility_mode` can be set to true for slower, more tolerant competitor crawling; target URL recovery is automatic either way. `callbackMetadata` (if present) is echoed back in any webhook deliveries for this job."}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptedJobResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCredits"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"409":{"description":"Idempotency conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyConflict"}}}},"422":{"description":"URL not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlNotAllowed"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/scan/lite":{"post":{"operationId":"createLiteScanJob","summary":"Lite SEO scan — quick, focused on entity coverage + cohort analysis.","description":"Quick scan focused on entity coverage and competitor cohort analysis — ideal for fast audits and bulk workflows. Costs 1.5 credits vs 2 for `POST /v1/scan`. Lite skips NLP page classification, the internal-link lookup, and most OpenAI content-brief work — it still returns benchmarks, related-entity density scores, natural-language entities on the target + competitors, highly related terms, keyword variations, and the competitor × entity cohort matrix. Lite jobs produce the reduced `customer_v1_lite` response shape (see `schema://customer-report-v1-lite`); requesting `response_format=customer_v1` on a Lite result is rejected with a 409 `UNSUPPORTED_FORMAT`.\n### Supported `region` values\n\nPass any `code` from this table as `region` to target that country's Google SERP. The full list is also available at `GET /v1/regions`. `googleDomain` is informational only — crawls hit `google.com` with the `gl` parameter rather than the country-specific domain.\n\n| code | name | google domain | gl | default locale |\n|------|------|---------------|----|----------------|\n| `US` | 🇺🇸 United States | `google.com` | `us` | `en-US` |\n| `CA` | 🇨🇦 Canada | `google.ca` | `ca` | `en-CA` |\n| `UK` | 🇬🇧 United Kingdom | `google.co.uk` | `gb` | `en-GB` |\n| `AU` | 🇦🇺 Australia | `google.com.au` | `au` | `en-AU` |\n| `NZ` | 🇳🇿 New Zealand | `google.co.nz` | `nz` | `en-NZ` |\n| `ES` | 🇪🇸 Spain | `google.es` | `es` | `es-ES` |\n| `DE` | 🇩🇪 Germany | `google.de` | `de` | `de-DE` |\n| `IT` | 🇮🇹 Italy | `google.it` | `it` | `it-IT` |\n| `FR` | 🇫🇷 France | `google.fr` | `fr` | `fr-FR` |\n| `IE` | 🇮🇪 Ireland | `google.ie` | `ie` | `en-IE` |\n| `NL` | 🇳🇱 Netherlands | `google.nl` | `nl` | `nl-NL` |\n| `CH` | 🇨🇭 Switzerland | `google.ch` | `ch` | `de-CH` |\n| `SE` | 🇸🇪 Sweden | `google.se` | `se` | `sv-SE` |\n| `NO` | 🇳🇴 Norway | `google.no` | `no` | `nb-NO` |\n| `DK` | 🇩🇰 Denmark | `google.dk` | `dk` | `da-DK` |\n| `FI` | 🇫🇮 Finland | `google.fi` | `fi` | `fi-FI` |\n| `ZA` | 🇿🇦 South Africa | `google.co.za` | `za` | `en-ZA` |\n| `MX` | 🇲🇽 Mexico | `google.com.mx` | `mx` | `es-MX` |\n| `BR` | 🇧🇷 Brazil | `google.com.br` | `br` | `pt-BR` |\n| `CO` | 🇨🇴 Colombia | `google.com.co` | `co` | `es-CO` |\n| `IN` | 🇮🇳 India | `google.co.in` | `in` | `en-IN` |\n| `SG` | 🇸🇬 Singapore | `google.com.sg` | `sg` | `en-SG` |\n| `MY` | 🇲🇾 Malaysia | `google.com.my` | `my` | `ms-MY` |\n| `JP` | 🇯🇵 Japan | `google.co.jp` | `jp` | `ja-JP` |\n| `KE` | 🇰🇪 Kenya | `google.co.ke` | `ke` | `en-KE` |\n| `AE` | 🇦🇪 UAE | `google.ae` | `ae` | `ar-AE` |\n| `HK` | 🇭🇰 Hong Kong | `google.com.hk` | `hk` | `zh-HK` |\n","tags":["Scan"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,255}$"},"required":false,"name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"keyword":{"type":"string","minLength":1,"maxLength":150,"description":"Primary search keyword. Whitespace is normalized. Must be a concise search query: max 150 characters."},"region":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"Region code. Case-insensitive and trimmed on input. Use canonical uppercase codes in response bodies. See GET /v1/regions.","example":"uk"},"market":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"DEPRECATED: use `region`. Accepted for backward compatibility; the API emits `Deprecation: true` + `Link` response headers when this field is used. Must match `region` if both are sent.","example":"uk","deprecated":true},"locale":{"type":"string","description":"BCP-47 locale tag. Canonicalized via Intl.Locale (`en_us` → `en-US`; `zh_hant_hk` → `zh-Hant-HK`). Max 16 chars.","example":"en-GB"},"compatibility_mode":{"type":"boolean","description":"Optional. Defaults to false. When true, allows budgeted browser rendering for difficult competitor pages. The target URL already receives automatic recovery when needed."},"webhook":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"type":"string"},"default":["job.completed","job.failed"]}},"required":["url"]},"callbackMetadata":{"type":"object","additionalProperties":{}}},"required":["keyword"],"description":"Scan a URL + keyword combo. `region` selects the Google SERP country (27 supported — see GET /v1/regions). `market` is a deprecated alias for `region`; sending both with different values returns `REGION_MISMATCH`. `compatibility_mode` can be set to true for slower, more tolerant competitor crawling; target URL recovery is automatic either way. `callbackMetadata` (if present) is echoed back in any webhook deliveries for this job."}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptedJobResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCredits"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"409":{"description":"Idempotency conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyConflict"}}}},"422":{"description":"URL not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlNotAllowed"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/scan/deep":{"post":{"operationId":"createDeepScanJob","summary":"Deep SEO scan (heavier analysis path) for a URL + keyword.","description":"Deep 3-credit SEO scan with stronger competitor analysis: why a competitor outranks a URL, SERP/content-gap comparisons, related-entity density, common patterns across top-ranking pages, content briefs, and stronger strategic recommendations. Returns the full `customer_v1` shape, and Deep responses MAY also include the optional `serp_speed_benchmark` field — head-to-head loading speed and visual stability of the target page and the top 3 organic competitors in the same SERP (LCP, CLS, approximate TBT, TTFB). Self-hosted; never calls Google PageSpeed Insights, CrUX, or Lighthouse. Lite and Standard responses never include this field; clients must check for presence on Deep results.\n### Supported `region` values\n\nPass any `code` from this table as `region` to target that country's Google SERP. The full list is also available at `GET /v1/regions`. `googleDomain` is informational only — crawls hit `google.com` with the `gl` parameter rather than the country-specific domain.\n\n| code | name | google domain | gl | default locale |\n|------|------|---------------|----|----------------|\n| `US` | 🇺🇸 United States | `google.com` | `us` | `en-US` |\n| `CA` | 🇨🇦 Canada | `google.ca` | `ca` | `en-CA` |\n| `UK` | 🇬🇧 United Kingdom | `google.co.uk` | `gb` | `en-GB` |\n| `AU` | 🇦🇺 Australia | `google.com.au` | `au` | `en-AU` |\n| `NZ` | 🇳🇿 New Zealand | `google.co.nz` | `nz` | `en-NZ` |\n| `ES` | 🇪🇸 Spain | `google.es` | `es` | `es-ES` |\n| `DE` | 🇩🇪 Germany | `google.de` | `de` | `de-DE` |\n| `IT` | 🇮🇹 Italy | `google.it` | `it` | `it-IT` |\n| `FR` | 🇫🇷 France | `google.fr` | `fr` | `fr-FR` |\n| `IE` | 🇮🇪 Ireland | `google.ie` | `ie` | `en-IE` |\n| `NL` | 🇳🇱 Netherlands | `google.nl` | `nl` | `nl-NL` |\n| `CH` | 🇨🇭 Switzerland | `google.ch` | `ch` | `de-CH` |\n| `SE` | 🇸🇪 Sweden | `google.se` | `se` | `sv-SE` |\n| `NO` | 🇳🇴 Norway | `google.no` | `no` | `nb-NO` |\n| `DK` | 🇩🇰 Denmark | `google.dk` | `dk` | `da-DK` |\n| `FI` | 🇫🇮 Finland | `google.fi` | `fi` | `fi-FI` |\n| `ZA` | 🇿🇦 South Africa | `google.co.za` | `za` | `en-ZA` |\n| `MX` | 🇲🇽 Mexico | `google.com.mx` | `mx` | `es-MX` |\n| `BR` | 🇧🇷 Brazil | `google.com.br` | `br` | `pt-BR` |\n| `CO` | 🇨🇴 Colombia | `google.com.co` | `co` | `es-CO` |\n| `IN` | 🇮🇳 India | `google.co.in` | `in` | `en-IN` |\n| `SG` | 🇸🇬 Singapore | `google.com.sg` | `sg` | `en-SG` |\n| `MY` | 🇲🇾 Malaysia | `google.com.my` | `my` | `ms-MY` |\n| `JP` | 🇯🇵 Japan | `google.co.jp` | `jp` | `ja-JP` |\n| `KE` | 🇰🇪 Kenya | `google.co.ke` | `ke` | `en-KE` |\n| `AE` | 🇦🇪 UAE | `google.ae` | `ae` | `ar-AE` |\n| `HK` | 🇭🇰 Hong Kong | `google.com.hk` | `hk` | `zh-HK` |\n","tags":["Scan"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,255}$"},"required":false,"name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"keyword":{"type":"string","minLength":1,"maxLength":150,"description":"Primary search keyword. Whitespace is normalized. Must be a concise search query: max 150 characters."},"region":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"Region code. Case-insensitive and trimmed on input. Use canonical uppercase codes in response bodies. See GET /v1/regions.","example":"uk"},"market":{"type":"string","enum":["US","CA","UK","AU","NZ","ES","DE","IT","FR","IE","NL","CH","SE","NO","DK","FI","ZA","MX","BR","CO","IN","SG","MY","JP","KE","AE","HK"],"description":"DEPRECATED: use `region`. Accepted for backward compatibility; the API emits `Deprecation: true` + `Link` response headers when this field is used. Must match `region` if both are sent.","example":"uk","deprecated":true},"locale":{"type":"string","description":"BCP-47 locale tag. Canonicalized via Intl.Locale (`en_us` → `en-US`; `zh_hant_hk` → `zh-Hant-HK`). Max 16 chars.","example":"en-GB"},"compatibility_mode":{"type":"boolean","description":"Optional. Defaults to false. When true, allows budgeted browser rendering for difficult competitor pages. The target URL already receives automatic recovery when needed."},"webhook":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"type":"string"},"default":["job.completed","job.failed"]}},"required":["url"]},"callbackMetadata":{"type":"object","additionalProperties":{}}},"required":["keyword"],"description":"Scan a URL + keyword combo. `region` selects the Google SERP country (27 supported — see GET /v1/regions). `market` is a deprecated alias for `region`; sending both with different values returns `REGION_MISMATCH`. `compatibility_mode` can be set to true for slower, more tolerant competitor crawling; target URL recovery is automatic either way. `callbackMetadata` (if present) is echoed back in any webhook deliveries for this job."}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptedJobResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCredits"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"409":{"description":"Idempotency conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyConflict"}}}},"422":{"description":"URL not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlNotAllowed"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/jobs/{jobId}":{"get":{"operationId":"getJob","summary":"Fetch lightweight job status and error state.","tags":["Jobs"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"Job status snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobNotFound"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/jobs/{jobId}/result":{"get":{"operationId":"getJobResult","summary":"Fetch the terminal result payload for a completed job.","description":"Returns the formatted result payload. Shape varies by job type and (for scans) by depth:\n- classify / scan (standard or deep) → `customer_v1` by default.\n- Lite scans → `customer_v1_lite` by default (reduced shape, see `schema://customer-report-v1-lite`).\n\nRequesting `response_format=customer_v1` on a Lite scan (or `customer_v1_lite` on a non-Lite scan) is rejected with a 409 `UNSUPPORTED_FORMAT`. Call `GET /v1/jobs/{jobId}` first to confirm the job is terminal.","tags":["Jobs"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"jobId","in":"path"},{"schema":{"type":"string","enum":["customer_v1","customer_v1_lite","legacy"]},"required":false,"name":"response_format","in":"query"}],"responses":{"200":{"description":"Terminal result payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResultResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobNotFound"}}}},"409":{"description":"Either the job is not yet in a terminal state (`RESULT_NOT_READY`) or the requested `response_format` is incompatible with the job (`UNSUPPORTED_FORMAT`, e.g. `customer_v1` asked on a Lite scan).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResultConflict"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/credits":{"get":{"operationId":"getCredits","summary":"Current ledger balance for the caller's org.","tags":["Credits"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Credit balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/regions":{"get":{"operationId":"getRegions","summary":"List of supported scan regions for `region` on POST /v1/scan*.","description":"Public, unauthenticated, not credit-billed. Static — clients should cache responses for a day (`Cache-Control: public, max-age=86400`). Use the `code` from any entry as `region` on `POST /v1/scan` (and the lite/deep variants) and on the MCP scan tools. `googleDomain` is informational only — crawls hit `google.com` with the `gl` parameter rather than the country-specific domain.","tags":["Regions"],"responses":{"200":{"description":"Supported regions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegionsResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalError"}}}}}}},"/v1/webhooks/test":{"post":{"operationId":"testWebhook","summary":"Enqueue a signed test webhook delivery to a URL.","description":"Useful for verifying your endpoint's HMAC-SHA256 handling before linking it to real events. IP-rate-limited (independent of per-key burst).","tags":["Webhooks"],"security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"event":{"type":"string","enum":["job.completed","job.failed","billing.topup_succeeded","billing.topup_failed"],"default":"job.completed"}},"required":["url"],"description":"Send a signed test webhook delivery to the supplied URL. Useful for verifying your endpoint's HMAC handling before linking it."}}}},"responses":{"202":{"description":"Test delivery queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestAccepted"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"403":{"description":"Scope forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScopeError"}}}},"422":{"description":"URL not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlNotAllowed"}}}},"429":{"description":"Too many requests. Body is a `oneOf` of rate-limit / org-active-limit / org-queued-limit / scan-queue-saturated variants — branch on `error.code`. `Retry-After` is present when the limiter emits it; `X-RateLimit-*` is present on rate-limit-middleware 429s; `X-Concurrency-*` is present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED).","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"required":false,"description":"Seconds until the caller should retry. Present when the limiter emits it (rate-limit middleware; scan admission when retry_after_seconds is computed)."},"X-RateLimit-Limit":{"schema":{"type":"string","description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"required":false,"description":"The rate-limit ceiling applied to the caller. Present on rate-limit-middleware 429s."},"X-RateLimit-Remaining":{"schema":{"type":"string","description":"Remaining requests in the current window."},"required":false,"description":"Remaining requests in the current window."},"X-RateLimit-Reset":{"schema":{"type":"string","description":"Unix seconds when the rate-limit window resets."},"required":false,"description":"Unix seconds when the rate-limit window resets."},"X-Concurrency-Limit":{"schema":{"type":"string","description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"required":false,"description":"Cap that the caller just hit. Present on concurrency-limit 429s (ORG_ACTIVE_LIMIT_REACHED / ORG_QUEUED_LIMIT_REACHED). The body's `limit` field carries the same value."},"X-Concurrency-Remaining":{"schema":{"type":"string","description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."},"required":false,"description":"Remaining in-flight capacity. Zero when the caller just hit the ceiling; retry after existing jobs drain (observe via webhooks or GET /v1/jobs/:id polling)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooManyRequestsBody"}}}}}}}},"webhooks":{"jobCompleted":{"post":{"operationId":"onJobCompleted","summary":"Fired when a job reaches the `completed` terminal state.","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCompletedWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}},"jobFailed":{"post":{"operationId":"onJobFailed","summary":"Fired when a job reaches the `failed` terminal state.","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFailedWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}},"billingTopupSucceeded":{"post":{"operationId":"onBillingTopupSucceeded","summary":"Auto-topup purchase succeeded.","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingTopupSucceededWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}},"billingTopupFailed":{"post":{"operationId":"onBillingTopupFailed","summary":"Auto-topup purchase failed (transient or hard).","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingTopupFailedWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}},"billingRefundFailed":{"post":{"operationId":"onBillingRefundFailed","summary":"A Stripe refund attempt failed.","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingRefundFailedWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}},"billingDisputeCreated":{"post":{"operationId":"onBillingDisputeCreated","summary":"A Stripe chargeback/dispute opened on an org's pack purchase.","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingDisputeCreatedWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}},"billingEarlyFraudWarningCreated":{"post":{"operationId":"onBillingEarlyFraudWarningCreated","summary":"Stripe raised an Early Fraud Warning on a charge.","tags":["Webhooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"OnPage-Event","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Delivery-Id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"OnPage-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingEarlyFraudWarningCreatedWebhookBody"}}}},"responses":{"200":{"description":"Ack. Any 2xx is accepted."}}}}}}