# HTTP API reference

> Every endpoint Skyline adds to the Horizon API, with parameters and responses.

Source: https://boring-observability.dev/skyline/docs/api
Section: Reference — Skyline for Laravel documentation
Updated: 2026-07-13

---

The Skyline dashboard is a single-page app talking to a JSON API. Every operation it performs is available to you — for a deploy script, an incident runbook, or a bot that drains a queue when a downstream service goes down.

This page documents the endpoints Skyline **adds**, and the extra parameters it adds to inherited ones. The upstream Horizon endpoints (`/stats`, `/workload`, `/masters`, `/monitoring`, `/metrics/*`, `/batches/*`) are unchanged.

## Base path and authentication

Endpoints are mounted under the dashboard path, which is `horizon.path` in your config and defaults to `horizon`. So the full base URL is `/horizon/api`.

There is **no separate API authentication**. Every endpoint sits behind the same `horizon.middleware` (`['web']` by default) and the same `viewHorizon` gate as the dashboard itself. In practice that means a session cookie: whoever can view the dashboard can call the API, and nobody else can. Calls that change state are ordinary `POST` and `DELETE` requests through the `web` middleware group, so they need a CSRF token.

The `curl` snippets below show the shape of each request. They are not copy-pasteable as-is: without a session cookie you get a redirect, and without a CSRF token a state-changing call gets a `419`.

> **These endpoints are destructive**
>
> `DELETE /api/queues/…` discards every job in a queue. If you expose the dashboard beyond your operators, review your `viewHorizon` gate before you rely on the UI's confirmation modals as the safety net — the API has no modal.

## Skyline endpoints

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/trends` | Workload, wait and failure time-series. |
| `POST` | `/api/queues/{connection}/{queue}/pause` | Pause one queue. |
| `DELETE` | `/api/queues/{connection}/{queue}/pause` | Resume one queue. |
| `GET` | `/api/jobs/queues/{queue}` | Jobs waiting inside a queue. |
| `GET` | `/api/jobs/delayed` | Scheduled and retrying jobs. |
| `GET` | `/api/jobs/reserved` | Jobs currently executing. |
| `POST` | `/api/jobs/perform/{id}` | Run a delayed job immediately. |
| `DELETE` | `/api/jobs/{id}` | Delete a pending or delayed job. |
| `DELETE` | `/api/queues/{connection}/{queue}` | Empty a queue. |

## Job list responses

Every job-listing endpoint returns the same envelope, with the job payload already decoded:

```json
{
  "jobs": [ { "id": "8813", "name": "App\\Jobs\\ProcessPodcast", "queue": "default", "payload": { }, "status": "pending" } ],
  "total": 412
}
```

They paginate with a cursor, not a page number. Pass the index after which to read as `starting_at`; it defaults to `-1`, meaning the beginning.

> **total may be null**
>
> When a listing is scoped with `?queue=`, `total` comes back as `null`. Counting the matches for one queue means scanning the whole state, so Skyline skips it and paginates by cursor alone. Treat `null` as "unknown", not zero.

## GET /api/trends

Returns the time-series behind the dashboard's trend charts — workload, wait and failures, bucketed and retained according to your [`trends`](https://boring-observability.dev/skyline/docs/configuration#trends) config. Takes no parameters.

## POST / DELETE /api/queues/{connection}/{queue}/pause

Pause and resume a single queue. Both return `204 No Content` on success.

```bash
curl -X POST   https://example.com/horizon/api/queues/redis/exports/pause
curl -X DELETE https://example.com/horizon/api/queues/redis/exports/pause
```

Both respond `409 Conflict` with `{"message": "Queue pausing is not supported in this environment."}` when the Laravel version or the cache store cannot support per-queue pausing. See [Pausing & resuming queues](https://boring-observability.dev/skyline/docs/pausing-queues).

## GET /api/jobs/queues/{queue}

The jobs waiting inside a single queue, in order.

| Parameter | Default | Meaning |
| --- | --- | --- |
| `starting_at` | `-1` | Cursor index to read after. |
| `name` | — | Case-insensitive substring match on the job class name. |

## GET /api/jobs/delayed

Delayed jobs: those scheduled for the future and those waiting out a retry backoff.

| Parameter | Default | Meaning |
| --- | --- | --- |
| `starting_at` | `-1` | Cursor index to read after. |
| `name` | — | Case-insensitive substring match on the job class name. |
| `filter` | — | `scheduled` (never attempted) or `retry` (released after a failure). Any other value is ignored and returns both. |
| `queue` | — | Restrict to one queue. Forces `total` to `null`. |

Each job carries an absolute `available_at` timestamp, so you can show a real next-run time rather than a backoff duration.

## GET /api/jobs/reserved

Jobs a worker has reserved and is executing right now — what the dashboard's **In Progress** tab shows.

| Parameter | Default | Meaning |
| --- | --- | --- |
| `starting_at` | `-1` | Cursor index to read after. |
| `name` | — | Case-insensitive substring match on the job class name. |
| `queue` | — | Restrict to one queue. Forces `total` to `null`. |

## POST /api/jobs/perform/{id}

Runs a delayed or retrying job immediately instead of waiting for its available-at time. This is the **Perform Now** button. The request returns as soon as the trigger is queued; the job itself runs on a worker.

```bash
curl -X POST https://example.com/horizon/api/jobs/perform/8813
```

The job id is not validated at the HTTP layer — an unknown id is accepted here and resolved when the trigger runs.

## DELETE /api/jobs/{id}

Removes a single job from its queue. Returns `204 No Content` on success.

| Status | When |
| --- | --- |
| `204` | The job was removed and any `ShouldBeUnique` lock released. |
| `404` | No such job. |
| `422` | The job is not `pending` or `delayed` — a reserved, completed or failed job cannot be deleted. |
| `422` | The job's queue connection is unknown, or is not a Redis connection. |
| `409` | A worker reserved the job between the lookup and the delete, so it is no longer removable. |

## DELETE /api/queues/{connection}/{queue}

Empties a queue: discards every pending and delayed job in it. Returns `200` with the number removed.

```bash
curl -X DELETE https://example.com/horizon/api/queues/redis/exports

