apistockdocs
v0.4 GitHub apistock.dev
Guides/Operate

Ops API

Admin APIs of the Full preset (internal/modules/ops), implemented in examples/full-single. The full schema is in the app's api/openapi.json and at /docs. Decisions: ADR-0026, ADR-0031, ADR-0033, ADR-0036, ADR-0037, ADR-0038, ADR-0040.

Authentication#

/ops/* requests use a signed-in session: a browser's session cookie, or a bearer token from POST /v1/auth/login with "transport": "bearer". The account needs a platform role (authentication guide):

terminal
go run ./cmd/api grant-role you@example.com platform_admin
TOKEN=$(curl -s -X POST http://127.0.0.1:8080/v1/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"your password","transport":"bearer"}' | jq -r .token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/ops/settings
Situation Response
No or invalid session 401 unauthenticated
Signed in without the operation's permission 403 forbidden
Role platform_admin Every ops permission
Role ops_viewer ops.settings.read, ops.jobs.read, ops.audit.read, ops.releases.read, ops.mail.read

Changes are attributed to the signed-in user in history, job metadata and audit events.

Permissions#

Permission Allows
ops.settings.read List and read settings and their history
ops.settings.write Change and reset settings
ops.jobs.read Read job definitions, scheduled jobs, runs and queues
ops.jobs.write Change and reset job configuration; pause and resume queues
ops.jobs.run Run a job now, retry or cancel a run
ops.audit.read List and read audit events
ops.releases.read List releases and the instances running them
ops.mail.read See how the app sends email
ops.mail.test Send a test email
ops.auth.read See which sign-in methods are configured

Missing permission: 403 forbidden.

Conventions#

  • Errors are application/problem+json with code, detail and request_id.
  • Changes send the version they last read; a newer version returns a *_version_conflict error. Read again and retry.
  • Unknown request fields are ignored.

Runtime settings#

Method and path Purpose Success
GET /ops/settings?group= List settings 200 {settings: [...]}
GET /ops/settings/{key} One setting 200
PUT /ops/settings/{key} Change: {value, version, reason?} 200
DELETE /ops/settings/{key} Reset to default: {version, reason?} 200
GET /ops/settings/{key}/history?before=&limit= Changes, newest first 200 {changes: [...]}
terminal
curl -X PUT http://127.0.0.1:8080/ops/settings/example.ping_message \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"value":"hello","version":0,"reason":"demo"}'
json
{
  "key": "example.ping_message",
  "kind": "string",
  "group": "example",
  "description": "Reply of GET /v1/ping. …",
  "value": "hello",
  "default": "pong",
  "modified": true,
  "invalid_stored_value": false,
  "version": 1,
  "updated_at": "2026-09-14T12:00:00Z",
  "updated_by": "usr_mfrggzdfmztwq2lk",
  "reason_required": false,
  "restart_required": false,
  "restart_pending": false,
  "constraints": {"max_len": 100}
}

Job definitions#

Method and path Purpose Success
GET /ops/jobs/definitions All definitions with effective and default config, next and last run 200 {definitions: [...]}
GET /ops/jobs/scheduled Enabled scheduled jobs, soonest first 200 {definitions: [...]}
GET /ops/jobs/definitions/{name} One definition 200
PUT /ops/jobs/definitions/{name} Change only the sent fields: {enabled?, schedule?, timeout?, max_attempts?, queue?, priority?, version, reason?} 200
DELETE /ops/jobs/definitions/{name} Reset to code defaults: {version, reason?} 200
GET /ops/jobs/definitions/{name}/history?before=&limit= Changes: action, old_config, new_config, reason, actor 200 {changes: [...]}
POST /ops/jobs/definitions/{name}/run Run now 202 with the run
terminal
curl -X PUT http://127.0.0.1:8080/ops/jobs/definitions/heartbeat \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"schedule":"@every 2h","timeout":"30s","version":0,"reason":"less noise"}'
json
{
  "name": "heartbeat",
  "description": "Logs a heartbeat. …",
  "config":   {"enabled": true, "schedule": "@every 2h", "timeout": "30s", "max_attempts": 3, "queue": "default", "priority": 1},
  "defaults": {"enabled": true, "schedule": "@every 1h", "timeout": "1m0s", "max_attempts": 3, "queue": "default", "priority": 1},
  "modified": true,
  "invalid_override": false,
  "version": 1,
  "updated_by": "usr_mfrggzdfmztwq2lk",
  "next_run_at": "2026-09-14T14:00:00Z",
  "last_run": {"id": 42, "kind": "heartbeat", "state": "completed", "attempt": 1, "…": "…"}
}

A reason is required to disable a job or change an enabled job's schedule.

Job runs#

Method and path Purpose Success
GET /ops/jobs/runs?kind=&queue=&state=&limit=&cursor= Runs, newest first; state is comma-separated: available, cancelled, completed, discarded, pending, retryable, running, scheduled 200 {jobs: [...], next_cursor?}
GET /ops/jobs/runs/{id} One run with attempt errors 200
POST /ops/jobs/runs/{id}/retry Make it run again now 200
POST /ops/jobs/runs/{id}/cancel Cancel; a running job's context is cancelled 200

Run fields: id, kind, queue, state, attempt, max_attempts, priority, created_at, scheduled_at, attempted_at, finalized_at, errors[{at, attempt, message}], request_id, actor_kind, actor_id. Arguments are never returned.

Queues#

Method and path Purpose Success
GET /ops/queues Active queues: name, paused, paused_at, created_at, updated_at 200 {queues: [...]}
POST /ops/queues/{name}/pause Stop every instance fetching from the queue 204
POST /ops/queues/{name}/resume Resume it 204

Audit log#

Method and path Purpose Success
GET /ops/audit?actor_kind=&actor_id=&action=&action_prefix=&resource_type=&resource_id=&org_id=&outcome=&request_id=&from=&to=&limit=&cursor= Events, newest first; filters combine; from (inclusive) and to (exclusive) are RFC 3339 times compared with occurred_at 200 {events: [...], next_cursor?}
GET /ops/audit/{id} One event 200
terminal
curl -H "Authorization: Bearer $TOKEN" \
  'http://127.0.0.1:8080/ops/audit?action_prefix=settings.&limit=20'
json
{
  "events": [
    {
      "id": 7,
      "occurred_at": "2026-09-14T12:00:00.123Z",
      "recorded_at": "2026-09-14T12:00:00.125Z",
      "actor_kind": "user",
      "actor_id": "usr_mfrggzdfmztwq2lk",
      "action": "settings.value.changed",
      "resource_type": "setting",
      "resource_id": "example.ping_message",
      "outcome": "success",
      "request_id": "req_99c4a38756b2eb8f",
      "trace_id": "951ff1fe97c8f8616496d020314fea38",
      "metadata": {"reason": "demo", "reset": false, "version": 1}
    }
  ],
  "next_cursor": "7"
}
  • request_id links an event to its access log line, trace and any jobs the request enqueued.
  • ip and user_agent appear on events recorded during a request, such as sign-ins.
  • Metadata values under sensitive keys such as password or token are stored as "[REDACTED]"; oversized metadata is replaced with {"metadata_dropped": "too_large"}.
  • Events can't be changed. Retention policies arrive in v0.5.

Releases#

Every instance records its build when it starts (version, commit, build time, whether the tree had uncommitted changes, Go version, host), sends a heartbeat every 30 seconds and marks itself stopped when it shuts down cleanly. An instance is running until it stops or misses three heartbeats, so crashed instances drop out after about 90 seconds. Instances last seen more than 90 days ago are deleted.

Method and path Purpose Success
GET /ops/releases?limit=&cursor= Releases (one per version and commit), newest first: first_started_at, last_seen_at, running, starts, modified 200 {releases: [...], next_cursor?}
GET /ops/releases/current Releases running now, each with its running instances; more than one during a rolling deploy 200 {releases: [...]}
GET /ops/releases/instances?version=&commit=&running=&limit=&cursor= Instance starts, newest first 200 {instances: [...], next_cursor?}
terminal
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/ops/releases/current
json
{
  "releases": [
    {
      "version": "v1.4.0",
      "commit": "3f9a1c2b7d4e8a90",
      "instances": [
        {"id": 12, "instance_id": "9b1c…", "version": "v1.4.0", "commit": "3f9a1c2b7d4e8a90",
         "build_time": "2026-09-15T09:58:00Z", "modified": false, "go_version": "go1.26.1", "host": "acme-api-7d9f8-x2kq",
         "started_at": "2026-09-15T10:02:11Z", "last_seen_at": "2026-09-15T10:31:41Z", "running": true}
      ]
    }
  ]
}

Builds without version control information or a link-time version show "version": "dev" and no commit.

Email#

Method and path Purpose Success
GET /ops/mail Provider (resend or smtp), delivery (mailpit or provider), non-secret details from the environment, and the current from_name, from_email, reply_to 200
POST /ops/mail/test Queue a test email: {to} 202 {status: "queued", to, delivery}
json
{"provider": "smtp", "delivery": "provider", "details": {"host": "smtp.postmarkapp.com", "port": "587", "tls": "starttls", "auth": "username and password"},
 "from_name": "Acme", "from_email": "hello@acme.com", "reply_to": "support@acme.com"}

Resend details are {"api_key": "configured"} or "missing". Change the sender with PUT /ops/settings/mail.from_email (and mail.from_name, mail.reply_to). The test email's delivery appears in GET /ops/jobs/runs?kind=apistock.mail.send. Setup: email guide.

Sign-in methods#

Which sign-in methods this deployment has configured, and what turns the others on (ADR-0045, sign-in provider setup). Permission ops.auth.read (ops_viewer, platform_admin).

Endpoint Purpose Success
GET /ops/auth/providers Each method: key (email_password, authenticator_app, passkeys, passkeys_ios, passkeys_android, google, google_ios, google_android, apple, apple_ios), name, enabled, detail for enabled methods (relying party ID and origins, app IDs, Android packages), missing environment variables and the guide section for the others 200 {methods: [...]}

Values of secrets are never returned; the same report is printed at start in development and by go run ./cmd/api auth-providers.

Error codes#

Code Status When
unauthenticated 401 No valid session
forbidden 403 Missing permission
not_found 404 No such route
validation_failed 422 Request doesn't match the schema
setting_not_found 404 Unknown setting key
setting_version_conflict 409 Setting changed since it was read
setting_reason_required 422 Reason missing for a setting that requires one
invalid_setting_value 422 Value fails the setting's type or validation; detail says why
job_definition_not_found 404 Unknown job name
job_definition_version_conflict 409 Definition changed since it was read
job_reason_required 422 Reason missing to disable or reschedule
invalid_job_config 422 Schedule, timeout, attempts or priority out of bounds; detail says why
job_definition_disabled 409 Run now on a disabled job
job_not_found 404 Unknown run ID (or removed by retention)
queue_not_active 422 No worker runs that queue
invalid_cursor 400 Malformed cursor
invalid_job_state 422 Unknown state filter
audit_event_not_found 404 Unknown audit event ID (or removed by retention)
invalid_audit_filter 422 Unknown outcome, malformed action_prefix, or from not before to
invalid_recipient 422 The test email recipient isn't an email address

Error codes are public API: new ones are added, existing ones never change.

Audit actions#

Action Resource
settings.value.changed setting
jobs.definition.changed job_definition
jobs.definition.run_requested job_definition
jobs.run.retried job
jobs.run.cancelled job
jobs.queue.paused job_queue
jobs.queue.resumed job_queue
mail.test.requested mail (ID: the provider; the recipient is not recorded)

examples/full-single stores these events in the audit_events table (modules/auditpg) and lists them with GET /ops/audit. Reading the audit log is not itself audited.

esc
↑↓ move↵ openesc close