
## Service logs

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

Returns the logs of a BLACKHOLE server via the `journalctl` system utility. Without the `service` parameter, the whole system journal is read (all services and system-wide messages). To read the logs of a specific systemd unit, pass `service` (the deploy creates `app.service` by default, unless you specified another name). Supports filtering by line count, time window, and substring.

## Parameters

| Parameter | In | Type | Required | Default | Description |
|----------|---|-----|:-----:|-----------|----------|
| `id` | path | string (UUID) | yes | — | BLACKHOLE server ID |
| `service` | query | string | no | — | The systemd unit name: `app`, `crm-dashboard`, etc. Do not specify the `.service` extension. By default the whole system journal is read |
| `lines` | query | number | no | 50 | Number of lines, 1–500 |
| `since` | query | string | no | — | Timestamp in the `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 | Echo of the `?service=` parameter, or `null` if it was not passed |
| `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 | Echo of the `?lines=` parameter (validated 1–500, default 50) |
| `data.returnedLines` | number | Length of `data.logs` |
| `data.since` | string \| null | Echo of `?since=`, or `null` |
| `data.grep` | string \| null | Echo of `?grep=`, or `null` |
| `data.hint` | string | Optional. Diagnostics when `data.logs.length === 0` |

## 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` | The schema is violated: `lines` outside 1–500, `grep` longer than 200 characters |
| 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` | Invalid or expired API key |
| 404 | `NOT_FOUND` | The server does not exist, was deleted, or belongs to another API key |
| 409 | `SERVER_NOT_READY` | The server is not ready for the operation: it is not running or the tunnel is not connected. The response carries a `hint` field with the reason and the next step. Wake the server or call [`/repair`](/docs/infra/lifecycle/repair) and repeat the request. The "listed as connected but the Gateway has no live tunnel" case on this route arrives as `502 TUNNEL_NOT_FOUND` (see below) |
| 429 | `RATE_LIMITED` | The limit of 10 operations per minute per server was exceeded |
| 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 |
| 502 | `LOGS_UNAVAILABLE` | The agent could not read the system journal: no such unit, an invalid filter, access denied, or `journalctl` returned an error |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **Line format — exactly as `journalctl` emits it.** Each line already contains the time, host name, 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`. Useful for diagnostics when the application does not respond through the Deploy API. Simply do not pass the `service` parameter — you will see the whole system journal, including `vibe-agent`, cloud-init, and other system-wide messages.
- **The 500-line limit combined with the cap of 10 calls per minute** means you can read at most 5000 lines per minute. For a larger volume of logs, make several calls with different `since` values, staying within the limit.

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

- **Read-only.** Requesting logs does not wake a sleeping galaxy. If the galaxy host is asleep or unreachable, the response is an empty `data.logs` array plus a diagnostic `data.hint` field. Wake the galaxy (by any request to the app) and retry.
- **`since` — a duration or RFC3339, not a relative text form.** For galaxy apps `since` accepts only a duration in seconds, minutes, or hours (`10m`, `2h`, `24h`) or an RFC3339 timestamp (`2026-06-24T10:00:00Z`) — use `24h` for a one-day window. The relative `journalctl` text forms (`"1 hour ago"`, `"10 minutes ago"`) are accepted only for Black Hole servers — for a galaxy app such a value returns `400 VALIDATION_ERROR`.
- **`grep`** works the same way — a plain substring, filtered platform-side.

## See also

- [Full deploy](./deploy.md) — see which service name is created.
- [Run a command](./exec.md) — `journalctl -u app -f` to stream logs in real time via `exec`.
- [Tunnel metrics](./metrics.md) — agent activity and HTTP connections.
