# Cron jobs

Scheduled tasks for a site: WordPress scheduled tasks, a PHP script, a web address or a command, every 5 minutes at most, stopped after 15 minutes, as the site's own user. Free on every plan; 5 per site, 25 per account. Times are UTC.

## List a site's cron jobs

`GET /sites/{site_id}/cron-jobs` · scope `cron:read`

Every cron job of the site with its schedule (as written and in words, UTC), what it runs, whether it is on, why it is paused if it is (the site is frozen or suspended, it timed out 3 times in a row, or our team paused it), its next 3 run times and its last result. `kinds` lists what this site type may run. `limits` shows the per-site and per-account counts.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/cron-jobs" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/cron-jobs", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "jobs": [
      {
        "id": 42,
        "site_id": 1234,
        "name": "WordPress scheduled tasks",
        "kind": "wpcron",
        "kind_label": "Run WordPress scheduled tasks",
        "target": "",
        "command": "wp cron event run --due-now",
        "summary": "WordPress scheduled tasks (wp cron event run --due-now)",
        "schedule": "7,22,37,52 * * * *",
        "schedule_text": "Every 15 minutes (at :07, :22 ...)",
        "schedule_spec": {
          "mode": "minutes",
          "every": 15
        },
        "enabled": true,
        "state": "active",
        "state_label": "On",
        "state_reason": "",
        "paused_reason": null,
        "wp_pseudo_cron_off": true,
        "next_runs": [
          "2026-09-26T10:22:00Z",
          "2026-09-26T10:37:00Z",
          "2026-09-26T10:52:00Z"
        ],
        "last_run_at": "2026-09-26T10:07:00Z",
        "last_status": "ok",
        "last_exit_code": 0,
        "last_duration_seconds": 1.42,
        "run_now_pending": false,
        "created_at": "2026-09-26T09:00:00Z",
        "updated_at": "2026-09-26T09:00:00Z"
      }
    ],
    "limits": {
      "per_site": 5,
      "per_account": 25,
      "site_count": 1,
      "account_count": 3,
      "min_interval_minutes": 5,
      "max_run_minutes": 15,
      "timezone": "UTC"
    },
    "kinds": [
      "wpcron",
      "php",
      "url",
      "command"
    ],
    "available": true,
    "paused_reason": ""
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## List every cron job on the account

`GET /cron-jobs` · scope `cron:read`

