API reference
Every operation of the catalogue, the REST routes, authentication, limits and every error code.
One catalogue, three ways in
Everything the dashboard shows and does is an operation of one catalogue. Each operation is a tool of the hosted MCP server, a POST https://skillhook.dev/api/v1/tools/{name} over REST, and a skillhook cloud <name> command in a terminal. GET /api/v1/tools lists them with their JSON Schema, the scope each needs and whether your key has it; the resource routes below map onto the dashboard's pages for callers who prefer nouns. The whole thing is also an OpenAPI 3.1 document (how to use it).
Authentication and scopes
Every request carries an organisation API key (shc_…, created by an admin under Settings → API keys, shown once and stored hashed) as Authorization: Bearer. Never put a key in a URL. A key acts as the role its scope names; API keys and scopes has the details.
| Scope | Type | Description |
|---|---|---|
fleet:read | viewer | Reads: the overview, stats, alerts, machines, skills, jobs, deliveries, hosted URLs (not the URLs), commands, settings, members, problem reports. |
fleet:run | member | Also acts: skill sources, job files, live output, hosted URLs; run and test skills, answer agents, replay, cancel, dismiss alerts. |
fleet:admin | admin | Also changes: the audit log, notification channels (read and test); save and delete skills, hosted URLs on and off, rename and disconnect machines, settings, configuration, restarts, updates, secrets. |
Conventions
- Machines pull. An action is queued as a command the machine runs on its next sync (seconds while it is online, up to ten minutes while it is not). Routes that wait answer
202withpending: truewhile the machine has not answered;GET /api/v1/commands/{id}?wait=30follows it. - Names or ids. A machine is named by id, name or hostname; a job or delivery by the cloud's id or the machine's own. An ambiguous name is a
409naming the id to use. - Paging. Lists are newest first and return
next_before; send it back asbeforefor the next page.limitis at most 100. - Idempotency.
POST /api/v1/issuestakes anIdempotency-Key(the same key from the same API key returns the original report). Everything else is idempotent by nature or queues a command you can follow. - Limits. 600 requests a minute per key (
429 rate_limitedwithRetry-After); bodies up to 512 KiB (768 KiB for a whole SKILL.md and on the MCP server). - Request ids. Every answer carries
x-request-id(send your own, 1–128 characters of letters, digits,._:-); quote it when you report a problem.
The catalogue
43 operations. Each takes a JSON object (its input_schema) and answers JSON; a refusal is a problem (below) over REST and an error result over MCP. The scope column is the least a key needs; a command the operation queues may need more (send_command with logs.tail needs fleet:admin).
| Operation | Scope | What |
|---|---|---|
describe_cloudreads | fleet:read | Overview: what needs attentionCall this first. The organisation and this key's scopes; every machine (status, mode, runners not ready); what needs a person now: agents waiting for an answer (with their question), open alerts, failing or warning health checks, jobs that failed and webhooks rejected in the last 24 hours; the day's numbers (jobs, cost, deliveries); and next_steps naming the tools that act on each. |
get_statsreads | fleet:read | StatisticsThe Stats page for every machine: per day of the window, jobs (succeeded, failed, needing a person), agent cost, tokens, p50/p95 duration and deliveries (accepted, rejected, skipped, via hosted URLs); totals with the success rate; and each skill's runs, failures, cost, p95 and last run. |
list_alertsreads | fleet:read | List alertsConditions worth a person's attention, newest first: an agent waiting for a person (needs_human), a machine gone offline, a failed job, a failing health check. They resolve themselves when the condition clears. state: resolved lists recently resolved ones. |
dismiss_alertacts | fleet:run | Dismiss an alertResolve an open alert by hand (it opens again only if its condition clears and recurs). Alerts also resolve themselves: answering a waiting agent resolves its needs_human alert. |
list_machinesreads | fleet:read | List machinesEvery paired machine: status (online, degraded, offline), mode (observe = read-only, control = accepts actions), link state, runner readiness, health summary, skillhook version, last seen. |
get_machinereads | fleet:read | Get a machineOne machine as its page shows it: the link (last sync, clock skew, events waiting), its policy (mode, allow/deny lists), failing or warning health checks with their fix, runner readiness, skills (and those that failed to load), schedules, linked repositories, running jobs and a day of its own stats; for admin keys its skillhook.json as last reported. |
rename_machineacts | fleet:admin | Rename a machineChange the name the cloud shows for a machine (its hostname stays what it reports). |
disconnect_machineacts, irreversibly | fleet:admin | Disconnect a machineRevoke a machine's token and cancel its pending commands: it stops syncing at once. Its history stays; pairing again creates a new machine. Confirm with the person first. |
list_skillsreads | fleet:read | List skillsSkills on every machine (or one): runner, model, auth type and whether its secret is set, when-filters, schedule, whether webhooks reach it, its hosted URL (on/off, never the URL) and its last job. |
get_skillreads | fleet:run | Read a skill's SKILL.mdA skill's summary and its SKILL.md fetched from the machine (the machine must be online). Edit the content and save it with save_skill. |
save_skillacts, irreversibly | fleet:admin | Save a skillWrite skills/<skill>/SKILL.md on a machine, creating or replacing it; the machine validates it first and answers with the error when it is invalid. Not for hooks from a linked repository (change those in the repository). auth: none needs allow_unauthenticated. No secret is created: use the dashboard or `skillhook cloud secret` for that. Try a draft with test_skill first. |
delete_skillacts, irreversibly | fleet:admin | Delete a skillRemove a skill from a machine's skills/ (moved to jobs/.removed-skills/ there, where it can be restored). Its webhook answers 404 from then on. Confirm with the person first. |
run_skillacts | fleet:run | Run a skillRun an installed skill on a machine as if a webhook arrived with this payload (no signature check, no filters). wait_seconds waits for the job to finish or to ask a person. |
test_skillacts | fleet:run | Test a SKILL.mdRun a SKILL.md once on a machine without installing it (trigger test), to try a skill before save_skill. |
list_inboxreads | fleet:read | The inboxWhat the agents need from a person, what they are doing and what they did, as the dashboard's Inbox shows it. Per job: title (what the agent called it), state (needs_input, in_progress, done, failed), progress (message, percent, step, with recent_updates while it runs), headline (the result in one line), summary (Markdown), links (the event's source, pull requests, issues, messages, documents, deployments, how to test; plus the webhook delivery that started it) and, while it waits for a person, question {text, context, choices: {options, recommended, multiple}}. view: needs_input, in_progress, done, failed or all (default: needs_input when anyone waits, else all); sort: activity (latest first, the default), oldest (waiting longest), progress (furthest along) or created. counts gives how many wait and how many run. next_offset pages. |
list_jobsreads | fleet:read | List jobsJobs on every machine, newest first, with status (how the process ended), outcome (whether the task was done), failure kind, the pending question and cost. waiting: true lists only agents waiting for a person. next_before pages. |
get_jobreads | fleet:read | Get a jobA job as its page shows it: status, outcome, the agent's response and result excerpt, failure, cost and tokens, the pending question and answer, the progress timeline, the live output when it is being streamed (watch_job), artifacts fetched so far and the commands sent about it. |
answer_jobacts | fleet:run | Answer an agentAnswer a job that is waiting for a person (or that ended with outcome needs_human). answer is what the person said; option names the one of the offered options they picked (answer may repeat it), options the several they picked when the question lets a person pick more than one (they reach the agent one per line, before the answer). resume: auto (default) resumes the agent's session with the answer when the run already ended; never only records it. |
cancel_jobacts, irreversibly | fleet:run | Cancel a jobCancel a queued or running job on its machine. Confirm with the person first. |
replay_jobacts | fleet:run | Replay a jobRun a job again with the same payload through the skill as it is now (a new job, trigger replay). |
watch_jobacts | fleet:run | Stream a job's outputStream a running job's stdout (or stderr) to the cloud for ttl_seconds (default 900); get_job then shows it as output, refreshed every few seconds. |
get_job_artifactreads | fleet:run | Read a job's fileFetch one of a job's files from its machine: stdout or stderr (the agent's transcript), prompt (what it was told), result, response, payload or event (the webhook it got). Up to 256 KiB is returned (the end of stdout and stderr, the start of the others). Secrets are scrubbed on the machine; payload, event and prompt (which contain the webhook body) are withheld when the organisation keeps no bodies. |
list_deliveriesreads | fleet:read | List deliveriesWebhooks the machines received, newest first, including rejected, skipped and duplicate ones with the reason. q searches the sender's delivery id, the machine's delivery id, the reason, the code, the path, the address and the user agent (case-insensitive, anywhere); since and until bound when it was received; delivery_id is the sender's id exactly, job the machine's id of the job it started. via: http came to the machine's own URL, ingress through a hosted URL. next_before pages. |
get_deliveryreads | fleet:read | Get a deliveryOne webhook delivery: outcome and reason, HTTP status, sender, redacted headers, the job it started and, with include_body, the stored body. |
replay_deliveryacts | fleet:run | Replay a deliveryRun a stored delivery again through the skill as it is now. force is needed for deliveries that failed their checks (it skips them); skip_filters ignores the skill's when-filters. machine or skill run it elsewhere: the stored body and redacted headers go to that machine or skill as a fresh run, the way run_skill does (no signature check, no filters, so force and skip_filters do not apply); that needs the body to be stored (no_body otherwise). |
list_hosted_urlsreads | fleet:read | List hosted URLsHosted webhook URLs (the cloud receives the webhook and holds it, sealed, up to 72 hours until the machine collects it): per skill whether it is on, its key prefix, what it received and how many deliveries wait for the machine. Never the URL itself: get_hosted_url. |
get_hosted_urlreads | fleet:run | Reveal a hosted URLThe hosted URL of a machine's skill, to configure in the webhook sender (GitHub, Sentry, Granola, Stripe…). The machine still checks every delivery's signature with the skill's own secret. |
enable_hosted_urlacts, irreversibly | fleet:admin | Turn on a hosted URLCreate a hosted URL for a machine's skill, or a new one: any previous URL for the skill stops working at once (update the sender). |
disable_hosted_urlacts, irreversibly | fleet:admin | Turn off a hosted URLTurn a skill's hosted URL off: senders get 404 until it is turned on again, with a new URL. Confirm with the person first. |
list_commandsreads | fleet:read | List commandsCommands sent to the machines (by people, keys and the cloud), newest first: type, status, who asked, the ids they were about and their error. get_command has a command's result. |
send_commandacts, irreversibly | fleet:read | Send a commandSend any protocol command to a machine and wait (up to 60 s) for its result: ping, health.get {deep, refresh}, runners.get {refresh}, logs.tail {lines}, config.get, config.patch {set, unset}, service.status, service.restart {when: idle|now}, update.check, update.install, schedules.list, schedule.run {name}, stats.get {since}, secret.list, expose.status, job.list, delivery.list. Needs the scope the command requires (logs, configuration and restarts need fleet:admin); the machine's own policy decides too. |
get_commandreads | fleet:read | Get a commandA command's status and result; wait_seconds (up to 30) waits for it to finish. |
get_settingsreads | fleet:read | Organisation settingsThe organisation (name, slug, plan) and its settings: whether webhook bodies are kept (store_payloads). |
update_settingsacts | fleet:admin | Change organisation settingsRename the organisation or change whether the cloud keeps webhook bodies the machines upload (store_payloads: false keeps none from now on). |
get_billingreads | fleet:read | Plan and usageThe organisation's plan (code, status, billing period, whether a cancellation is pending), the plan's limits, what the organisation uses of each (machines, people, hosted URLs, hosted deliveries this month, notification channels, API keys) with the room left, the plan the usage fits, and where a person changes the plan. A deployment that does not enforce plans says so (enforced: false). |
create_checkout_linkacts | fleet:admin | A link to buy a planA Stripe Checkout link for the Pro or Business plan, monthly or yearly, for a person to open in a browser and pay; this call charges nothing and the link expires after 24 hours. billing_unavailable when the deployment does not sell plans or the organisation already has one (a person changes it under Billing on the dashboard). |
list_membersreads | fleet:read | List membersThe organisation's people and their roles (viewer, member, admin, owner); admin keys also see pending invitations. Inviting and changing roles is done on the dashboard. |
list_channelsreads | fleet:admin | List notification channelsWhere alerts are sent (Slack, webhook, email): which alert types, whether on, and the last delivery or error. Adding a channel is done on the dashboard. |
test_channelacts | fleet:admin | Test a notification channelSend a sample alert to a channel and record whether it arrived. |
list_audit_logreads | fleet:admin | Audit logEvery change and every command, by whom (person, API key, machine, system), newest first. Payloads, answers and file contents are never in it. action filters by an exact action or a prefix ending in a dot (command., machine., hosted_url.). |
report_issueacts | fleet:read | Report a problem to SkillhookFile a report with the Skillhook team: a bug in skillhook or Skillhook Cloud, a question or a feature request. Say what happened, what you expected and how to reproduce it, and name the machine or job it concerns. It is stored for this organisation (list_issues), acknowledged by email (contact_email, else the person who created this API key) and routed to the team, who reply by email. Returns its number and dashboard link. |
list_issuesreads | fleet:read | List problem reportsProblems this organisation reported to the Skillhook team, newest first: number, title, description, kind, severity and status. |
get_issuereads | fleet:read | Get a problem reportOne report by number (12 or #12) or id, with its diagnostics and whether its emails went out. |
Resource routes
The dashboard's nouns as routes under https://skillhook.dev/api/v1. Where a route and an operation do the same thing the table says which; the operation's input schema (GET /api/v1/tools) documents the body, and the OpenAPI document spells out every parameter.
| Tools | Scope | What |
|---|---|---|
GET /tools | fleet:read | The operation catalogueEvery operation an API key can call, with its JSON Schema, the scope it needs and whether this key has it, plus the instructions written for agents. skillhook's CLI and MCP server build their commands from this. |
POST /tools/{tool} | fleet:read | Run one operation of the catalogueThe JSON body is the operation's input (its input_schema in GET /api/v1/tools); the answer is its result. 404 unknown_tool, 403 forbidden for a scope the key lacks, 400 invalid_request for a body that fails the schema. Bodies up to 768 KiB. |
| Account | Scope | What |
|---|---|---|
GET /me | fleet:read | Whose key this isThe key's organisation (id, slug, name), its name and scopes, and the role it acts as. |
| Machines | Scope | What |
|---|---|---|
GET /machines= list_machines | fleet:read | List machinesEvery paired machine that is not disconnected: status, mode, link state, runners, health summary, versions. |
GET /machines/{machine}= get_machine | fleet:read | Get a machineOne machine by id, name or hostname, with its failing checks and its skills. |
POST /machines/{machine}/commands= send_command | fleet:read | Send a protocol commandAny protocol command (health.get, logs.tail, config.get, config.patch, service.restart, update.check, …). The key's scope and the machine's own policy decide; 202 while the machine has not answered. |
GET /commands/{command}= get_command | fleet:read | Get a commandA command's status and result; ?wait=30 holds the request until it finishes (up to 30 s). Readable with the role that may send it. |
| Skills | Scope | What |
|---|---|---|
POST /machines/{machine}/skills/{skill}/run= run_skill | fleet:run | Run a skillRun an installed skill as if a webhook arrived with this payload (no signature check, no filters). wait_seconds waits for the job to finish or to ask a person. |
POST /machines/{machine}/secrets | fleet:admin | Generate a skill's secret on the machineThe machine generates the secret and seals it to recipient_key (an X25519 public key the caller made for this request); POST /api/v1/commands/{command}/claim collects the sealed value once. The cloud never holds the value. Needs fleet:admin and a machine in control mode. skillhook cloud secret does all of it. |
GET /skills= list_skills | fleet:read | List skillsSkills on every machine, or one machine's (?machine=). |
POST /commands/{command}/claim | fleet:admin | Collect a sealed secret onceThe sealed value of a secrets request, to the key that asked, once: 202 {state: pending} until the machine answered, then {state: sealed, sealed}; afterwards claimed, or expired two minutes after the machine received the request; exists when the machine kept the secret it had; failed with the machine's error. |
| Jobs | Scope | What |
|---|---|---|
GET /inbox= list_inbox | fleet:read | The inboxWhat the agents need from a person, what they are doing and what they did: per job its title, state, progress, the result in one line, the summary, typed links (plus the webhook delivery that started it) and, while it waits, its question with choices. Without view: needs_input when anyone waits, else all. |
GET /jobs= list_jobs | fleet:read | List jobsNewest first; next_before pages. |
GET /jobs/{job}= get_job | fleet:read | Get a jobBy the cloud id or the machine's job id, with its timeline, live output, artifacts and commands. |
POST /jobs/{job}/answer= answer_job | fleet:run | Answer an agentDelivered to the waiting run, or the agent's session resumes with the answer when the run already ended (resume: auto). 202 while the machine has not confirmed. |
POST /jobs/{job}/cancel= cancel_job | fleet:run | Cancel a jobCancel a queued or running job on its machine. |
POST /jobs/{job}/replay= replay_job | fleet:run | Replay a jobRun a job again with the same payload (a new job, trigger replay). |
| Deliveries | Scope | What |
|---|---|---|
GET /deliveries= list_deliveries | fleet:read | List deliveriesWebhooks the machines received, newest first, rejected ones included; q searches the sender's delivery id, the machine's id, the reason, code, path, address and user agent; since and until bound received_at; delivery_id and job match exactly. next_before pages. |
GET /deliveries/{delivery}= get_delivery | fleet:read | Get a deliveryOne delivery with its redacted headers; ?include=body adds the stored body when the organisation keeps bodies. |
POST /deliveries/{delivery}/replay= replay_delivery | fleet:run | Replay a deliveryRun a stored delivery again. force skips the checks it failed; skip_filters ignores the skill's when-filters. With machine or skill, the stored body and redacted headers run on that target as a fresh run (no signature check, no filters); 409 no_body when the body is not stored. |
| Support | Scope | What |
|---|---|---|
POST /issues= report_issue | fleet:read | Report a problem to the Skillhook teamAny key may report. 201 with the report and whether the acknowledgement email went out; the same Idempotency-Key (at most 120 characters) from the same key returns the original with 200. 10 an hour per key, 30 per organisation. |
GET /issues= list_issues | fleet:read | List problem reportsThe organisation's reports, newest first; next_before pages. |
GET /issues/{issue}= get_issue | fleet:read | Get a problem reportBy number (12 or #12) or id, with its diagnostics and whether its emails went out. |
| Billing | Scope | What |
|---|---|---|
GET /pricing | no key | The plansEvery plan with its prices (cents, USD) and limits, and whether this deployment enforces them. No key needed. |
GET /billing= get_billing | fleet:read | The organisation's plan and usageThe plan in force, its limits, what the organisation uses of each with the room left, the plan the usage fits, and where a person changes the plan. |
POST /api/mcp: Streamable HTTP, stateless: one JSON-RPC request per POST (initialize, tools/list, tools/call). The tools are the catalogue's operations this key's scope allows; a tool's error is a tool result with isError, never a JSON-RPC error. GET answers 405, DELETE 200 (no sessions). MCP and plugins says how to connect.
Errors
Every failure is RFC 9457 application/problem+json: type links to the code's entry below, title and detail say what happened, code is for programs and request_id for support. A failure on our side that a retry may fix is 503 unavailable with Retry-After, never a 401 or a 404.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | No credential: send an organisation API key as Authorization: Bearer shc_…. |
invalid_key | 401 | The key is unknown, revoked or expired. Create a new one under Settings → API keys. |
forbidden | 403 | The key's scope does not allow this operation, or the role behind it cannot send that command. |
invalid_request | 400 | The body or a parameter failed validation; the detail names the field. |
invalid_json | 400 | The body is not JSON. |
invalid_args | 400 | A command's arguments do not match the protocol (send_command, run_skill…). |
unknown_command | 400 | No such protocol command type, or (404) no such command id in this organisation. |
unknown_tool | 404 | No operation of that name; GET /api/v1/tools lists them. |
unknown_machine | 404 | No machine with that id, name or hostname in this organisation. |
unknown_job | 404 | No job with that cloud id or machine job id in this organisation. |
unknown_delivery | 404 | No delivery with that id in this organisation. |
unknown_alert | 404 | No open alert with that id. |
unknown_channel | 404 | No notification channel with that id. |
unknown_issue | 404 | No problem report with that number or id. |
no_hosted_url | 404 | The skill has no hosted URL turned on; enable_hosted_url makes one. |
not_found | 404 | The machine, job, delivery or invitation named does not exist in this organisation. |
ambiguous_machine | 409 | More than one machine has that name; use its id. |
ambiguous_job | 409 | More than one job has that machine job id; use the cloud id. |
ambiguous_delivery | 409 | More than one delivery has that machine delivery id; use the cloud id. |
revoked | 409 | The machine was disconnected; pair it again to send it commands. |
denied_by_machine | 409 | The machine's mode or its allow/deny lists refuse this command; the detail says which. |
no_body | 409 | The delivery's whole body is not stored here (the organisation keeps no webhook bodies, the machine keeps them home, the plan's retention window passed, or it was too large or binary), so it cannot run on another machine or skill. Replay it where it arrived instead. |
plan_limit | 402 | The organisation's plan has no room for one more of what was asked (a machine, a person, a hosted URL, a key); the detail names the limit. Upgrade under Billing, or make room. |
billing_unavailable | 409 | Plans are not sold on this deployment, or the organisation already has one: a person changes it under Billing on the dashboard. |
too_large | 413 | The body is over the limit (512 KiB; 768 KiB for a whole SKILL.md and on /api/mcp). |
rate_limited | 429 | Over the limit: 600 requests a minute per key, 10 problem reports an hour per key, 5 channel tests a minute per channel. Wait Retry-After seconds. |
internal | 500 | Something went wrong on our side; quote the request_id when you report it. |
unavailable | 503 | The service could not reach its database; retry after Retry-After seconds. Never a judgement on your key. |