{"openapi":"3.0.0","paths":{"/v1/workspaces/{workspace_id}/webhooks":{"post":{"operationId":"WebhookController_create","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Stripe-style idempotency key. The signing secret is returned exactly once; a retry without this header creates a second webhook + secret. 24h TTL.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookDto"}}}},"responses":{"201":{"description":"Webhook created. Includes plaintext `signing_secret` exactly once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Create a workspace webhook. The response includes the signing secret in clear EXACTLY ONCE — copy it now.","tags":["webhooks"]},"get":{"operationId":"WebhookController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List webhooks for the workspace (secrets masked)","tags":["webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/{id}":{"get":{"operationId":"WebhookController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponseDto"}}}},"404":{"description":"Webhook not found."}},"security":[{"bearer":[]}],"summary":"Get a webhook (secret masked)","tags":["webhooks"]},"patch":{"operationId":"WebhookController_update","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update url / active flag (active=true clears auto-disable)","tags":["webhooks"]},"delete":{"operationId":"WebhookController_remove","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a webhook","tags":["webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/{id}/deliveries":{"get":{"operationId":"WebhookController_recentDeliveries","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Recent delivery attempts."}},"security":[{"bearer":[]}],"summary":"Recent delivery attempts for a webhook (default 50, max 200, newest first)","tags":["webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secret":{"post":{"operationId":"WebhookController_rotateSecret","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Rotate the signing secret. Returns the new secret in clear EXACTLY ONCE.","tags":["webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/{id}/test":{"post":{"operationId":"WebhookController_fireTest","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"202":{"description":""}},"security":[{"bearer":[]}],"summary":"Fire a synthetic event to this webhook (asynchronous; check the destination logs)","tags":["webhooks"]}},"/v1/workspaces/{workspace_id}/jobs":{"post":{"operationId":"JobController_create","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Pass a key (1..256 chars, [A-Za-z0-9_\\-:]) to make retries safe — replays return the original response for 24h.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobDto"}}}},"responses":{"201":{"description":"Job created and queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponseDto"}}}},"400":{"description":"Invalid JSON Schema (must be a non-empty `type: object`)."},"403":{"description":"Caller lacks `trawl.create`, or workspace has not enabled the `trawl` service."},"429":{"description":"Rate limited. `RATE_LIMITED` — submission window exceeded (default 10/min per workspace); the `Retry-After` header gives the seconds until the window resets. `TOO_MANY_ACTIVE_JOBS` — too many queued/running jobs (default 25); retry once some finish."}},"security":[{"bearer":[]}],"summary":"Create (enqueue) a trawl job","tags":["jobs"]},"get":{"operationId":"JobController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by job status.","schema":{"enum":["queued","running","succeeded","failed","cancelled"],"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor for pagination.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}}],"responses":{"200":{"description":"Page of jobs with pagination cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobListResponseDto"}}}},"403":{"description":"Caller lacks `trawl.list`, or workspace has not enabled the `trawl` service."}},"security":[{"bearer":[]}],"summary":"List trawl jobs (paginated, newest first)","tags":["jobs"]}},"/v1/workspaces/{workspace_id}/jobs/{id}":{"get":{"operationId":"JobController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Job row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponseDto"}}}},"404":{"description":"Job not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Get a single trawl job","tags":["jobs"]}},"/v1/workspaces/{workspace_id}/jobs/{id}/cancel":{"post":{"operationId":"JobController_cancel","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Job cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponseDto"}}}},"404":{"description":"Job not found in this workspace."},"422":{"description":"Job is already in a terminal state and cannot be cancelled."}},"security":[{"bearer":[]}],"summary":"Cancel a queued or running trawl job","tags":["jobs"]}}},"info":{"title":"Trawl API","description":"Trawl service for the ProductCraft platform","version":"0.1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"CreateWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"Destination URL (https required in prod)."},"active":{"type":"boolean","description":"Disabled webhooks are never delivered to."}},"required":["url"]},"WebhookCreateResponseDto":{"type":"object","properties":{"id":{"type":"string"},"workspace_id":{"type":"string"},"url":{"type":"string"},"active":{"type":"boolean"},"failure_count":{"type":"number"},"last_status_code":{"type":"number","nullable":true},"last_attempt_at":{"type":"string","nullable":true},"auto_disabled_at":{"type":"string","nullable":true},"first_failure_at":{"type":"string","nullable":true},"created_at":{"type":"string"},"signing_secret":{"type":"string","description":"Plaintext signing secret returned EXACTLY ONCE on create + rotate. Persist immediately — it never appears on subsequent GETs."}},"required":["id","workspace_id","url","active","failure_count","last_status_code","last_attempt_at","auto_disabled_at","first_failure_at","created_at","signing_secret"]},"WebhookResponseDto":{"type":"object","properties":{"id":{"type":"string"},"workspace_id":{"type":"string"},"url":{"type":"string"},"active":{"type":"boolean"},"failure_count":{"type":"number"},"last_status_code":{"type":"number","nullable":true},"last_attempt_at":{"type":"string","nullable":true},"auto_disabled_at":{"type":"string","nullable":true},"first_failure_at":{"type":"string","nullable":true},"created_at":{"type":"string"}},"required":["id","workspace_id","url","active","failure_count","last_status_code","last_attempt_at","auto_disabled_at","first_failure_at","created_at"]},"WebhookListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookResponseDto"}}},"required":["data"]},"UpdateWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"New destination URL."},"active":{"type":"boolean","description":"Pause/resume the webhook. Setting active=true clears auto-disable."}}},"CreateJobDto":{"type":"object","properties":{"json_schema":{"type":"object","description":"JSON Schema describing the shape of the extraction result. Must be a non-empty object with `type: \"object\"`.","example":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"]}},"description":{"type":"string","description":"Natural-language description of what to extract.","example":"Extract the product title and price from each page."},"suggested_urls":{"description":"Seed URLs the worker should start from.","maxItems":50,"type":"array","items":{"type":"string"}},"webhook_id":{"type":"string","description":"Webhook id to notify when the job finishes."},"max_steps":{"type":"number","description":"Maximum number of worker steps before the job stops.","minimum":1,"maximum":1000},"timeout_s":{"type":"number","description":"Wall-clock timeout for the job, in seconds.","minimum":1,"maximum":3600},"model_tier":{"type":"string","description":"Model tier the worker should use (e.g. `standard`, `pro`)."}},"required":["json_schema","description"]},"JobResponseDto":{"type":"object","properties":{"id":{"type":"string"},"workspace_id":{"type":"string"},"status":{"type":"string","enum":["queued","running","succeeded","failed","cancelled"]},"json_schema":{"type":"object"},"description":{"type":"string"},"suggested_urls":{"nullable":true,"type":"array","items":{"type":"string"}},"max_steps":{"type":"number"},"timeout_s":{"type":"number"},"model_tier":{"type":"string"},"webhook_id":{"type":"string","nullable":true},"result":{"type":"object","nullable":true},"error":{"type":"string","nullable":true},"attempts":{"type":"number"},"enqueued_at":{"type":"string"},"started_at":{"type":"string","nullable":true},"completed_at":{"type":"string","nullable":true},"created_at":{"type":"string"}},"required":["id","workspace_id","status","json_schema","description","suggested_urls","max_steps","timeout_s","model_tier","webhook_id","result","error","attempts","enqueued_at","started_at","completed_at","created_at"]},"JobListPaginationDto":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}},"required":["next_cursor","has_more"]},"JobListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/JobResponseDto"}},"pagination":{"$ref":"#/components/schemas/JobListPaginationDto"}},"required":["data","pagination"]}}}}