Docs navigation

API

API reference

Generated from the OpenAPI specification Lunary Cloud publishes (v1.0.0). Also browsable in Swagger UI.

The customer-facing REST API for Lunary Cloud.

Authentication. Every endpoint requires a bearer token minted from the dashboard (Access → API tokens): Authorization: Bearer <token>. A token is scoped to one customer and carries scopesread_instances, write_instances, read_plugs, write_plugs, read_agent_sessions, write_agent_sessions, read_billing — and an optional expiry, so a read-only token can be issued for an agent or CI job.

Identifiers. Instances are addressed by slug (unique per customer), agent sessions by the reporter's external_id, runs by an integer id (int64). Cursors and artifact keys are opaque — echo them back verbatim.

Errors. Every failure returns the envelope { "error": "<code>" }, with an optional details object on some codes. Validation failures are 422 invalid with details.fields mapping each field to its problems. The code vocabulary is closed and additive: new codes may appear, existing ones won't be renamed. Common everywhere: 401 unauthorized (bad/expired token), 403 insufficient_scope (valid token, missing scope — details. required names it), 404 not_found, 429 rate_limited, and 400 malformed_json (unparseable body).

Pagination. List endpoints return { "data": [...], "meta": { "next_cursor": <opaque|null>, "limit": <n> } }. Page by echoing next_cursor back as ?after=; a null cursor is the last page.

Rate limits. Budgets are per customer and per class — reads and writes are metered separately, with writes stricter. Responses carry X-RateLimit-Limit / X-RateLimit-Remaining for the class they drew from; a 429 includes Retry-After (seconds).

Async writes. Provisioning, teardown, and plug runs are asynchronous. Teardown and plug runs answer 202 with data.run_id and a Location pointing at the run; provisioning answers 201 with Location pointing at the created instance and the apply run in meta.run_id. Either way, poll GET /runs/{run_id} for the outcome.

Enums. Fields documented with an enum keyword (managed_by, environment, health.status) are closed sets. Lifecycle fields (run and agent-session status, source, trigger, billing state) are open sets — their current values are listed in each description, new values may appear over time, and clients should treat unknown values as "other" rather than failing. Documented terminal sets (runs, agent sessions) are stable.

Idempotency. The Idempotency-Key header is reserved for write endpoints but not yet honored — today it is ignored, so do not rely on it for retry safety yet. Retry guidance until then: POST /instances answers a retried create with 409 slug_taken (treat as "the first request succeeded" and GET /instances/{slug}); POST /agent-sessions replays the existing session with 200.

Agent Sessions

GET /api/v1/agent-sessions List agent sessions

Recent sessions, newest first. Requires read_agent_sessions. Cursor-paged.

Parameters

status query · string

Narrow to one status.

limit query · integer

Page size (default 25, max 100).

after query · string

Opaque cursor from a prior meta.next_cursor.

Responses

  • 200 A page of sessions AgentSessionList
  • 400 Malformed cursor Error
POST /api/v1/agent-sessions Register a requested agent session

Called the moment a delegation is triggered, before any work starts. A session that never leaves requested is the visible form of a dropped event. Idempotent on external_id: re-registering an existing session returns it with 200 (in its current status, which may have progressed past requested) instead of an error, so a fail-open reporter can re-send unconditionally. Requires write_agent_sessions.

Request body

AgentSessionCreateRequest (application/json) — Session

Responses

  • 200 The already-registered session AgentSessionResponse
  • 201 The session AgentSessionResponse
  • 422 Validation failed Error
GET /api/v1/agent-sessions/{external_id} Get one agent session

By the reporter's external_id. Requires read_agent_sessions.

Parameters

external_id required path · string

The reporter's session handle.

Responses

  • 200 The session AgentSessionResponse
  • 404 Unknown session Error
POST /api/v1/agent-sessions/{external_id}/transitions Report an agent session status change

Moves a session to running or a terminal status (succeeded, no_changes, blocked, failed). Terminal states are sticky — a duplicate terminal report gets 409, which a fail-open reporter can ignore. Requires write_agent_sessions.

Parameters

external_id required path · string

The reporter's session handle.

Request body

AgentSessionTransitionRequest (application/json) — Transition

Responses

  • 200 The session AgentSessionResponse
  • 404 Unknown session Error
  • 409 Already terminal Error
  • 422 Validation failed Error

Billing

GET /api/v1/billing/invoices List invoices

Requires the read_billing scope. Reads the local projection of Stripe webhook events (no live Stripe call), newest first. Cursor-paged.

Parameters

limit query · integer

Page size (default 25, max 100).

after query · string

Opaque cursor from a prior meta.next_cursor.

Responses

  • 200 A page of invoices BillingInvoiceList
  • 400 Malformed cursor Error
  • 401 Missing/invalid token Error
  • 403 Token lacks read_billing Error
GET /api/v1/billing/payment-method Get the default payment method

