{"openapi":"3.1.0","paths":{"/v1/estimate":{"post":{"description":"Returns the per-unit rate, whether the site needs the protected tier, and a page estimate read from the sitemap when there is one. Starts nothing and spends no credits.","operationId":"PublicJobsController_estimate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateRequestDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateResponseDto"}}}}},"security":[{"api-key":[]}],"summary":"Price a run without starting one","tags":["CrawlHawk API v1"]}},"/v1/jobs":{"post":{"operationId":"PublicJobsController_create","parameters":[{"name":"Idempotency-Key","in":"header","description":"Send a unique value per intended job. A retry with the same key and body replays the original response for 24 hours instead of starting — and charging for — a second run.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobDto"}}}},"responses":{"202":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAcceptedDto"}}}},"402":{"description":"Not enough credits. The body carries the exact `required` and `available` amounts and a `top_up_url`."}},"security":[{"api-key":[]}],"summary":"Start a run","tags":["CrawlHawk API v1"]},"get":{"operationId":"PublicJobsController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobListDto"}}}}},"security":[{"api-key":[]}],"summary":"List the account's runs","tags":["CrawlHawk API v1"]}},"/v1/jobs/{id}":{"get":{"operationId":"PublicJobsController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicJobDto"}}}}},"security":[{"api-key":[]}],"summary":"Read a run’s status","tags":["CrawlHawk API v1"]}},"/v1/jobs/{id}/confirm":{"post":{"description":"Resolves both machine-side stops: a protected-tier escalation waiting on `awaiting_confirmation`, and a run parked on a batch boundary (`{\"continue_batch\": true}`).","operationId":"PublicJobsController_confirm","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmJobDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicJobDto"}}}}},"security":[{"api-key":[]}],"summary":"Let a stopped run continue","tags":["CrawlHawk API v1"]}},"/v1/jobs/{id}/abort":{"post":{"operationId":"PublicJobsController_abort","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicJobDto"}}}}},"security":[{"api-key":[]}],"summary":"Stop a run","tags":["CrawlHawk API v1"]}},"/v1/jobs/{id}/results":{"get":{"description":"The shape follows the mode. This is the endpoint an agent should read; `export` exists for handing a file to a human.","operationId":"PublicJobsController_results","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResultsDto"}}}}},"security":[{"api-key":[]}],"summary":"Read a run’s results as paged JSON","tags":["CrawlHawk API v1"]}},"/v1/jobs/{id}/export":{"post":{"description":"Returns a signed, single-use URL. The URL carries no API key, so it can be passed on to whatever needs the file.","operationId":"PublicJobsController_export","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExportDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportUrlDto"}}}}},"security":[{"api-key":[]}],"summary":"Mint a short-lived download URL","tags":["CrawlHawk API v1"]}},"/v1/account":{"get":{"operationId":"PublicAccountController_account","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDto"}}}}},"security":[{"api-key":[]}],"summary":"Credit balance, key metadata and the documented ceilings","tags":["CrawlHawk API v1"]}},"/v1/webhooks":{"get":{"operationId":"PublicWebhooksController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDto"}}}}}},"security":[{"api-key":[]}],"summary":"List the account’s webhook endpoints","tags":["CrawlHawk API v1"]},"post":{"description":"The signing secret is returned once, here. Every delivery carries an X-CrawlHawk-Signature of `t=<unix>,v1=<hmac-sha256 of \"t.body\">` computed with it.","operationId":"PublicWebhooksController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhookDto"}}}}},"security":[{"api-key":[]}],"summary":"Register a webhook endpoint","tags":["CrawlHawk API v1"]}},"/v1/webhooks/{id}":{"delete":{"operationId":"PublicWebhooksController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"api-key":[]}],"summary":"Remove a webhook endpoint","tags":["CrawlHawk API v1"]}},"/api-keys":{"get":{"operationId":"ApiKeysController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountKeyDto"}}}}}},"security":[{"bearer":[]}],"summary":"List the account’s API keys (never the secrets)","tags":["API keys"]},"post":{"description":"The secret is returned exactly once — we store only its SHA-256. Keys are prefixed `chk_live_` / `chk_test_` so a leaked one is recognisable to secret scanners.","operationId":"ApiKeysController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKeyDto"}}}}},"security":[{"bearer":[]}],"summary":"Mint a key","tags":["API keys"]}},"/api-keys/{id}":{"delete":{"operationId":"ApiKeysController_revoke","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountKeyDto"}}}}},"security":[{"bearer":[]}],"summary":"Revoke a key","tags":["API keys"]}}},"info":{"title":"CrawlHawk API","description":"One engine, five surfaces. The web app, the MCP server, the Apify actors,\nthe n8n node and any direct integration all call this same `/v1`.\n\n**Auth** — `Authorization: Bearer chk_live_…`. Mint keys from your account;\nthe secret is shown once and stored only as a hash.\n\n**Protected sites** — some sites sit behind bot protection and cost more per URL.\nThere is no modal to click out here, so the decision is pre-authorised on the job:\n`on_protected` plus `max_credit_per_unit`.\n\n**Long runs** — crawls take minutes. Register a webhook rather than polling;\nevery delivery is HMAC-signed and retried with exponential backoff.","version":"1.0.0","contact":{}},"tags":[],"servers":[{"url":"https://api.crawlhawk.com"}],"components":{"securitySchemes":{"api-key":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"A CrawlHawk API key, e.g. `chk_live_…`."}},"schemas":{"EstimateRequestDto":{"type":"object","properties":{"url":{"type":"string"},"mode":{"type":"string","enum":["links","sitemap","products","markdown","contacts"]},"domain_count":{"type":"integer","description":"`contacts` only: how many domains the run would cover, when you already know the list size."}},"required":["url","mode"]},"EstimateResponseDto":{"type":"object","properties":{"url":{"type":"string"},"mode":{"type":"string","enum":["links","sitemap","products","markdown","contacts"]},"credit_per_unit":{"type":"integer","description":"Credits per unit — per URL, per product, or per domain."},"credit_per_1000":{"type":"integer"},"protected":{"type":"boolean","description":"True when the site is behind a bot-protection tier, which is what makes the rate jump."},"requires_protected_tier":{"type":"boolean","description":"True when the run needs the protected tier — start it with `on_protected` set deliberately."},"protected_marker":{"type":"string","nullable":true,"description":"What flagged the protection (WAF marker, HTTP status)."},"estimated_pages":{"type":"integer","nullable":true,"description":"Pages we expect to fetch, read from the sitemap when the site publishes one. Null when it cannot be established cheaply."},"estimated_credits":{"type":"integer","nullable":true,"description":"`estimated_pages` × `credit_per_unit`. Null when the page count is unknown."},"credits_available":{"type":"integer","description":"The key owner's current balance."}},"required":["url","mode","credit_per_unit","credit_per_1000","protected","requires_protected_tier","credits_available"]},"JobScopeDto":{"type":"object","properties":{"one_page":{"type":"boolean","description":"Crawl only the given URL, nothing linked from it."},"specific_route":{"type":"boolean","description":"Stay under the path of the start URL."},"include_subdomains":{"type":"boolean"},"include_external":{"type":"boolean"},"include_broken":{"type":"boolean"},"include_orphan":{"type":"boolean"},"include_images":{"type":"boolean"},"include_files":{"type":"boolean"},"include_sitemap":{"type":"boolean"},"include_query_strings":{"type":"boolean","description":"Treat `?a=1` variants as distinct URLs. Off by default — most sites would otherwise crawl forever."}}},"JobOptionsDto":{"type":"object","properties":{"preferred_language":{"type":"string","description":"Keep only pages in this language (e.g. \"en\", \"de\"). Auto-detected when omitted."},"all_languages":{"type":"boolean","description":"Products mode: keep every language variant instead of collapsing to one."},"region_aware":{"type":"boolean","description":"Compare region tags too, so \"en-GB\" and \"en-US\" differ."},"strict_default_language":{"type":"boolean","description":"Treat URLs with no language marker as default-language leaks. Off by default."},"sitemap_first":{"type":"boolean","description":"Seed the crawl from the sitemap instead of following links. Falls back to link-following when there is no sitemap."},"discovery":{"type":"boolean","default":true},"use_template_extraction":{"type":"boolean","default":true,"description":"Products mode: reuse a learned per-site extraction template."},"suppress_contact":{"type":"boolean","default":true,"description":"Products mode: strip seller contact details from the description."},"collect_hidden_prices":{"type":"boolean","default":false,"description":"Products mode: keep prices hidden behind a \"price on request\" flow."}}},"JobExternalDto":{"type":"object","properties":{"user_id":{"type":"string","description":"The calling channel's own user identifier (Apify user, n8n owner, …). Stored on the run and readable in diagnostics."},"run_id":{"type":"string","description":"The calling channel's own run identifier."}}},"CreateJobDto":{"type":"object","properties":{"url":{"type":"string","description":"Start URL. Required for every mode except `contacts`, where `domains` may be used instead."},"mode":{"type":"string","enum":["links","sitemap","products","markdown","contacts"],"description":"`links` (URL inventory), `sitemap` (XML sitemap), `products` (structured product data), `markdown` (page text for RAG), `contacts` (company/person contact data)."},"domains":{"description":"`contacts` only: the domains to process.","type":"array","items":{"type":"string"}},"scope":{"$ref":"#/components/schemas/JobScopeDto"},"options":{"$ref":"#/components/schemas/JobOptionsDto"},"on_protected":{"type":"string","enum":["auto","pause","fail"],"default":"auto","description":"What to do when the site turns out to be bot-protected and the rate jumps. `auto`: continue while the rate stays at or under `max_credit_per_unit`. `pause`: stop at `awaiting_confirmation` and wait for POST /v1/jobs/{id}/confirm. `fail`: end the run rather than pay more."},"max_credit_per_unit":{"type":"integer","description":"Credit ceiling per URL that `on_protected: auto` may accept without asking. Required when `on_protected` is `auto`."},"legal_basis_accepted":{"type":"boolean","description":"`contacts` only, and mandatory there: the caller confirms a lawful basis for processing the contact data this run collects (Terms 5.7). The acceptance is recorded against the key owner and the call time."},"external":{"$ref":"#/components/schemas/JobExternalDto"}},"required":["mode"]},"JobAcceptedDto":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["queued","running","awaiting_confirmation","paused","completed","failed","aborted"]},"mode":{"type":"string","enum":["links","sitemap","products","markdown","contacts"]},"url":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}},"required":["job_id","status","mode","created_at"]},"JobCountsDto":{"type":"object","properties":{"pages":{"type":"integer","description":"URLs discovered on the site so far."},"products":{"type":"integer","description":"Pages classified as products (`products` mode)."},"results":{"type":"integer","description":"Results ready to read from /results — extracted products, or contact records."}},"required":["pages","products","results"]},"JobErrorDto":{"type":"object","properties":{"code":{"type":"string","enum":["server_error","provider_down","pipeline_bug","rate_limited","invalid_url","url_not_resolved","blacklisted_domain","target_blocked","no_data","partial_target_failure","user_stopped","sample_stopped","js_render_required","protected_rate_refused","credits_exhausted","stale_purged","schedule_previous_run_in_progress","schedule_missed_window","schedule_start_failed","unknown"],"description":"Why the run ended, from a closed list. A reason we cannot classify arrives as `unknown` rather than as raw text."},"message":{"type":"string"}},"required":["code","message"]},"PublicJobDto":{"type":"object","properties":{"job_id":{"type":"string"},"mode":{"type":"string","enum":["links","sitemap","products","markdown","contacts"]},"status":{"type":"string","enum":["queued","running","awaiting_confirmation","paused","completed","failed","aborted"]},"url":{"type":"string","nullable":true},"counts":{"$ref":"#/components/schemas/JobCountsDto"},"credits_used":{"type":"integer","description":"Credits this run has consumed so far."},"credit_per_unit":{"type":"integer","nullable":true,"description":"The per-unit rate in force. Set once the run knows whether the site is protected."},"protected":{"type":"boolean","description":"Whether the run is being charged at the protected tier."},"batch":{"type":"integer","description":"The batch currently being crawled. 1 for a single-batch run."},"batch_ready":{"type":"boolean","description":"True when the run stopped on a batch boundary with more of the site left. Continue it with POST /v1/jobs/{id}/confirm {\"continue_batch\": true}."},"next_batch":{"type":"integer","nullable":true,"description":"The batch a confirm would start. Null when the run is not waiting on a boundary."},"external":{"type":"object","nullable":true,"additionalProperties":true,"description":"The `external` object supplied when the job was created."},"source":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","nullable":true,"format":"date-time"},"error":{"nullable":true,"type":"object","allOf":[{"$ref":"#/components/schemas/JobErrorDto"}]}},"required":["job_id","mode","status","counts","credits_used","protected","batch","batch_ready","source","created_at"]},"JobListDto":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PublicJobDto"}},"total":{"type":"integer"},"page":{"type":"integer"},"page_size":{"type":"integer"}},"required":["items","total","page","page_size"]},"ConfirmJobDto":{"type":"object","properties":{"continue_batch":{"type":"boolean","description":"Continue past a batch boundary (`batch.ready` is true) instead of confirming a protected-tier escalation."},"max_credit_per_unit":{"type":"integer","description":"Raise the run's per-URL credit ceiling as part of confirming, so a later escalation does not stop it again."}}},"JobResultsDto":{"type":"object","properties":{"job_id":{"type":"string"},"mode":{"type":"string"},"items":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Shape depends on the mode: `links`/`sitemap` return `{url, title, type, status_code}`, `products` return the structured product record, `markdown` returns `{url, title, markdown}`, `contacts` return one record per domain with its people."},"total":{"type":"integer"},"page":{"type":"integer"},"page_size":{"type":"integer"},"has_more":{"type":"boolean","description":"True when another page of results follows."}},"required":["job_id","mode","items","total","page","page_size"]},"CreateExportDto":{"type":"object","properties":{"format":{"type":"string","enum":["xlsx","csv","json","md","xml"]}},"required":["format"]},"ExportUrlDto":{"type":"object","properties":{"url":{"type":"string","description":"Short-lived, signed download URL. Single use — fetch it straight away."},"format":{"type":"string","enum":["xlsx","csv","json","md","xml"]},"expires_at":{"type":"string","format":"date-time"}},"required":["url","format","expires_at"]},"AccountKeyDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string","description":"chk_live / chk_test."},"last4":{"type":"string","description":"Last 4 characters of the key."},"source":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","nullable":true,"format":"date-time"},"revoked_at":{"type":"string","nullable":true,"format":"date-time"}},"required":["id","name","prefix","last4","source","created_at"]},"AccountDto":{"type":"object","properties":{"account_id":{"type":"string"},"credits":{"type":"integer","description":"Credits available to spend."},"top_up_url":{"type":"string","description":"Where to buy more when the balance runs out."},"key":{"nullable":true,"description":"Metadata for the key this request authenticated with. Null when the caller is a connected application using an OAuth access token.","type":"object","allOf":[{"$ref":"#/components/schemas/AccountKeyDto"}]},"auth_method":{"enum":["api_key","oauth"],"type":"string","description":"How this request authenticated."},"scopes":{"description":"What the caller is allowed to do. Empty for an API key, which carries the whole account.","type":"array","items":{"type":"string"}},"keys_active":{"type":"integer","description":"Live keys on the account."},"webhook_failing":{"type":"boolean","description":"True when a registered webhook has exhausted its retries and is not being delivered to."},"limits":{"type":"object","additionalProperties":true,"description":"The documented ceilings that protect the infrastructure."}},"required":["account_id","credits","top_up_url","key","auth_method","scopes","keys_active","webhook_failing","limits"]},"WebhookDto":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string","enum":["job.completed","job.failed","job.awaiting_confirmation","job.batch_ready"]}},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"last_success_at":{"type":"string","nullable":true,"format":"date-time"},"failing_since":{"type":"string","nullable":true,"format":"date-time"},"last_error":{"type":"string","nullable":true}},"required":["id","url","events","active","created_at"]},"CreateWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"HTTPS endpoint to deliver to. Private and loopback addresses are refused."},"events":{"type":"array","description":"Events to subscribe to. Every event when omitted.","items":{"type":"string","enum":["job.completed","job.failed","job.awaiting_confirmation","job.batch_ready"]}}},"required":["url"]},"CreatedWebhookDto":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string","enum":["job.completed","job.failed","job.awaiting_confirmation","job.batch_ready"]}},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"last_success_at":{"type":"string","nullable":true,"format":"date-time"},"failing_since":{"type":"string","nullable":true,"format":"date-time"},"last_error":{"type":"string","nullable":true},"secret":{"type":"string","description":"Signing secret — shown once, at creation. Verify X-CrawlHawk-Signature with it."}},"required":["id","url","events","active","created_at","secret"]},"CreateApiKeyDto":{"type":"object","properties":{"name":{"type":"string","description":"Label shown in the key list, e.g. \"n8n production\"."},"source":{"type":"string","enum":["direct","mcp","apify","n8n","factoryzone"],"default":"direct","description":"Which surface will call with this key. Stamped onto every run it starts, so revenue and usage can be read per channel."},"test":{"type":"boolean","description":"Mint a `chk_test_` key instead of `chk_live_`."}}},"CreatedApiKeyDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string","description":"chk_live / chk_test."},"last4":{"type":"string","description":"Last 4 characters of the key."},"source":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","nullable":true,"format":"date-time"},"revoked_at":{"type":"string","nullable":true,"format":"date-time"},"key":{"type":"string","description":"The secret, shown exactly once. We store only its SHA-256, so it cannot be recovered."}},"required":["id","name","prefix","last4","source","created_at","key"]}}}}