## Run history

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

Returns one non-deleted job's history, newest first by scheduledFor and id. Works when the feature is unavailable. No secret is returned; history and 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](../jobs.md); [signature verification](./signature.md).

## Parameters

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

## 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

HTTP 200. Illustrative response shape with synthetic IDs, not a live request result.

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "run_example",
        "jobId": "job_example",
        "serverId": "server_example",
        "portalId": "account_example",
        "path": "/tasks/digest?mode=daily",
        "scheduleRevision": 1,
        "trigger": "SCHEDULE",
        "scheduledFor": "2026-10-10T09:00:00.000Z",
        "status": "SUCCEEDED",
        "attempts": 1,
        "nextAttemptAt": null,
        "readyDeadlineAt": "2026-10-10T09:30:00.000Z",
        "deliveryDeadlineAt": "2026-10-14T09:00:00.000Z",
        "waitingForServer": false,
        "startedAt": "2026-10-10T09:00:02.000Z",
        "lastAttemptStartedAt": "2026-10-10T09:00:02.000Z",
        "late": false,
        "finishedAt": "2026-10-10T09:00:02.150Z",
        "durationMs": 150,
        "responseSource": "app",
        "httpStatus": 202,
        "errorCode": null,
        "lastAttemptErrorCode": null,
        "requestId": "request_example",
        "responseSnippet": "{\"accepted\":true}",
        "createdAt": "2026-10-10T09:00:00.000Z",
        "updatedAt": "2026-10-10T09:00:02.150Z"
      }
    ],
    "nextCursor": null
  }
}
```

## Response fields

| Field | Type | Description |
|---|---|---|
| `success` | boolean | Always `true` on success. |
| `data` | object | Object containing the page of runs in `items` and the cursor in `nextCursor`. |

The signing secret is not returned.

Fields of `data`:

| Field | Type | Description |
|---|---|---|
| `items` | array | Runs on this page |
| `nextCursor` | string / null | Pass as cursor; null means the end |

Fields of each run in `data.items`:

| Field | Type | Description |
|---|---|---|
| `id` | string | Run ID; the runId in the delivered body |
| `jobId` | string | Job ID |
| `serverId` | string | Server ID |
| `portalId` | string / null | Account snapshot when the run was created |
| `path` | string | Path snapshot; PATCH does not redirect an existing run |
| `scheduleRevision` | integer | Schedule revision snapshot |
| `trigger` | string | SCHEDULE or MANUAL; delivered body values are lowercase |
| `scheduledFor` | string | Scheduled time or manual run creation time, UTC ISO-8601 |
| `status` | string | Current state from the table below |
| `attempts` | integer | Reserved delivery attempts, 0–4 |
| `nextAttemptAt` | string / null | Next check or retry time for PENDING; otherwise null |
| `readyDeadlineAt` | string | createdAt + 30 minutes, fixed readiness deadline |
| `deliveryDeadlineAt` | string | createdAt + 96 hours, PENDING delivery lifetime limit |
| `waitingForServer` | boolean | Server readiness has not yet been confirmed |
| `startedAt` | string / null | First attempt start, not proof that the app accepted it |
| `lastAttemptStartedAt` | string / null | Latest attempt start |
| `late` | boolean | First attempt began later than scheduledFor + 60 seconds; 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; app does not prove that the handler ran |
| `httpStatus` | integer / null | Application HTTP response status, if available |
| `errorCode` | string / null | Outcome reason code |
| `lastAttemptErrorCode` | string / null | Last unsuccessful attempt reason; preserved on administrative cancellation |
| `requestId` | string / null | Delivery channel request ID, if available |
| `responseSnippet` | string / null | Bounded application response snippet, only for an authorized reader |
| `createdAt` | string | Creation time, UTC ISO-8601 |
| `updatedAt` | string | Last update time, UTC ISO-8601 |

### States and reasons

`PENDING` means queued or waiting; `RUNNING` is an attempt being sent. All other states are terminal: `SUCCEEDED` (HTTP 2xx), `FAILED`, `SKIPPED_BILLING`, `SKIPPED_ACCESS`, `SUPERSEDED`, `SERVER_UNAVAILABLE`. SUCCEEDED does not mean background work has finished. 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` | Readiness not confirmed by the 30-minute deadline |
| `ATTEMPTS_EXHAUSTED` | Four attempts exhausted; inspect lastAttemptErrorCode for the reason |
| `NEWER_SCHEDULE` | Newer scheduled occurrence superseded an unsent run |

Missing or changed account binding and payment/access refusals take priority over administrative cancellation; next come readiness deadline, deletion/disabling/revision, entitlement, feature availability, 96-hour lifetime and attempt budget; a newer schedule supersedes a run last. MANUAL is not cancelled solely by enabled=false or cron/timezone changes. RUNNING finishes its attempt.

### Retries

HTTP 2xx succeeds; 3xx is a permanent failure without following redirects; 4xx, including 408, is permanent except 429. 429 and 5xx may retry. At most four attempts; after the first three, random delays range from 1 ms to 1, 5 and 15 minutes respectively. A valid Retry-After (seconds or HTTP-date) increases the delay; more than 24 hours ends the run with RETRY_AFTER_TOO_LONG. A retry cannot go beyond deliveryDeadlineAt: reaching it means RUN_EXPIRED rather than another send.

Temporary reasons: `BH_TUNNEL_DISCONNECTED`, `BH_APP_STARTING`, `BH_APP_TIMEOUT`, `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`/`HTTP_5XX` may also retry. With `ATTEMPTS_EXHAUSTED`, the original reason remains in `lastAttemptErrorCode`.

## Errors

| HTTP | Code | Reason |
|---|---|---|
| 401 | `MISSING_API_KEY` | Missing API key |
| 403 | `SERVER_ROLE_FORBIDDEN` | Development team role does not permit this action |
| 404 | `NOT_FOUND` | Server not found or inaccessible to the caller |
| 404 | `NOT_FOUND` | Job is missing, deleted or belongs to another server |
| 400 | `INVALID_CURSOR` | Invalid or unknown cursor |
| 400 | `INVALID_LIMIT` | Limit must be an integer from 1 to 100 |

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

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

## See also

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