# Create a cron schedule for a program

POST /v1/schedules: Create a cron schedule for a program

```http
POST /v1/schedules
```

|  |  |
|---|---|
| Operation id | `createSchedule` |
| Key scope | `run`: keys that run jobs (and `admin` keys) |
| Section | [Schedules](https://docs.kaab.ist/reference/api#schedules) |

## Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `program` | string | yes | min length 1 |
| `inputs` | map of string or number or boolean |  |  |
| `cron` | string | yes | pattern `^\S+\s+\S+\s+\S+\s+\S+\s+\S+$` |
| `timezone` | string |  | min length 1 · default `"UTC"` |
| `change_detection` | object |  |  |
| `change_detection.enabled` | boolean | yes |  |
| `change_detection.fields` | string[] |  |  |
| `webhook_url` | string (uri) |  |  |
| `options` | object |  |  |
| `options.region` | `"eu"`, `"us"` |  | The execution region, `eu` or `us`. Defaults to your organisation's region. |
| `options.max_tier` | `"http"`, `"browser"`, `"stealth"` |  | The most expensive tier the job may escalate to: `http`, `browser` or `stealth`. Defaults to your plan's maximum; a page that needs more fails the job with `max_tier_exceeded`. |
| `options.prefer_tier` | `"http"`, `"browser"`, `"stealth"` |  | The tier the job starts at: `http`, `browser` or `stealth`. Defaults to `http` for a program that only uses `goto` and `extract`, and `browser` otherwise; a site profile may raise it. |
| `options.assets` | `"block"`, `"allow"` |  | Whether browser tiers load images, fonts and media: `block` (the default) or `allow`. |
| `options.block_urls` | string[] |  | Up to 50 URL globs, matched against the full URL, whose requests a browser tier aborts before they are sent, whatever `assets` says. Use it for a site's own analytics; a pattern that matches something the page needs breaks the page, and the `http` tier ignores it. · max items 50 |
| `options.proxy` | object |  | Which proxy the job's traffic goes through. Defaults to `{ "mode": "platform", "fallback": "none" }`. |
| `options.proxy.mode` | `"platform"`, `"byo"` |  | `platform` uses Kaabist's proxy pool; `byo` uses a proxy your organisation registered. Defaults to `platform`. |
| `options.proxy.byo_id` | string |  | The `byo_…` id of the bring-your-own proxy to use with `mode: "byo"`. Without it, `byo` uses your organisation's default proxy. · pattern `^byo_[0-9a-hjkmnp-tv-z]{26}$` |
| `options.proxy.country` | string |  | The exit country, as an uppercase ISO-3166-1 alpha-2 code such as `DE`. · pattern `^[A-Z]{2}$` |
| `options.proxy.pool` | `"datacenter"`, `"residential"`, `"mobile"` |  | The platform proxy class: `datacenter`, `residential` or `mobile`. |
| `options.proxy.fallback` | `"platform"`, `"none"` |  | What happens when a bring-your-own proxy fails. `none` (the default) fails the job with `byo_proxy_unreachable`; `platform` runs on the platform pool, adds the warning `byo_fallback_used` and bills the proxy traffic. |
| `options.llm` | object |  | The language model that heals locators and classifies unrecognised pages. Defaults to your organisation's configured model, else the platform's model when it is enabled. |
| `options.llm.byo_id` | string |  | The `byo_…` id of a bring-your-own model. Only `capable` models heal, and a bring-your-own model's tokens are not billed. · pattern `^byo_[0-9a-hjkmnp-tv-z]{26}$` |
| `options.captcha` | object |  | The captcha solver `policies.captcha: "solve"` uses. Defaults to your organisation's configured solver, else the platform's. |
| `options.captcha.byo_id` | string |  | The `byo_…` id of a bring-your-own captcha solver. Its solves are not billed. · pattern `^byo_[0-9a-hjkmnp-tv-z]{26}$` |
| `options.session` | string |  | The name of a session to bind. Its storage state is loaded before step 1, so the job starts signed in. · min length 1 |
| `options.resume_from_session` | boolean |  | When `true` and the bound session is valid, the job skips ahead to the step after the `checkpoint` that saved this session, instead of signing in again. Defaults to `false`. |
| `options.session_probe` | `"http"`, `"browser"` |  | Which tier checks the bound session before the job runs: `http` (the default, one request) or `browser`, for a site whose signed-in landmark needs JavaScript. A `browser` probe is billed as a browser page load. |
| `options.cache` | object |  | Lets the job reuse a recent snapshot of the same page. Ignored when the program binds a session or has secret inputs. |
| `options.cache.max_age` | integer | yes | The oldest snapshot the job accepts, in seconds, from `0` to `86400`. · ≥ 0 · ≤ 86400 |
| `options.viewport` | object |  | The browser window size for browser tiers. Defaults to 1280×800. |
| `options.viewport.width` | integer | yes | Viewport width in pixels, from `320` to `3840`. · ≥ 320 · ≤ 3840 |
| `options.viewport.height` | integer | yes | Viewport height in pixels, from `240` to `2160`. · ≥ 240 · ≤ 2160 |
| `options.browser_profile` | `"desktop"`, `"mobile"` |  | The user agent and viewport bundle: `desktop` (the default) or `mobile`. |
| `options.engine` | `"chromium"`, `"firefox"` |  | The stealth tier's browser. Without it, a stealth job starts on a hardened Chromium and switches to Camoufox (a Firefox build) for a hostname where Chromium was refused. `firefox` always runs Camoufox; `chromium` never switches. The browser tier always runs Chromium. |
| `options.fill_mode` | `"insert"`, `"type"` |  | How `fill` puts text into a field at a browser tier. `insert` (the default) commits the whole value at once, as a paste does; `type` sends one key event per character, for fields with an input mask or a per-keystroke validator. |
| `options.follow_popups` | boolean |  | Browser tiers: when `true`, a page the program opens in a popup (`window.open`, `target=_blank`) is opened in the same tab instead, so a one-time sign-in token is spent by the tab the program drives. Defaults to `false`. |
| `options.locale` | string |  | The browser locale as a BCP-47 tag. Defaults to `en-US`, or to the locale of `proxy.country` when one is set. · min length 2 |
| `options.timezone` | string |  | The browser time zone as an IANA name. Defaults to `UTC`, or to the time zone of `proxy.country` when one is set. · min length 1 |
| `options.max_duration_ms` | integer |  | The whole job's time budget in milliseconds, from `1000` to `3600000`, not counting time spent parked. Defaults to your plan's limit. · ≥ 1000 · ≤ 3600000 |
| `options.max_credits` | integer |  | The most credits the job may spend; exceeding it fails the job. Defaults to your plan's limit. · ≥ 1 |
| `options.max_steps` | integer |  | The most steps the job may execute, from `1` to `200`. Defaults to `200`. · ≥ 1 · ≤ 200 |
| `options.headers` | map of string |  | Extra request headers, as a map of name to value, sent by the `http` tier and, on the browser tiers, on the page's own top-level navigations only: never on its images, scripts or `fetch` calls, nor to another host the page talks to. |
| `options.canary` | object |  | The canary probe that runs the program on a schedule to notice when a site changes. The stored canary settings on the program override these key by key. |
| `options.canary.enabled` | boolean |  | `true` opts in and `false` opts out. When absent, a program used in the last seven days that has a `canary.url` or is only `goto` and `extract` gets a canary. |
| `options.canary.cron` | string |  | The schedule the canary runs on, as a cron expression such as `0 */6 * * *`. · min length 1 |
| `options.canary.url` | string |  | The URL the canary runs against, a template. It is bound as the input named `url`, which `{{inputs.url}}` reads. |
| `options.canary.inputs` | map of any |  | Input values the canary runs with. Credentials do not belong here; they are stored separately on the program. |
| `options.obstacles` | object |  | How the obstacle layer treats cookie banners, dialogs, overlays, toasts and tours. Defaults to `{ "allow_hide": true }`. |
| `options.obstacles.allow_hide` | boolean |  | Whether a `dialog` or `consent` obstacle that cannot be dismissed may be hidden as a last resort. Defaults to `true`; set `false` for a page whose modal is the page, such as an age gate or a paywall, so the job parks for a person instead. |
| `options.obstacles.ignore` | string[] |  | Up to 20 CSS selectors the obstacle probe never reports, for elements such as a chat bubble or a sticky header that are part of the page. · max items 20 |
| `overlap_policy` | `"skip"`, `"queue"` |  | default `"skip"` |
| `catch_up` | boolean |  | default `false` |

## Example request

```bash
curl -X POST "https://api.kaab.ist/v1/schedules" \
  -H "Authorization: Bearer $KAAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"program":"acme-report@production","inputs":{"report_id":"42"},"cron":"*/30 * * * *","timezone":"Europe/Istanbul","change_detection":{"enabled":true,"fields":["price"]},"webhook_url":"https://hooks.acme.test/kaab"}'
```

## Responses

### 201

Schedule

[Schedule](https://docs.kaab.ist/reference/api/schemas/schedule)

```json
{
  "id": "sch_01j9zqk3q7m4n5p6r7s8t9v0wx",
  "object": "schedule",
  "program": "acme-report@production",
  "inputs": {
    "report_id": "42"
  },
  "cron": "*/30 * * * *",
  "timezone": "Europe/Istanbul",
  "status": "active",
  "paused_reason": null,
  "mode": "live",
  "change_detection": {
    "enabled": true,
    "fields": [
      "price"
    ]
  },
  "overlap_policy": "skip",
  "catch_up": false,
  "webhook_url": "https://hooks.acme.test/kaab",
  "last_run_at": "2026-09-18T10:00:00Z",
  "next_run_at": "2026-09-18T10:30:00Z",
  "created_at": "2026-09-18T10:00:00Z"
}
```

## Errors

Every error answers with the [error envelope](https://docs.kaab.ist/reference/api/schemas/error-response); its `code` is one of the [error codes](https://docs.kaab.ist/reference/codes#errors).

| Status | When |
|---|---|
| 400 | `request_invalid`: request validation failed |
| 401 | Missing or invalid API key |
| 402 | `insufficient_credits`: top up to continue |
| 403 | The key's scope does not cover this operation |
| 409 | Conflicting state (see `error.code`) |
| 422 | Semantically invalid request (see `error.code`) |
| 429 | Rate limited; see `Retry-After` |
| 500 | `internal`: unexpected server error |