{"deleted": 1284}
```

Responds `422` when the connection is unknown, or when its driver does not support clearing. The unique lock of every removed job is released — see [Unique job locks](https://boring-observability.dev/skyline/docs/unique-job-locks).

## Extended Horizon endpoints

These endpoints exist in Horizon; Skyline adds parameters to them. All accept `starting_at` as upstream does.

| Endpoint | Added parameters |
| --- | --- |
| `GET /api/jobs/pending` | `name`, `queue` |
| `GET /api/jobs/completed` | `name`, `queue` |
| `GET /api/jobs/silenced` | `name` |
| `GET /api/jobs/failed` | `name` |

On `/api/jobs/failed`, the upstream `tag` parameter still works and takes precedence over `name`; the two are mutually exclusive.

## From PHP

If you are scripting against Skyline in-process rather than over HTTP, the same data is on the job repository, which you can resolve from the container:

```php
use Laravel\Horizon\Contracts\JobRepository;

$jobs = app(JobRepository::class);

$jobs->getReserved();                              // executing now
$jobs->getDelayed(null, null, 'retry');            // backing off after a failure
$jobs->getPendingForQueue('exports');              // waiting in one queue
$jobs->countPending('ProcessPodcast');             // by class-name substring
```

And to prepend a raw payload onto a queue, bypassing the job class entirely:

```php
Queue::connection('redis')->pushRawOnFront($payload, 'media');
```

> **Version note**
>
> `GET /api/jobs/reserved`, the `filter` parameter on `/api/jobs/delayed`, and the `queue` parameter on the job listings arrived with the job-state tabs. If your installed Skyline version predates them, the reserved endpoint 404s and the unknown parameters are ignored. Upgrade with `composer update boring-o11y/laravel-skyline`.


## Common questions

### Can I call the Skyline API from a script?

Yes, but the endpoints sit behind the same viewHorizon gate and web middleware as the dashboard itself, so a real script has to carry a session cookie and a CSRF token. A bare curl gets a redirect or a 419, not a paused queue.

### Does Skyline change the existing Horizon API endpoints?

No. Skyline adds endpoints; it does not alter or remove the ones Horizon already exposes, so anything built against the Horizon API keeps working after the swap.
