HTTP API reference
Every endpoint Skyline adds to the Horizon API, with parameters and responses.
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.
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/pause | Pause every master, on every host. |
DELETE | /api/pause | Resume every master, and lift queue:pause --all. |
DELETE | /api/queues/pause | Lift queue:pause --all only. |
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. |
GET | /api/locks/unique | Held unique locks and skipped dispatches. |
DELETE | /api/locks/unique | Release a held unique or overlap lock. |
GET | /api/locks/limits | Rate limiters, throttles and overlap locks that held jobs back, and debounced jobs. |
GET | /api/restarts | Unplanned worker restarts by reason. Needs insights. |
GET | /api/worker-resources | Fleet-wide worker memory and CPU. Needs insights. |
Job list responses#
Every job-listing endpoint returns the same envelope, with the job payload already decoded:
{
"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.
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 config. Takes no
parameters.
POST / DELETE /api/pause#
Pause and resume the whole fleet, the dashboard's Status tile switch. POST has the effect of
horizon:pause, but pushes the pause onto each master's Redis command queue rather than signalling a
process, so it reaches masters on every host. It responds 409 when no master is running.
DELETE resumes every master and, when queue:pause --all is on, lifts that too. Queues paused
one at a time stay paused. Both return 204 No Content. DELETE /api/queues/pause lifts
queue:pause --all alone, and responds 409 on a Laravel version without it (before 13.25).
GET /api/stats reports that switch as allQueuesPaused.
POST / DELETE /api/queues/{connection}/{queue}/pause#
Pause and resume a single queue. Both return 204 No Content on success.
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.
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.
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.
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.
GET / DELETE /api/locks/unique, GET /api/locks/limits#
The data behind the Locks & Limits screen.
GET /api/locks/unique returns the held locks, flagged first, with each lock's state
(held, stranded, job_missing, unlinked or
foreign_owner), its kind (unique or overlap) and the
skipped-dispatch counts. Pass flagged=1 for flagged locks only, and
limit (1 to 500, default 100) to cap the list. GET /api/locks/limits returns the counts per
rate limiter, throttle and overlap lock, with debounced jobs in a separate debounces list. With
lock_insights off, both return
"enabled": false and empty lists.
DELETE /api/locks/unique releases one lock. It takes the lock's key and the
owner the listing returned, and only releases the lock if that owner still holds it.
curl -X DELETE https://example.com/horizon/api/locks/unique \
-H 'Content-Type: application/json' \
-d '{"key": "laravel_unique_job:App\\Jobs\\ImportFeed:42", "owner": "Xb3k…"}'
{"released": true}
| Status | When |
|---|---|
200 | {"released": true}, or {"released": false, "reason": "not_held"} when the lock was already free. |
404 | Horizon isn't tracking a lock with that key. |
409 | The lock has been taken again by another owner since the listing, or it is kept in another cache store. |
422 | The key isn't a unique job lock or a tracked WithoutOverlapping lock, or no owner was sent. |
GET /api/restarts, GET /api/worker-resources#
The data behind the restart and worker resource charts. Both are gated on
insights and return 404 while
insights.enabled is false — a genuine 404, not an empty body, so a dashboard built against
them can tell "turned off" from "nothing happened yet".
Both share the trends timeline: labels is the bucket timeline, and every series is aligned to it, so a
chart can plot them without reconciling buckets itself. The window is
trends.interval and trends.retention.
curl https://example.com/horizon/api/restarts
{
"labels": ["09:00", "09:15", "09:30"],
"reasons": {
"memory": [0, 3, 1],
"crashed": [0, 0, 2]
}
}
reasons is keyed by stop reason — memory, timed_out, max_jobs,
max_time and crashed — and is an empty object when nothing has restarted. Only unplanned
restarts appear; workers retired by scaling down or by horizon:terminate never reach this path.
curl https://example.com/horizon/api/worker-resources
{
"labels": ["09:00", "09:15", "09:30"],
"memory": [67108864, 71303168, null],
"cpu": [1.8, 2.5, null]
}
memory is the average total resident bytes held by all workers in the bucket, and cpu the
average number of cores in use. A bucket nothing was sampled in is null rather than 0, so a
gap in sampling is not mistaken for an idle fleet.
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:
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:
Queue::connection('redis')->pushRawOnFront($payload, 'media');
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.