All cron jobs on every site of the account (at most 25), each with its site id and domain.

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/cron-jobs" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/cron-jobs", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/cron-jobs", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "jobs": [
      {
        "id": 42,
        "site_id": 1234,
        "name": "WordPress scheduled tasks",
        "kind": "wpcron",
        "kind_label": "Run WordPress scheduled tasks",
        "target": "",
        "command": "wp cron event run --due-now",
        "summary": "WordPress scheduled tasks (wp cron event run --due-now)",
        "schedule": "7,22,37,52 * * * *",
        "schedule_text": "Every 15 minutes (at :07, :22 ...)",
        "schedule_spec": {
          "mode": "minutes",
          "every": 15
        },
        "enabled": true,
        "state": "active",
        "state_label": "On",
        "state_reason": "",
        "paused_reason": null,
        "wp_pseudo_cron_off": true,
        "next_runs": [
          "2026-09-26T10:22:00Z",
          "2026-09-26T10:37:00Z",
          "2026-09-26T10:52:00Z"
        ],
        "last_run_at": "2026-09-26T10:07:00Z",
        "last_status": "ok",
        "last_exit_code": 0,
        "last_duration_seconds": 1.42,
        "run_now_pending": false,
        "created_at": "2026-09-26T09:00:00Z",
        "updated_at": "2026-09-26T09:00:00Z",
        "domain": "example.com"
      }
    ],
    "count": 1,
    "per_account": 25
  }
}
```

Errors: `rate_limited`, `scope_missing`, `unauthorized`

## One cron job with its last runs

`GET /sites/{site_id}/cron-jobs/{cron_id}` · scope `cron:read`

The job and its last 10 runs: when, how long, the exit code, and the output (the first 1,000 and last 7,000 bytes of long output are kept).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `cron_id` | path | integer | yes | The cron job id (see the list). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "id": 42,
    "site_id": 1234,
    "name": "WordPress scheduled tasks",
    "kind": "wpcron",
    "kind_label": "Run WordPress scheduled tasks",
    "target": "",
    "command": "wp cron event run --due-now",
    "summary": "WordPress scheduled tasks (wp cron event run --due-now)",
    "schedule": "7,22,37,52 * * * *",
    "schedule_text": "Every 15 minutes (at :07, :22 ...)",
    "schedule_spec": {
      "mode": "minutes",
      "every": 15
    },
    "enabled": true,
    "state": "active",
    "state_label": "On",
    "state_reason": "",
    "paused_reason": null,
    "wp_pseudo_cron_off": true,
    "next_runs": [
      "2026-09-26T10:22:00Z",
      "2026-09-26T10:37:00Z",
      "2026-09-26T10:52:00Z"
    ],
    "last_run_at": "2026-09-26T10:07:00Z",
    "last_status": "ok",
    "last_exit_code": 0,
    "last_duration_seconds": 1.42,
    "run_now_pending": false,
    "created_at": "2026-09-26T09:00:00Z",
    "updated_at": "2026-09-26T09:00:00Z",
    "runs": [
      {
        "id": 881,
        "status": "ok",
        "status_label": "Finished",
        "trigger": "schedule",
        "started_at": "2026-09-26T10:07:00Z",
        "finished_at": "2026-09-26T10:07:02Z",
        "duration_seconds": 1.42,
        "exit_code": 0,
        "output": "Success: Executed a total of 3 cron events.\n",
        "output_bytes": 46,
        "output_truncated": false,
        "note": ""
      }
    ]
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## The last runs of a cron job

`GET /sites/{site_id}/cron-jobs/{cron_id}/runs` · scope `cron:read`

Newest first. status: ok, failed (non-zero exit code), timeout (stopped at 15 minutes), skipped (the previous run was still going, or the server was paused) or error (could not start). trigger: schedule or manual (Run now).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `cron_id` | path | integer | yes | The cron job id (see the list). |
| `limit` | query | integer | no | How many (newest first, at most 20 are kept). Default: `20`. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42/runs" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42/runs", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42/runs", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "runs": [
      {
        "id": 881,
        "status": "ok",
        "status_label": "Finished",
        "trigger": "schedule",
        "started_at": "2026-09-26T10:07:00Z",
        "finished_at": "2026-09-26T10:07:02Z",
        "duration_seconds": 1.42,
        "exit_code": 0,
        "output": "Success: Executed a total of 3 cron events.\n",
        "output_bytes": 46,
        "output_truncated": false,
        "note": ""
      }
    ]
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Add a cron job

`POST /sites/{site_id}/cron-jobs` · scope `cron:write`

Adds a job and puts it on the site's server within about a minute. It runs as the site's own user, in the site folder, with the site's PHP version, and is stopped after 15 minutes. Refused with limit_reached at 5 jobs on the site or 25 on the account, and with validation_failed for a schedule more often than every 5 minutes, a multi-line command, or a script path outside the site folder.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `name` | body | string | no | A name you recognise (up to 80 characters). |
| `kind` | body | string (one of: wpcron, php, url, command) | no | What runs. wpcron: WordPress scheduled tasks (`wp cron event run --due-now`, WordPress sites only); php: a PHP script, `target` = path inside the site folder ending in .php (not for Static HTML); url: open a web address, `target` = http(s) address; command: a shell command, `target` = the command (one line, up to 1,000 characters). Required when creating. |
| `target` | body | string | no | The script path, web address or command (see `kind`). Not used for wpcron. |
| `schedule_mode` | body | string (one of: minutes, hourly, daily, weekly, advanced) | no | How the schedule is given. minutes: every N minutes (`every`); hourly: once an hour at `minute`; daily: at `hour`:`minute` UTC; weekly: on `weekday` at `hour`:`minute` UTC; advanced: a 5-field cron `expression`. Required when creating. |
| `every` | body | integer (one of: 5, 10, 15, 20, 30) | no | minutes mode: run every 5, 10, 15, 20 or 30 minutes. We pick a fixed start offset inside each window (e.g. :07, :22, :37, :52), so sites do not all start at the same second. |
| `minute` | body | integer | no | hourly / daily / weekly: the minute (0-59). Default: a fixed minute picked for the job. |
| `hour` | body | integer | no | daily / weekly: the hour, UTC. |
| `weekday` | body | integer | no | weekly: 0 = Sunday, 1 = Monday ... 6 = Saturday. |
| `expression` | body | string | no | advanced: five fields "minute hour day-of-month month day-of-week", UTC, e.g. "*/15 * * * *" or "30 2 * * 1-5". It may never run more often than every 5 minutes (checked across the whole hour, including from :55 to :00). A "*/N" minute field is moved to a fixed offset. |
| `enabled` | body | boolean | no | Switch the job on (default) or off. |
| `wp_pseudo_cron_off` | body | boolean | no | wpcron only: also stop WordPress running its scheduled tasks on page visits (sets DISABLE_WP_CRON in wp-config.php, only if it is not set already; taken out again when the job is deleted or switched off). Default true for wpcron. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/cron-jobs" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "WordPress scheduled tasks", "kind": "wpcron", "schedule_mode": "minutes", "every": 15, "wp_pseudo_cron_off": true}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/cron-jobs", headers=headers, json={"name": "WordPress scheduled tasks", "kind": "wpcron", "schedule_mode": "minutes", "every": 15, "wp_pseudo_cron_off": True}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"name": "WordPress scheduled tasks", "kind": "wpcron", "schedule_mode": "minutes", "every": 15, "wp_pseudo_cron_off": true})
});
console.log(res.status, await res.json());
```

