Trawl docs
Quickstart

Quickstart.

First extraction in four steps.

By the end of this guide you'll have submitted an extraction job, watched it run, and read back a schema-conforming JSON result. Plan ten minutes.


Prerequisites

  • A ProductCraft workspace (create one at console.productcraft.co)
  • Any HTTP client (curl, fetch, Postman, etc.)

There's no Trawl SDK yet — the HTTP API is the integration surface today. Generated SDKs will follow the same pattern as the other ProductCraft products later.


Step 1

Enable Trawl and mint an API key

Trawl is workspace-scoped. Enable the trawl service on your workspace at console.productcraft.co, then mint a workspace API key (a PAK, prefixed pcft_live_) with the trawl.create and trawl.read actions.

Every request carries the key as a bearer token:

Authorization: Bearer pcft_live_...

If the workspace hasn't enabled the service, every route returns 403 with code SERVICE_NOT_ENABLED before the route runs. If the key lacks the required trawl.* action you get a 403 policy denial instead. Full detail on workspaces, keys, and permissions is in Guide 01 · Foundations.


Step 2

Create a job

A job takes two required inputs: a JSON Schema describing the shape you want back, and a natural-language description of what to extract. Seed URLs are optional — without them the agent searches the web itself. With them, it starts from your pages.

POST /v1/workspaces/:workspace_id/jobs
curl https://api.trawl.productcraft.co/v1/workspaces/<workspace_id>/jobs \
  -H 'authorization: Bearer pcft_live_...' \
  -H 'content-type: application/json' \
  -d '{
    "description": "Extract the product name and the pricing plans (name and monthly USD price) from this pricing page.",
    "json_schema": {
      "type": "object",
      "properties": {
        "product_name": { "type": "string" },
        "plans": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "monthly_price_usd": { "type": ["number", "null"] }
            },
            "required": ["name"]
          }
        }
      },
      "required": ["product_name", "plans"]
    },
    "suggested_urls": ["https://example.com/pricing"]
  }'

Response — 201

{
  "id": "3f2b6c1a-9d4e-4b7f-8a21-...",
  "workspace_id": "<workspace_id>",
  "status": "queued",
  "json_schema": { "type": "object", "...": "..." },
  "description": "Extract the product name and the pricing plans...",
  "suggested_urls": ["https://example.com/pricing"],
  "max_steps": 10,
  "timeout_s": 120,
  "model_tier": "default",
  "webhook_id": null,
  "result": null,
  "error": null,
  "attempts": 0,
  "enqueued_at": "2026-07-10T09:00:00.000Z",
  "started_at": null,
  "completed_at": null,
  "created_at": "2026-07-10T09:00:00.000Z"
}

Save the id — that's what you poll in the next step. Two things worth knowing before you write your own schema:

  • The top level of json_schema must be a non-empty object with "type": "object". To extract a list, wrap it in an object property (as plans does above) — a bare top-level array is rejected with a 400.
  • Make hard-to-find fields nullable, like monthly_price_usd above. The agent is instructed to use null rather than invent a value it can't find.

description can be up to 2,000 characters; suggested_urls takes up to 50 http(s) URLs. The default wall-clock budget is 120 seconds — raise it with timeout_s (1–3,600) for jobs that need to visit more pages. Retrying the create safely? Add an optional Idempotency-Key header — replays with the same key and body return the original response for 24 hours.


Step 3

Poll until the job finishes

Jobs run asynchronously: queued → running → succeeded or failed. The result is stored on the job row, so polling GET is a first-class way to collect it — no webhook required.

GET /v1/workspaces/:workspace_id/jobs/:id
curl https://api.trawl.productcraft.co/v1/workspaces/<workspace_id>/jobs/<job_id> \
  -H 'authorization: Bearer pcft_live_...'

status moves through queued (accepted, waiting for a worker) → running (a worker has picked it up — started_at is stamped) → one of the terminal states: succeeded, failed, or cancelled. Terminal states are final. With the default 120-second budget, a few seconds between polls is plenty.


Step 4

Read the extracted data

On success, the result field holds the extracted data — a JSON object validated against your schema before it's stored. Malformed or non-conforming output is never delivered.

Response — status succeeded

{
  "id": "3f2b6c1a-9d4e-4b7f-8a21-...",
  "status": "succeeded",
  "result": {
    "product_name": "Example",
    "plans": [
      { "name": "Starter", "monthly_price_usd": 9 },
      { "name": "Team", "monthly_price_usd": 29 },
      { "name": "Enterprise", "monthly_price_usd": null }
    ]
  },
  "error": null,
  "started_at": "2026-07-10T09:00:02.000Z",
  "completed_at": "2026-07-10T09:00:41.000Z",
  "...": "..."
}

That's the whole loop: schema in, validated JSON out. If the job ends failed instead, result is null and error carries a human-readable reason — for example extraction did not complete within the step/time budget when the sources didn't yield the data in time. Adjust the description, seeds, or timeout_s and submit a new job.


Where to go next