Workspace, key, request.
Everything in Trawl hangs off your workspace, and every request carries one bearer credential. Get the auth story right once and every later guide is just endpoints.
1
Workspace = boundary
Everything in Trawl is scoped to your workspace: your jobs, your results, your webhooks, your delivery logs. Every resource route lives under /v1/workspaces/:workspace_id/..., and every query is filtered by that workspace id — a caller authenticated against workspace A can never read or mutate workspace B's data. Cross-workspace ids don't leak anything either: a job or webhook id that belongs to another workspace returns a plain 404, never data.
Before anything works, activate Trawl on the workspace (Console → Services → Trawl → Enable). Workspaces without the trawl service enabled return 403 SERVICE_NOT_ENABLED on every route, before any data is touched.
2
Base URL, versioning, wire shape
The base URL is https://api.trawl.productcraft.co. Every route is versioned under /v1 — including the health probe, GET /v1/healthz (no auth, returns { "status": "ok" }). The interactive OpenAPI reference lives at /docs and the raw spec at /docs-json.
The wire is snake_case, everywhere — request bodies, query strings, and responses. Write json_schema, suggested_urls, timeout_s; read workspace_id, started_at, next_cursor. Two wire details worth knowing up front:
- Unknown body fields are silently stripped, not rejected — a typo'd field name won't error, it just won't do anything. Validation failures return
400with the field-level messages in the body. - Path params (
workspace_id,id) must be UUIDs; anything else is a400before the route logic runs.
There's no Trawl SDK yet — the HTTP API is the integration surface today, and generated SDKs will follow the same pattern as the other ProductCraft products later. Everything in these guides is plain curl / fetch.
3
Mint a key with trawl.* actions
Programmatic access uses a workspace API key (a PAK, prefixed pcft_live_), minted at console.productcraft.co. Keys bind to policies, and policies grant trawl.* actions. The full catalog:
trawl.create— submit jobstrawl.read— fetch a single jobtrawl.list— list jobstrawl.cancel— cancel a queued/running jobtrawl.webhook.read— read webhooks + their delivery logstrawl.webhook.manage— create, update, rotate, test, delete webhooks
A policy statement scoping a CI key to job operations looks like this — Trawl's routes check every action against the workspace-wide resource, so grant on * rather than a narrower URN pattern:
[
{
"effect": "allow",
"actions": ["trawl.create", "trawl.read", "trawl.list", "trawl.cancel"],
"resources": ["*"]
},
{
"effect": "allow",
"actions": ["trawl.webhook.read", "trawl.webhook.manage"],
"resources": ["*"]
}
]Grant only what the caller needs: a poller that submits and reads jobs doesn't need trawl.webhook.manage; a dashboard that only renders job history needs just trawl.read + trawl.list. For humans in the console, the workspace roles already carry Trawl grants — owners get everything; members get read-only (trawl.read, trawl.list, trawl.webhook.read).
4
How requests are authenticated
Every request carries one bearer credential in the Authorization header:
Authorization: Bearer pcft_live_...The same shared ProductCraft session that the console uses (the auth_token cookie on .productcraft.co) also works — handy for poking the API from a browser you're already signed into. When both are present, the Authorization header wins. For anything running on a server, use a PAK.
curl https://api.trawl.productcraft.co/v1/workspaces/<workspace_id>/jobs \
-H 'authorization: Bearer pcft_live_...'5
The failure modes, in the order you'll hit them
Auth problems come in three distinct shapes. Learn to read them and you'll never chase the wrong fix:
- 401 — who are you? No credential, an expired session, or a revoked key:
{ "message": "Unauthorized", "statusCode": 401 }A header that isn't Bearer <token> (wrong scheme, missing token) gets the same generic 401 — check for a stray prefix or a shell-quoting mistake before suspecting the key itself.
- 403 policy denial — you're authenticated, but not allowed. The key's policy doesn't grant the action this route requires:
{
"message": "Policy denies trawl.create on *",
"error": "Forbidden",
"statusCode": 403
}The message names the missing action — add it to the key's policy (or use a key that has it).
- 403 service gate — the workspace itself isn't set up. Trawl was never enabled on this workspace:
{
"statusCode": 403,
"message": "trawl is not enabled on this workspace",
"code": "SERVICE_NOT_ENABLED"
}The code field disambiguates this from a policy denial — no amount of policy editing fixes it; enable the service in the console instead.
And one near-miss worth knowing: a 404 on a resource you're sure exists usually means the id belongs to a different workspace than the one in the path. That's the tenancy boundary doing its job — fix the workspace_id in the URL.
6
What you now have
A workspace with Trawl enabled, a key scoped to the trawl.* actions your integration actually needs, and the ability to tell a bad credential from a missing grant from a disabled service at a glance. If you haven't already run the quickstart, this is the moment — otherwise, on to your first real extraction. The per-route permission gates are all listed in the API reference.