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 scopes — read_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
/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 |
Responses
-
200
A page of sessions
AgentSessionList -
400
Malformed cursor
Error
/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
/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
/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
/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 |
Responses
-
200
A page of invoices
BillingInvoiceList -
400
Malformed cursor
Error -
401
Missing/invalid token
Error -
403
Token lacks read_billing
Error
/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
/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
/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 |
Responses
-
200
A page of instances
InstanceList -
400
Malformed cursor
Error -
401
Missing/invalid token
Error -
403
Token lacks read_instances
Error
/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
/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
/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
/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
/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
/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
/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
/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
/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
/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
/api/v1/routines/{id}
Delete a routine
Parameters
id
required
|
path · integer |
Responses
- 204 Deleted
-
404
No such routine for this customer
Error
/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
/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
/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
/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
/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 |
Responses
-
200
A page of runs
RunList -
400
Malformed cursor
Error
/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: |
Responses
-
200
The run
RunResponse -
400
Unknown include value
Error -
404
No such run for this customer
Error
/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 |
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