
## Service logs

`GET /v1/infra/servers/:id/logs`

Returns the logs of a BLACKHOLE server via the `journalctl` system utility, with filtering by line count, time window, and substring. By default a deploy creates the `app.service` unit, unless you specify another name.

## Parameters

| Parameter | In | Type | Required | Default | Description |
|----------|---|-----|:-----:|-----------|----------|
| `id` | path | string (UUID) | yes | — | BLACKHOLE server ID. List: [`GET /v1/infra/servers`](/docs/infra/servers/list) |
| `service` | query | string | no | — | The systemd unit name: `app`, `crm-dashboard`, etc. Do not include the `.service` suffix. By default, the whole system journal is read |
| `lines` | query | number | no | 50 | Number of lines, 1–500 |
| `since` | query | string | no | — | Timestamp in `journalctl` format: `"1 hour ago"`, `"2026-04-22 10:00:00"`, `"10 minutes ago"` |
| `grep` | query | string | no | — | Substring to filter by, up to 200 characters |

## Examples

### curl — personal key

```bash
# Application logs for the last hour with a filter on "error"
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/logs?service=app&lines=100&since=1%20hour%20ago&grep=error"

# The whole system journal, last 10 lines (for debugging cloud-init)
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/logs?lines=10"
```

### curl — OAuth application

```bash
curl -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/logs?service=app&lines=50"
```

### JavaScript — personal key

```javascript
const params = new URLSearchParams({
  service: 'app',
  lines: '100',
  since: '30 minutes ago',
})

const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/logs?${params}`,
  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)
const { data } = await res.json()
data.logs.forEach(line => console.log(line))
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/logs?service=app&lines=50`,
  {
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.service` | string \| null | Value of the `?service=` parameter from the request, or `null` if the parameter was not passed. Absent for an app in a galaxy |
| `data.logs` | array\<string\> | **The main field.** Log lines in arrival order (newest at the end of the array) |
| `data.lines` | array\<string\> | Alias of `data.logs` (the same array). Kept for backward compatibility — in new code use `data.logs` |
| `data.requestedLines` | number | Value of the `?lines=` parameter from the request |
| `data.returnedLines` | number | Length of `data.logs` |
| `data.since` | string \| null | Value of the `?since=` parameter from the request, or `null` |
| `data.grep` | string \| null | Value of the `?grep=` parameter from the request, or `null` |
| `data.hint` | string | Optional. Diagnostics when `data.logs.length === 0` |
| `data.recovery` | object | Only for an app in a galaxy whose host is asleep or unreachable. How to start the app and how long to wait — [Galaxy app sleep and wake](/docs/infra/galaxy-sleep) |

## Response example

```json
{
  "success": true,
  "data": {
    "service": null,
    "logs": [
      "Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root",
      "Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026/04/22 11:17:27 [exec] id= timeout=10s workdir=\"\" command=\"uname -a\"",
      "Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists..."
    ],
    "lines": [
      "Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root",
      "Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026/04/22 11:17:27 [exec] id= timeout=10s workdir=\"\" command=\"uname -a\"",
      "Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists..."
    ],
    "requestedLines": 50,
    "returnedLines": 3,
    "since": null,
    "grep": null
  }
}
```

## Error response example

400 — server in OPEN mode:

