Skyline

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.

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/trendsWorkload, wait and failure time-series.
POST/api/pausePause every master, on every host.
DELETE/api/pauseResume every master, and lift queue:pause --all.
DELETE/api/queues/pauseLift queue:pause --all only.
POST/api/queues/{connection}/{queue}/pausePause one queue.
DELETE/api/queues/{connection}/{queue}/pauseResume one queue.
GET/api/jobs/queues/{queue}Jobs waiting inside a queue.
GET/api/jobs/delayedScheduled and retrying jobs.
GET/api/jobs/reservedJobs 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/uniqueHeld unique locks and skipped dispatches.
DELETE/api/locks/uniqueRelease a held unique or overlap lock.
GET/api/locks/limitsRate limiters, throttles and overlap locks that held jobs back, and debounced jobs.
GET/api/restartsUnplanned worker restarts by reason. Needs insights.
GET/api/worker-resourcesFleet-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.

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.

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.

ParameterDefaultMeaning
starting_at-1Cursor 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.

ParameterDefaultMeaning
starting_at-1Cursor 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.

ParameterDefaultMeaning
starting_at-1Cursor 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.

StatusWhen
204The job was removed and any ShouldBeUnique lock released.
404No such job.
422The job is not pending or delayed — a reserved, completed or failed job cannot be deleted.
422The job's queue connection is unknown, or is not a Redis connection.
409A 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}
StatusWhen
200{"released": true}, or {"released": false, "reason": "not_held"} when the lock was already free.
404Horizon isn't tracking a lock with that key.
409The lock has been taken again by another owner since the listing, or it is kept in another cache store.
422The 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.

EndpointAdded parameters
GET /api/jobs/pendingname, queue
GET /api/jobs/completedname, queue
GET /api/jobs/silencedname
GET /api/jobs/failedname

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');
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.

Queue control, not just queue monitoring.

Skyline is a drop-in replacement for Laravel Horizon that lets you act on what you see — pause a queue, jump a job to the front, drain a backlog. $139 once, every app you run it on.

Buy Skyline — $139 once

Secure checkout by Anystack. 30 days to change your mind.