### Response

`201`

```json
{
  "data": {
    "id": 42,
    "site_id": 1234,
    "name": "WordPress scheduled tasks",
    "kind": "wpcron",
    "kind_label": "Run WordPress scheduled tasks",
    "target": "",
    "command": "wp cron event run --due-now",
    "summary": "WordPress scheduled tasks (wp cron event run --due-now)",
    "schedule": "7,22,37,52 * * * *",
    "schedule_text": "Every 15 minutes (at :07, :22 ...)",
    "schedule_spec": {
      "mode": "minutes",
      "every": 15
    },
    "enabled": true,
    "state": "active",
    "state_label": "On",
    "state_reason": "",
    "paused_reason": null,
    "wp_pseudo_cron_off": true,
    "next_runs": [
      "2026-09-26T10:22:00Z",
      "2026-09-26T10:37:00Z",
      "2026-09-26T10:52:00Z"
    ],
    "last_run_at": "2026-09-26T10:07:00Z",
    "last_status": "ok",
    "last_exit_code": 0,
    "last_duration_seconds": 1.42,
    "run_now_pending": false,
    "created_at": "2026-09-26T09:00:00Z",
    "updated_at": "2026-09-26T09:00:00Z"
  }
}
```

Errors: `conflict`, `limit_reached`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Change a cron job

`PATCH /sites/{site_id}/cron-jobs/{cron_id}` · scope `cron:write`

Change any of the fields; the others stay as they are. To change the schedule give `schedule_mode` with its fields. Switching a job on again (`enabled: true`) also clears a pause after repeated timeouts. On the server within about a minute.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `cron_id` | path | integer | yes | The cron job id (see the list). |
| `name` | body | string | no | A name you recognise (up to 80 characters). |
| `kind` | body | string (one of: wpcron, php, url, command) | no | What runs. wpcron: WordPress scheduled tasks (`wp cron event run --due-now`, WordPress sites only); php: a PHP script, `target` = path inside the site folder ending in .php (not for Static HTML); url: open a web address, `target` = http(s) address; command: a shell command, `target` = the command (one line, up to 1,000 characters). Required when creating. |
| `target` | body | string | no | The script path, web address or command (see `kind`). Not used for wpcron. |
| `schedule_mode` | body | string (one of: minutes, hourly, daily, weekly, advanced) | no | How the schedule is given. minutes: every N minutes (`every`); hourly: once an hour at `minute`; daily: at `hour`:`minute` UTC; weekly: on `weekday` at `hour`:`minute` UTC; advanced: a 5-field cron `expression`. Required when creating. |
| `every` | body | integer (one of: 5, 10, 15, 20, 30) | no | minutes mode: run every 5, 10, 15, 20 or 30 minutes. We pick a fixed start offset inside each window (e.g. :07, :22, :37, :52), so sites do not all start at the same second. |
| `minute` | body | integer | no | hourly / daily / weekly: the minute (0-59). Default: a fixed minute picked for the job. |
| `hour` | body | integer | no | daily / weekly: the hour, UTC. |
| `weekday` | body | integer | no | weekly: 0 = Sunday, 1 = Monday ... 6 = Saturday. |
| `expression` | body | string | no | advanced: five fields "minute hour day-of-month month day-of-week", UTC, e.g. "*/15 * * * *" or "30 2 * * 1-5". It may never run more often than every 5 minutes (checked across the whole hour, including from :55 to :00). A "*/N" minute field is moved to a fixed offset. |
| `enabled` | body | boolean | no | Switch the job on (default) or off. |
| `wp_pseudo_cron_off` | body | boolean | no | wpcron only: also stop WordPress running its scheduled tasks on page visits (sets DISABLE_WP_CRON in wp-config.php, only if it is not set already; taken out again when the job is deleted or switched off). Default true for wpcron. |