```json
{
  "success": false,
  "error": {
    "code": "NOT_BLACKHOLE",
    "message": "Deployment API only available for BLACKHOLE servers"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Schema violation: `lines` outside 1–500, `service` longer than 100 characters, `grep` longer than 200 characters. For an app in a galaxy, also `since` that is neither a `10m`-style duration nor an RFC 3339 timestamp |
| 400 | `NOT_BLACKHOLE` | Server in OPEN mode — the Deploy API is unavailable |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not provided |
| 401 | `INVALID_API_KEY` | The key is not recognized — no such key exists on the platform |
| 402 | `BILLING_EXHAUSTED` | The server is asleep with wake disabled, and the server owner's balance is frozen. Top up the balance and retry |
| 402 | `ACCOUNT_FROZEN` | The Vibecode balance is frozen. The request is rejected before the operation and a sleeping server is not woken — top up the balance and retry |
| 403 | `SERVER_WAKE_BLOCKED` | The server is asleep and waking is blocked — for example, platform access has ended or an administrative block is in place. A journal read does not bring such a server up, and a retry will not help until the block is lifted. For details on the block, see [Wake a server](/docs/infra/lifecycle/wake) |
| 403 | `WRONG_KEY` | The server exists but is not yours. Operations on the application's content — deploy, exec, upload and log reads — are open on any one of three grounds: the server's managing key, a key whose application is bound to that server (`Application.serverId`), or membership in that server's development team (both roles, Developer and Administrator). Access tokens and the icon upload require the managing key. Managing the machine itself is also open to the Administrator role on the team. The response carries a `hint` with a two-step recovery. See [Server access recovery](/docs/infra/server-access-recovery) |
| 403 | `EXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH` | The call was made with an external-collaborator key, and the address names a server other than the one the key was issued for. Such a key is valid for exactly one server, and the response does not reveal whether the requested server exists. Working with another server requires the key issued for that external membership |
| 403 | `EXTERNAL_COLLABORATOR_NOT_A_MEMBER` | An external-collaborator key addresses its own server, but access is not granted right now: the membership was removed, it expired, or external development collaborators are switched off for the Bitrix24 account that owns the server. The code does not tell these reasons apart and reveals nothing about the server. Membership is re-read on every request, so revoked access stops working immediately |
| 404 | `GALAXY_APP_NOT_FOUND` | Only for an app in a galaxy: the app record is not linked to a galaxy host or has no subdomain |
| 404 | `NOT_FOUND` | There is no server with this `id`, or it was deleted. A server that exists but is not yours answers `403 WRONG_KEY` — the code tells "no such server" apart from "no rights to it" |
| 409 | `SERVER_NOT_READY` | A standalone server is not ready for the operation: it is not running, the tunnel is not connected, or a successful non-empty Gateway snapshot confirms that a tunnel stored as `CONNECTED` is absent. An empty or unavailable snapshot does not confirm absence. The GET does not start repair: for a confirmed stale tunnel it refuses before reading logs and returns an explicit `POST /v1/infra/servers/:id/repair` in `hint`. Call that POST, then repeat the request. The platform still wakes a sleeping standalone server itself. The `GALAXY_APP` branch has the separate contract described below. A sleeping server with a live plan-change operation (`resize`) returns `409 EXEC_BUSY` before waking; wait for that operation to finish. |
| 409 | `EXEC_BUSY` | A sleeping standalone server with a live plan-change operation (`resize`) refuses before waking; wait for the operation to finish. Also for a Galaxy app (`kind: "GALAXY_APP"`): the host's shared exec channel is busy — the log is read through that same channel. The response carries a `Retry-After` header and the `retryable: true` / `retryAfter` fields — retry at that interval. The application owner cannot release the shared channel, so contact support if the channel stays busy |
| 409 | `WAKE_IN_PROGRESS` | Another request is already waking this sleeping server. Wait for it to finish and retry |
| 409 | `SERVER_STATE_CHANGED` | The server state changed during automatic recovery. Repeat the request |
| 422 | `GUEST_NOT_BOOTING` | The server's guest system does not boot, so operations on its contents are unavailable |
| 422 | `VM_MISSING` | The server record has no virtual machine on the provider side, so there is nothing to wake. Replace the server: delete it and create a new one. `error.hint` names the endpoint of each step only if that step is available to this request, and for an unavailable step it gives the reason for the refusal and what to change. The `error.userMessage` field arrives only when both steps are available |
| 422 | `VM_POINTER_LOST` | The record has no `externalId`, but the cloud review does see a machine under this server's cloud name: the link is lost, and the machine itself may keep running and being billed. Do NOT delete the server and create a new one — that would leave the machine running with no record at all. The server is not moved to `error`; contact support to have the link restored |
| 502 | `WAKE_FAILED` | Server readiness could not be confirmed. This response does not mean that the server was stopped. Check the [current server state](/docs/infra/servers/get) before taking another action |
| 502 | `PROVIDER_ERROR` | The cloud provider returned an error while starting the sleeping server's virtual machine. The tunnel is not involved here, so [`/repair`](/docs/infra/lifecycle/repair) will not help |
| 429 | `RATE_LIMITED` | The limit of 10 operations per minute per server was exceeded. The current limit is returned in the `x-ratelimit-limit` header |
| 502 | `TUNNEL_NOT_FOUND` / `GATEWAY_UNREACHABLE: …` | No live tunnel to the server, or the Gateway is unreachable — call [`/repair`](/docs/infra/lifecycle/repair) and retry. `GATEWAY_UNREACHABLE` carries details after the colon, so match `error.code` by prefix, not by an exact comparison |
| 503 | `GATEWAY_TIMEOUT: …` | The Gateway did not respond in time. The code also carries details after the colon — compare by prefix |
| 503 | `WAKE_NO_CAPACITY` | Startup has already been refused for lack of capacity. Read the [current server state](/docs/infra/servers/get) before trying again |
| 503 | `WAKE_TIMEOUT` | The sleeping server did not come up within the allotted 6.5 minutes — either the machine never started or the tunnel never connected. The server returns to `sleeping`, so a repeat request is safe. See "Known specifics" |
| 502 | `RUNTIME_FAILED` | Installing the runtime on the server failed. A log read waits for the installation to finish and refuses when it fails |
| 503 | `RUNTIME_TIMEOUT` | Installing the runtime did not finish within 10 minutes. The response contains `retryAfter: 30` and the `Retry-After: 30` header |
| 502 | `EXEC_FAILED` and other agent codes | Only for an app in a galaxy: the host agent could not read the container log. The code and message come from the agent |
| 502 | `LOGS_UNAVAILABLE` | The agent could not read the system journal: no such unit, an invalid filter, access denied, or `journalctl` returned an error |

For the full list of common API errors, see [Errors](/docs/errors).

## Known specifics

- **A sleeping standalone virtual machine is woken by a journal read.** If the server (`kind: "STANDALONE"`) is in `sleeping` status, the platform starts the wake and waits for it within the same request — up to 6.5 minutes — and only then reads the journal. The client's request timeout must be longer than that window. For a Galaxy app, a read behaves differently — see the "Galaxy apps" section below. A sleeping server with a live plan-change operation (`resize`) returns `409 EXEC_BUSY` before waking; wait for that operation to finish.
- **Line format — exactly as `journalctl` emits it.** Each line already contains the time, hostname, process name, and its PID — no extra processing on the client is needed. Output the lines as is.
- **The `grep` filter is a plain substring, not a regular expression.** For complex patterns, fetch more lines and filter on the client.
- **The tunnel agent's logs are visible without `service`** — under the name `vibe-agent`. They help find the cause when the application does not respond through the Deploy API.
- **The 500-line limit is multiplied by the call cap.** The `lines` bound applies to each request. The current request limit is returned in the `x-ratelimit-limit` header. Your actual ceiling is 500 × the value from that header. For a larger volume of logs, make several calls with different `since` values.

## Galaxy apps

If the server is a Galaxy app (`kind=GALAXY_APP`), the endpoint returns the stdout/stderr of the application container itself (`docker logs`), not the host system journal. The `service` parameter does not apply to Galaxy apps.

- **Reading does not wake the machine.** While the galaxy host is asleep or unreachable, the response arrives with status `200`, an empty `data.logs` array and two diagnostic fields — `data.hint` and `data.recovery`. Bring the app up with [`POST /v1/infra/servers/:id/wake`](/docs/infra/lifecycle/wake) and retry the request: the wake starts the same container, so lines written before the sleep stay in the log. A cold galaxy takes minutes to boot. Its boot budget arrives in the `recovery.coldBootBudgetSeconds` field, and readiness is checked by polling [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) on the `reachability.effectiveStatus` field. For an explanation of all response fields and the steps to follow, see [Galaxy app sleep and wake](/docs/infra/galaxy-sleep).
- **`since` — a duration or RFC 3339, not a relative text form.** For Galaxy apps `since` accepts only a duration in seconds, minutes, or hours — `10m`, `2h`, `24h` — or an RFC 3339 timestamp such as `2026-06-24T10:00:00Z`. For a one-day window, use `24h`. The relative `journalctl` text forms (`"1 hour ago"`, `"10 minutes ago"`) are accepted only for Black Hole servers.
- **The `grep` filter works the same way** — a plain substring, filtered platform-side.

## See also

- [Full deploy](./deploy.md)
- [Run a command](./exec.md)
- [Tunnel metrics](./metrics.md)