Requires the read_billing scope. Served from Stripe-mirrored state; all fields are null until a payment method is on file.

Responses

  • 200 The payment method BillingPaymentMethodResponse
  • 401 Missing/invalid token Error
  • 403 Token lacks read_billing Error
GET /api/v1/billing/subscription Get the subscription

Requires the read_billing scope. Served from Stripe-mirrored state — current plan, status, and renewal/cancellation dates, with no live Stripe call.

Responses

  • 200 The subscription BillingSubscriptionResponse
  • 401 Missing/invalid token Error
  • 403 Token lacks read_billing Error

Instances

GET /api/v1/instances List instances

Requires the read_instances scope. Cursor-paged.

Parameters

limit query · integer

Page size (default 25, max 100).

after query · string

Opaque cursor from a prior meta.next_cursor.

Responses

  • 200 A page of instances InstanceList
  • 400 Malformed cursor Error
  • 401 Missing/invalid token Error
  • 403 Token lacks read_instances Error
POST /api/v1/instances Provision an instance

Requires the write_instances scope. Quota-gated; the deploy spec is built from the catalog preset server-side; catalog_slug custom runs an image you supply, described by the typed custom object (never a compose file). Provisioning is async — the response's meta.run_id is the apply run to poll via GET /runs/{run_id}. A slug that already exists returns 409 slug_taken: on a retried timeout, treat that as "the first request succeeded" and GET /instances/{slug}. The instance comes up with exposure internal; public exposure is enabled by Lunary after deploy, so public and domain are rejected (422 not_settable). Refused while the customer's VM is offline (409 tether_offline) — nothing is provisioned. The body is validated before the VM is checked, so a malformed request is 422 even while the VM is offline. Every error carries a details.message; slug_taken also carries details.slug, invalid per-field details.fields.

Request body

CreateInstanceRequest required (application/json) — Instance to provision

Responses

  • 201 The provisioned instance InstanceCreateResponse
  • 409 `slug_taken`, `tether_offline`, `retained_app_mismatch`, or `undecryptable_retained_data` Error
  • 422 `not_settable`, `over_budget`, `not_provisionable`, `missing_catalog_slug`, `missing_secrets`, `no_free_port`, `invalid`, or `operation_failed` Error
DELETE /api/v1/instances/{slug} Tear down an instance

Requires the write_instances scope. Only operator-managed instances can be torn down; the teardown runs async, so a run id is returned — poll it via GET /runs/{run_id}.

Parameters

slug required path · string

Instance slug.

remove_volumes query · boolean

Purge the instance's data instead of retaining it (default false).

Responses

  • 202 Teardown enqueued RunAccepted
  • 404 No such instance for this customer Error
  • 422 Not operator-managed, or invalid Error
GET /api/v1/instances/{slug} Get one instance

Requires the read_instances scope.

Parameters

slug required path · string

Instance slug.

Responses

  • 200 The instance InstanceResponse
  • 404 No such instance for this customer Error

Plugs

GET /api/v1/instances/{instance_slug}/plugs List an instance's plugs

What can be run on this instance: configured (plugs configured on it) and catalog — every customer-facing plug its app supports, with its summary, required params, and whether it is dangerous, supports a dry run, produces artifacts, or can be scheduled. The response is always complete — the set is bounded by the app's plug catalog, so it is never paginated. Internal lifecycle plugs are never listed. Requires the read_plugs scope.

Parameters

instance_slug required path · string

Instance slug.

Responses

  • 200 Configured and available plugs PlugsResponse
  • 404 No such instance for this customer Error
POST /api/v1/instances/{instance_slug}/plugs/{plug}/runs Run a plug

Creates a run of this plug and returns its id — poll it via GET /runs/{run_id}. A plug the customer may configure but hasn't is configured on first run, so this is always one step; a plug that was explicitly disabled returns 409. Optionally takes params (a one-off override of the stored configuration — the stored row is not modified) and dry_run. 404 means an unknown or unowned instance, or a plug that isn't customer-triggerable. Requires the write_plugs scope.

Parameters

instance_slug required path · string

Instance slug.

plug required path · string

The plug to run.

Request body

TriggerPlugRequest (application/json) — Trigger options

Responses

  • 202 Run enqueued RunAccepted
  • 404 Unowned instance, or a plug that isn't customer-triggerable Error
  • 409 The plug is disabled on this instance Error
  • 422 Invalid plug parameters, a dry run the plug doesn't support, or a backup/snapshot refused because the plan's artifact allowance is used up (`over_budget`) Error

Registries

GET /api/v1/registries List registry credentials

The caller's private-registry credentials — host and username only; the token is never returned. Requires the read_instances scope.

Responses

  • 200 Registry credentials RegistryCredentialList
POST /api/v1/registries Store a registry credential

Stores a pull credential for a private image registry, encrypted at rest. It is released only to your own VM while an instance whose image lives on that host is deployed or redeployed. One per host: a second for the same host is 409 host_taken — delete the first to rotate. Requires the write_instances scope.