### Example

```bash
curl -s -X PATCH "https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "php", "target": "scripts/nightly.php", "schedule_mode": "daily", "hour": 3, "minute": 20}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.patch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42", headers=headers, json={"kind": "php", "target": "scripts/nightly.php", "schedule_mode": "daily", "hour": 3, "minute": 20}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42", {
  method: "PATCH",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"kind": "php", "target": "scripts/nightly.php", "schedule_mode": "daily", "hour": 3, "minute": 20})
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "id": 42,
    "site_id": 1234,
    "name": "WordPress scheduled tasks",
    "kind": "wpcron",
    "kind_label": "Run WordPress scheduled tasks",
    "target": "",
    "command": "wp cron event run --due-now",
    "summary": "WordPress scheduled tasks (wp cron event run --due-now)",
    "schedule": "7,22,37,52 * * * *",
    "schedule_text": "Every 15 minutes (at :07, :22 ...)",
    "schedule_spec": {
      "mode": "minutes",
      "every": 15
    },
    "enabled": true,
    "state": "active",
    "state_label": "On",
    "state_reason": "",
    "paused_reason": null,
    "wp_pseudo_cron_off": true,
    "next_runs": [
      "2026-09-26T10:22:00Z",
      "2026-09-26T10:37:00Z",
      "2026-09-26T10:52:00Z"
    ],
    "last_run_at": "2026-09-26T10:07:00Z",
    "last_status": "ok",
    "last_exit_code": 0,
    "last_duration_seconds": 1.42,
    "run_now_pending": false,
    "created_at": "2026-09-26T09:00:00Z",
    "updated_at": "2026-09-26T09:00:00Z"
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Delete a cron job

`DELETE /sites/{site_id}/cron-jobs/{cron_id}` · scope `cron:write` · **destructive**

Deletes the job and its run history, and takes it off the server within about a minute. A run already going finishes.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `cron_id` | path | integer | yes | The cron job id (see the list). |
| `confirm` | body | boolean | yes | Must be true: the job and its run history are deleted. |

### Example

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "deleted": true,
    "id": 42
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Run a cron job now

`POST /sites/{site_id}/cron-jobs/{cron_id}/run` · scope `cron:write`

Starts the job on its server at once (once a minute per job, 10 times in 10 minutes per account). The run reports back like a scheduled one: read it with cron.runs a few seconds later (trigger "manual"). Refused while the site is frozen or suspended.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `cron_id` | path | integer | yes | The cron job id (see the list). |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42/run" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42/run", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cron-jobs/42/run", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "started": true,
    "message": "Started.",
    "job": {
      "id": 42,
      "site_id": 1234,
      "name": "WordPress scheduled tasks",
      "kind": "wpcron",
      "kind_label": "Run WordPress scheduled tasks",
      "target": "",
      "command": "wp cron event run --due-now",
      "summary": "WordPress scheduled tasks (wp cron event run --due-now)",
      "schedule": "7,22,37,52 * * * *",
      "schedule_text": "Every 15 minutes (at :07, :22 ...)",
      "schedule_spec": {
        "mode": "minutes",
        "every": 15
      },
      "enabled": true,
      "state": "active",
      "state_label": "On",
      "state_reason": "",
      "paused_reason": null,
      "wp_pseudo_cron_off": true,
      "next_runs": [
        "2026-09-26T10:22:00Z",
        "2026-09-26T10:37:00Z",
        "2026-09-26T10:52:00Z"
      ],
      "last_run_at": "2026-09-26T10:07:00Z",
      "last_status": "ok",
      "last_exit_code": 0,
      "last_duration_seconds": 1.42,
      "run_now_pending": false,
      "created_at": "2026-09-26T09:00:00Z",
      "updated_at": "2026-09-26T09:00:00Z"
    }
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`
