Trawl guides
01 · Foundations

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 400 with the field-level messages in the body.
  • Path params (workspace_id, id) must be UUIDs; anything else is a 400 before 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 jobs
  • trawl.read — fetch a single job
  • trawl.list — list jobs
  • trawl.cancel — cancel a queued/running job
  • trawl.webhook.read — read webhooks + their delivery logs
  • trawl.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:

Policy statements for a Trawl integration key
[
  {
    "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.

GET /v1/workspaces/:workspace_id/jobs
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.