Request body

CreateRegistryCredentialRequest required (application/json) — Credential to store

Responses

  • 201 The stored credential RegistryCredentialResponse
  • 409 `host_taken` Error
  • 422 `invalid` (per-field `details.fields`) Error
DELETE /api/v1/registries/{id} Delete a registry credential

Removes the credential. Instances already running keep their pulled image; the next deploy or redeploy of an instance on that host fails until a new credential is stored. Requires the write_instances scope.

Parameters

id required path · integer

Responses

  • 204 Deleted
  • 404 No such credential for this customer Error

Routines

GET /api/v1/routines List routines

All of the caller's routines, each naming its workflow, its target instance, its cadence (cron is the exact expression, UTC), and the last thing the scheduler did with it. Bounded by the per-customer routine limit (50), so it is never paginated. Requires the read_plugs scope.

Parameters

instance query · string

Only routines targeting this instance slug.

Responses

  • 200 Routines RoutineList
POST /api/v1/routines Create a routine

Binds a stock workflow to an instance, optionally on a cadence. cadence is one of on_demand/hourly/daily/weekly/monthly/custom; custom takes the exact cron expression (UTC). A workflow whose plug requires confirmation to repeat unattended (restore, redeploy) needs acknowledge: true — the acknowledgement is recorded against the calling token, never a caller-supplied name. Refused with routine_limit at the per-customer cap (50). Requires the write_plugs scope.

Request body

CreateRoutineRequest (application/json) — The routine

Responses

  • 201 Created RoutineResponse
  • 409 The workflow's plug is disabled on the target Error
  • 422 Refused — see the `error` code Error
DELETE /api/v1/routines/{id} Delete a routine

Parameters

id required path · integer

Responses

  • 204 Deleted
  • 404 No such routine for this customer Error
GET /api/v1/routines/{id} Get a routine

Parameters

id required path · integer

Responses

  • 200 The routine RoutineResponse
  • 404 No such routine for this customer Error
PATCH /api/v1/routines/{id} Update a routine

Changes any of name, cadence/cron, params, overlap, enabled (the customer's own switch), and instance — which retargets the routine. Changing the workflow or params clears a recorded acknowledgement unless the same call carries acknowledge: true. Requires the write_plugs scope.

Parameters

id required path · integer

Request body

UpdateRoutineRequest (application/json) — Fields to change

Responses

  • 200 Updated RoutineResponse
  • 404 No such routine for this customer Error
  • 409 The workflow's plug is disabled on the target Error
  • 422 Refused — see the `error` code Error
POST /api/v1/routines/{id}/resume Resume a paused routine

Clears a system pause (paused_reason). The customer's own enabled switch is separate — flip it via PATCH. Refused while the workflow's plug is switched off: turning the plug back on is what resumes those. Requires the write_plugs scope.

Parameters

id required path · integer

Responses

  • 200 Resumed RoutineResponse
  • 404 No such routine for this customer Error
  • 409 The workflow's plug is disabled on the target Error
POST /api/v1/routines/{id}/runs Run a routine now

Creates a run of the routine's workflow against its target and returns the run id — poll it via GET /runs/{run_id}. Requires the write_plugs scope.

Parameters

id required path · integer

Responses

  • 202 Run enqueued RunAccepted
  • 404 No such routine for this customer Error
  • 409 The workflow's plug is disabled on the target Error

Runs

GET /api/v1/runs List runs

Recent runs, newest first. Requires the read_plugs scope. Cursor-paged.

Parameters

instance_slug query · string

Narrow to one instance's runs.

limit query · integer

Page size (default 25, max 100).

after query · string

Opaque cursor from a prior meta.next_cursor.

Responses

  • 200 A page of runs RunList
  • 400 Malformed cursor Error
GET /api/v1/runs/{id} Get one run

The run with what it did: steps and artifacts. Pass ?include=context for the plug's raw threaded result — it is opt-in because it is sensitive: a plug may deliberately return a secret there (reset_admin_password returns the new password once), and its shape is plug-defined with no compatibility guarantee. Requires the read_plugs scope.

Parameters

id required path · integer

Run id.

include query · string

Comma-separated extra fields to include. Supported: context. Unknown values are a 400 invalid_include.

Responses

  • 200 The run RunResponse
  • 400 Unknown include value Error
  • 404 No such run for this customer Error
GET /api/v1/runs/{run_id}/artifacts/download Get a download URL for a run's artifact

A short-lived presigned URL for one artifact the run recorded. The key must be one this run actually produced — anything else reads as 404, including another customer's run. Requires the read_plugs scope.

Parameters

run_id required path · integer

Run id.

key required query · string

The artifact's object key, as returned by GET /runs/:id.

Responses

  • 200 A time-limited download URL ArtifactDownloadResponse
  • 404 No such artifact on a run of this customer Error
  • 503 The artifact store isn't configured Error