## Run history

`GET /v1/infra/servers/:id/jobs/:jobId/runs`

Returns the run history of one non-deleted job, newest first by `scheduledFor` and `id`. Works even when the feature is unavailable. History and application response snippets require server permissions.

Requires a key with `vibe:infra` and existing server permissions. Writes need the managing key or a team role permitted to manage the lifecycle. General rules: [Scheduled HTTP calls](../jobs.md). Request verification in the handler: [Verify signatures](./signature.md).

## Parameters

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | Server ID from the [server list](../servers/list.md) |
| `jobId` | path | string | yes | Job ID from the [job list](./list.md) |
| `cursor` | query | string | no | The `nextCursor` value from the previous page. Pass it as is; do not construct it yourself |
| `limit` | query | integer | no | Page size, from 1 to 100. Defaults to 50 |

## Examples

### curl — personal key

```bash
curl --max-time 20 -X GET "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/jobs/JOB_ID/runs?limit=50" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
curl --max-time 20 -X GET "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/jobs/JOB_ID/runs?limit=50" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — personal key

```javascript
const serverId = 'SERVER_ID';
const jobId = 'JOB_ID';
const res = await fetch(`https://vibecode.bitrix24.com/v1/infra/servers/${encodeURIComponent(serverId)}/jobs/${encodeURIComponent(jobId)}/runs?limit=50`, {
  method: 'GET',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
  },
});
const result = await res.json();
if (!res.ok || !result.success) throw new Error(result.error?.code ?? `HTTP_${res.status}`);
console.log(result);
```

### JavaScript — OAuth application

```javascript
const serverId = 'SERVER_ID';
const jobId = 'JOB_ID';
const res = await fetch(`https://vibecode.bitrix24.com/v1/infra/servers/${encodeURIComponent(serverId)}/jobs/${encodeURIComponent(jobId)}/runs?limit=50`, {
  method: 'GET',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
});
const result = await res.json();
if (!res.ok || !result.success) throw new Error(result.error?.code ?? `HTTP_${res.status}`);
console.log(result);
```

## Response fields

| Field | Type | Description |
|---|---|---|
| `success` | boolean | Always `true` on success |
| `data.items` | array | Runs on the current page |
| `data.nextCursor` | string / null | Cursor of the next page for the `cursor` parameter. `null` means the end of the history |

Fields of each run in `data.items`:

| Field | Type | Description |
|---|---|---|
| `id` | string | Run ID. This is the `runId` in the request body sent to the handler |
| `jobId` | string | Job ID |
| `serverId` | string | Server ID |
| `portalId` | string / null | Bitrix24 account at the time the run was created |
| `path` | string | Path at the time the run was created. A job `PATCH` does not change an existing run |
| `scheduleRevision` | integer | Schedule revision at the time the run was created |
| `trigger` | string | `SCHEDULE` or `MANUAL`. The request body sent to the handler carries the same values in lowercase |
| `scheduledFor` | string | Scheduled time or manual run creation time, UTC ISO-8601 |
| `status` | string | Current state, see "States and reasons" below |
| `attempts` | integer | Reserved delivery attempts, from 0 to 4 |
| `nextAttemptAt` | string / null | Next check or retry time for a run in `PENDING` status, otherwise `null` |
| `readyDeadlineAt` | string | Server readiness deadline: `createdAt` plus 30 minutes |
| `deliveryDeadlineAt` | string | Lifetime limit of a run in `PENDING` status: `createdAt` plus 96 hours |
| `waitingForServer` | boolean | Server readiness has not yet been confirmed |
| `startedAt` | string / null | First attempt start. Not proof that the app accepted the request |
| `lastAttemptStartedAt` | string / null | Latest attempt start |
| `late` | boolean | The first attempt began later than `scheduledFor` plus 60 seconds. The value is preserved across retries |
| `finishedAt` | string / null | Terminal outcome time, UTC ISO-8601 |
| `durationMs` | integer / null | Latest attempt duration in milliseconds |
| `responseSource` | string / null | `app`, `gateway` or `null`. The value `app` does not prove that the handler did the work |
| `httpStatus` | integer / null | Application HTTP response status, if available |
| `errorCode` | string / null | Outcome reason code |
| `lastAttemptErrorCode` | string / null | Reason of the last unsuccessful attempt. Preserved on administrative cancellation |
| `requestId` | string / null | Delivery channel request ID, if available |
| `responseSnippet` | string / null | Bounded application response snippet. Visible only to a reader with server permissions |
| `createdAt` | string | Creation time, UTC ISO-8601 |
| `updatedAt` | string | Last update time, UTC ISO-8601 |

The signing secret is not returned.

### States and reasons

`PENDING` means the run is queued or waiting for server readiness; `RUNNING` means an attempt is being sent. All other states are terminal: `SUCCEEDED` (the app answered HTTP 2xx), `FAILED`, `SKIPPED_BILLING`, `SKIPPED_ACCESS`, `SUPERSEDED`, `SERVER_UNAVAILABLE`. `SUCCEEDED` does not mean background work has finished, and `startedAt` does not prove acceptance by the handler.

| Code | Meaning |
|---|---|
| `JOB_DISABLED` | Scheduled job disabled before sending |
| `SCHEDULE_CHANGED` | Scheduled run revision is stale |
| `JOB_DELETED` | Job deleted before sending |
| `ENTITLEMENT_REVOKED` | Schedule entitlement revoked |
| `FEATURE_DISABLED` | Feature disabled before sending |
| `RUN_EXPIRED` | 96-hour delivery lifetime expired |
| `TARGET_BINDING_CHANGED` | Server account binding changed after run creation |
| `READY_DEADLINE_EXCEEDED` | The server did not confirm readiness within 30 minutes |
| `ATTEMPTS_EXHAUSTED` | Four attempts exhausted. The original reason is in `lastAttemptErrorCode` |
| `NEWER_SCHEDULE` | Newer scheduled occurrence superseded an unsent run |

Reasons are checked in order. Missing or changed account binding and payment or access refusals come first and take priority over administrative cancellation. Next come the readiness deadline, deletion, disabling and revision change, entitlement, feature availability, the 96-hour lifetime and the attempt budget. Superseding by a newer scheduled occurrence is checked last. A `MANUAL` run is not cancelled solely by `enabled: false` or a change of `cronExpr` and `timezone`. A run in `RUNNING` status finishes its attempt.

### Retries

HTTP 2xx counts as success. 3xx is a permanent failure, and the platform does not follow redirects. 4xx, including 408, is also a permanent failure, except 429. After 429 and 5xx, the platform may retry. A run gets at most four attempts. After the first three, the platform waits a random delay from 1 ms to 1, 5 and 15 minutes respectively. A valid `Retry-After` header in seconds or HTTP-date format increases the delay, and a delay longer than 24 hours ends the run with `RETRY_AFTER_TOO_LONG`. A retry is never scheduled beyond `deliveryDeadlineAt`: at that boundary the run ends with `RUN_EXPIRED` rather than another send.

Temporary reasons: `BH_TUNNEL_DISCONNECTED`, `BH_APP_STARTING`, `BH_APP_TIMEOUT`, `BH_APP_CONNECTION_LOST`, `BH_APP_TRUNCATED`, `BH_APP_BUDGET_EXHAUSTED`, `BUDGET_EXHAUSTED`, `PORTAL_SATURATED`, `GATEWAY_SATURATED`, `TIMEOUT`, `TRANSPORT`, `LEASE_EXPIRED`. Permanent reasons: `BH_APP_RESPONSE_TOO_LARGE`, `RESPONSE_TOO_LARGE`, `BAD_ENVELOPE`, `UNKNOWN_GATEWAY_CODE`, `UNKNOWN_FORWARD_ERROR`, `INTERNAL_ERROR`, `HTTP_3XX`, `HTTP_4XX`. `HTTP_429` and `HTTP_5XX` can also be retried. With `ATTEMPTS_EXHAUSTED`, the original reason remains in `lastAttemptErrorCode`.

## Response example

`200 OK` response right after a manual run: the run is waiting for server readiness, and no attempts have been made yet.

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "cmvht3kfl1vpa4bpf4406sbpt",
        "jobId": "cmvht3jly1xgh4bo25ge2t996",
        "serverId": "9547e3f2-1369-4ed4-b047-a589b0a303c4",
        "portalId": "0d9f7744-f853-47c7-b06e-6f61f9dd1b7f",
        "path": "/tasks/digest?mode=weekday",
        "scheduleRevision": 2,
        "trigger": "MANUAL",
        "scheduledFor": "2026-10-21T07:44:22.831Z",
        "status": "PENDING",
        "attempts": 0,
        "nextAttemptAt": "2026-10-21T07:44:22.831Z",
        "readyDeadlineAt": "2026-10-21T08:14:22.831Z",
        "deliveryDeadlineAt": "2026-10-25T07:44:22.831Z",
        "waitingForServer": true,
        "startedAt": null,
        "lastAttemptStartedAt": null,
        "late": false,
        "finishedAt": null,
        "durationMs": null,
        "responseSource": null,
        "httpStatus": null,
        "errorCode": null,
        "lastAttemptErrorCode": null,
        "requestId": null,
        "responseSnippet": null,
        "createdAt": "2026-10-21T07:44:22.831Z",
        "updatedAt": "2026-10-21T07:44:22.831Z"
      }
    ],
    "nextCursor": null
  }
}
```

## Error response example

404 — the job is missing, deleted or belongs to another server:

```json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Job not found"
  }
}
```

## Errors

| HTTP | Code | Reason |
|---|---|---|
| 400 | `INVALID_CURSOR` | The `cursor` value is damaged or was not issued by the platform |
| 400 | `INVALID_LIMIT` | `limit` is not an integer from 1 to 100 |
| 404 | `NOT_FOUND` | Server not found or inaccessible to the caller; `message` is `Server not found` |
| 404 | `NOT_FOUND` | The job is missing, deleted or belongs to another server; `message` is `Job not found` |
| 401 | `MISSING_API_KEY` | Missing API key |
| 403 | `SERVER_ROLE_FORBIDDEN` | The development team member's role does not permit this action |

Full list of shared error codes — [Errors](/docs/errors).

## See also

- [List jobs](./list.md)
- [Run manually](./run.md)
