Getting started

Your first job

Install an SDK or the kaab CLI, run one job against a product page, and read the envelope that comes back.

This page runs one job against a shop's product page in curl, TypeScript, Python, Go or the kaab CLI, and shows the envelope it returns. It takes a test key and a few minutes, and every later page builds on the request you send here.

When to use it

Start here once you have an sk_test_ key (API keys and modes). The request is the url + extract shorthand, the simplest form a job takes. When you need a login, a click or a schedule, the same call takes a stored program instead; see Programs and versions.

Install and configure

Every client reads your key from KAAB_API_KEY, and the API address from KAAB_BASE_URL when you set it (the default is https://api.kaab.ist/v1).

export KAAB_API_KEY=sk_test_…

curl

Nothing to install. Send the key as Authorization: Bearer $KAAB_API_KEY.

TypeScript

Node 18 or later, ESM or CommonJS:

npm install @kaabist/sdk
import { Kaab } from "@kaabist/sdk";
const kaab = new Kaab(); // reads KAAB_API_KEY; or new Kaab({ apiKey, baseUrl })

Python

Python 3.11 or later, blocking (Kaab) or asyncio (AsyncKaab):

pip install kaabist
from kaabist import Kaab
kaab = Kaab()  # reads KAAB_API_KEY; or Kaab("sk_test_…", base_url=...)

Go

Go 1.24 or later. The Go SDK is published as its own module:

go get github.com/kaabist/kaab-go
import "github.com/kaabist/kaab-go/kaab"

c := kaab.New("") // reads KAAB_API_KEY; kaab.WithBaseURL(...) to override

Request and response types are in the client package of the same module.

The CLI

kaab is a single binary for Linux, macOS and Windows on amd64 and arm64, built on the Go SDK. Release archives are named kaab_<version>_<os>_<arch> (.tar.gz, .zip on Windows); with a Go toolchain you can also build it from the module:

go install github.com/kaabist/kaab-go/cmd/kaab@latest
kaab auth login            # paste your sk_test_ key

kaab auth login checks the key and keeps it in your operating system's keychain. KAAB_API_KEY overrides it, which is what CI should use. See The CLI.

Run the job

The job reads a shop's first product page and asks for its title and price, waiting up to 30 seconds for the result:

response=$(curl -sS --fail-with-body "${KAAB_BASE_URL:-https://api.kaab.ist/v1}/jobs" \
  -H "Authorization: Bearer $KAAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: wait=30" \
  -d '{
    "url": "'"${SITE_URL:-https://shop.example.com}"'/p/1",
    "extract": {
      "title": "h1",
      "price": { "selector": ".price", "type": "money", "locale": "tr", "currency": "TRY" }
    }
  }')
echo "$response"

Each client sends POST /v1/jobs with Prefer: wait=30. If the job finishes inside those 30 seconds you get the result in the same response; if it does not, you get the job as it is, and you wait for it separately.

Waiting separately

For a job that may take longer, create it without waiting and follow it to the end. Each SDK's wait helper long-polls GET /v1/jobs/{id}, so a job that is not moving costs about one request a minute:

// Or start it and wait separately: `jobs.wait` long-polls until the job ends.
const started = await kaab.jobs.create({
  url: `${site}/p/2`,
  extract: { title: "h1" },
});
const done = await kaab.jobs.wait(started.id, { timeoutMs: 120_000 });
console.log(`${done.id}: ${done.status} ${JSON.stringify(done.data)}`);

What comes back

A finished job is one envelope. This is the one the request above returns, trimmed to the keys that matter here (ids and timings differ on your run):

{
  "id": "job_01j9zqk3q7m4n5p6r7s8t9v0wx",
  "object": "job",
  "mode": "test",
  "status": "succeeded",
  "reason": null,
  "program": { "id": null, "name": null, "version": null, "version_id": null },
  "data": {
    "title": "Kulaklık Klasik 101",
    "price": { "amount": 1337.4, "currency": "TRY" }
  },
  "raw": null,
  "warnings": [],
  "meta": {
    "tier": "http",
    "tier_requested": "http",
    "escalations": [],
    "region": "eu",
    "final_url": "https://shop.example.com/p/1",
    "cache": "miss",
    "duration_ms": 412,
    "parked_ms": 0,
    "steps": [
      { "path": "steps[0]", "action": "goto", "status": "ok", "duration_ms": 380, "healed": false },
      { "path": "steps[1]", "action": "extract", "status": "ok", "duration_ms": 12, "healed": false }
    ],
    "snapshot": "/v1/jobs/job_01j9zqk3q7m4n5p6r7s8t9v0wx/snapshot"
  },
  "error": null,
  "input_request": null,
  "attention": null,
  "progress": null,
  "created_at": "2026-09-18T10:00:00Z",
  "finished_at": "2026-09-18T10:00:01Z"
}

What to read in it:

  • status is the one key to switch on. Here it is succeeded, so data is present.
  • data is typed: price is a number and a currency because the field said type: "money" with a Turkish locale, so 1.337,40 TL was parsed rather than returned as text.
  • program is all null because the job used the shorthand rather than a stored program. A job that names a program carries its id, name and version here.
  • meta.tier is http: the page is server-rendered, so no browser was needed. meta.steps shows the two steps the shorthand compiled to.
  • warnings is empty. A field that came back empty or unreadable would be listed here, with its path.

Understanding the envelope covers every status and what it carries.

Worked example

Extend the schema with two more locator kinds the same page supports: a regular expression over an element's text, and a JSONPath over the JSON the page embeds in <script id="__NEXT_DATA__">:

{
  "url": "https://shop.example.com/p/1",
  "extract": {
    "title": "h1",
    "price": { "selector": ".price", "type": "money", "locale": "tr", "currency": "TRY" },
    "sku": { "selector": ".sku", "regex": "SKU:\\s*([A-Z0-9-]+)" },
    "stock": { "json": "$.props.pageProps.product.stock", "type": "integer" }
  }
}

data now carries "sku": "HP-1037" and "stock": 8 beside the title and price. The job still runs at the http tier: both locators read the document that one request returned. Change the URL to /p/2 and the same schema reads the second product. When a page needs a browser instead, the job moves up a tier by itself and says so in meta.escalations; Tiers and escalation follows such a job step by step.

Pitfalls

  • Reading data without checking status. A job that has not succeeded has no data. A request with Prefer: wait that ran out of time answers 202 with a queued or running job.
  • localhost URLs. Jobs run on Kaabist's workers, which cannot reach your machine or private addresses; such a request fails ssrf_blocked. Use a public URL.
  • A wait above 60. Prefer: wait accepts 1 to 60 seconds. For longer jobs, use the wait helper or a webhook.

Reference

Was this page helpful?