Documentation

Using openapi.json

Generate a client, import it into ChatGPT Actions, Postman or an agent.

Where it is

The REST API is described in OpenAPI 3.1 at /openapi.json. It is generated from the operation catalogue every time it is served, so it is always current: every catalogue operation with its input schema, and the resource routes under /api/v1. Its servers entry is this deployment.

Authentication

One security scheme, bearerAuth (HTTP bearer): an organisation API key as Authorization: Bearer shc_… on every request, never in a URL. The key's scope decides which operations answer 403; see API keys and scopes.

Generating a client

openapi-typescript turns the document into types; openapi-fetch is a typed client over them.

ChatGPT custom GPT Actions

  1. Create a GPT → Configure → Actions → Create new action → Import from URL, and paste the document's URL above.
  2. Authentication: API Key, Auth Type Bearer, and paste a key. Give it the narrowest scope the GPT needs: fleet:read to look, fleet:run to answer agents and replay.
  3. The GPT then acts as that key, within its organisation and scope, and every call is in the audit log under the key's name.

Postman

File → Import → paste the document's URL. On the imported collection, set Authorization to Bearer Token with the key, and every request inherits it. Keep the key as a collection variable marked secret rather than in a request.

Reading the catalogue at run time

A client that builds itself when it runs, the way skillhook's CLI and MCP server do, reads the catalogue instead of the OpenAPI document: one request, every operation with its JSON Schema (draft 2020-12, derived the way the MCP SDK derives a tool's), the scope it needs, whether this key may call it, and the instructions for agents. The shape is versioned (version: 1): it changes only when an older client would misread it.

GET/api/v1/tools
API key · {version, organisation, key, instructions, tools: [{name, title, description, scope, kind, allowed, input_schema}]}
POST/api/v1/tools/{name}
API key · The JSON body is the operation's input; the answer is its result

The error contract

Every refusal is an RFC 9457 problem: application/problem+json with type (a link to the matching anchor of the API reference), title, status, code, detail and request_id. Credentials are never echoed. Over MCP the same refusals are tool results with isError and the message.

CodeTypeDescription
400 invalid_request
problem
The input does not match the operation's schema; detail says which field.
400 invalid_json
problem
The body is not JSON.
401 unauthorized
problem
No bearer token. The answer carries WWW-Authenticate: Bearer.
401 invalid_key
problem
The key is unknown, revoked or expired.
403 forbidden
problem
The operation needs a wider scope; detail names it.
404 unknown_tool
problem
No such operation; GET /api/v1/tools lists them.
404 not_found
problem
No such machine, job, delivery or command in this organisation.
409 revoked
problem
The machine was disconnected; pair it again to send it commands.
409 denied_by_machine
problem
The machine's mode or its allow and deny lists refuse the command.
413 too_large
problem
The body is over the limit: 768 KiB for a tool call and for MCP, 512 KiB on the other routes.
429 rate_limited
problem
Too many requests from this key; Retry-After says when.
500 internal
problem
A bug on our side, logged and reported; quote the request id.
503 unavailable
problem
We could not reach our database; retry after Retry-After. Never a 401 or 404 when we could not look.

Request ids

Every answer carries an x-request-id header, and every problem the same id in its body. Send your own with an X-Request-Id header (letters, digits, ., _, : and -, up to 128 characters) and it is used instead; otherwise one is generated. Quote it when you report a problem (report_issue, or Support on the dashboard): it is what the logs on our side are keyed by.