API reference¶
The authoritative reference is generated from the code, so it is always in sync with what is deployed:
- Interactive Swagger UI: /api/docs/ (try requests with your own key)
- Raw spec: /api/openapi.yaml (OpenAPI 3, for client generators and tooling)
Authentication¶
Every request carries an organization-scoped bearer key:
Authorization: Bearer lmt_...
Keys are created in Settings, API keys and shown once; only a hash is
stored. Revoking a key takes effect immediately. Keys come in two scopes:
full (everything below) and ingest-only (record runs and list
automation slugs, nothing else; deploy these inside runners and scripts so
a leaked key cannot read your ledger). A missing or invalid key gets
401; all error responses are {"error": "..."} with a message written
for a human.
Rate limits¶
Requests are counted per organization, across all of its keys and every API surface (REST, MCP, OTLP traces, inbound webhooks), in fixed one-minute windows:
| Plan | Limit |
|---|---|
| Free | 60 requests / minute |
| Team & MSP | 300 requests / minute |
| Business | 1,000 requests / minute |
| Enterprise | custom (1,000 by default) |
Over the limit, requests receive 429 with a Retry-After header giving
the seconds until the window resets; back off that long and retry. A
rate-limit 429 always carries Retry-After; the monthly event-cap 429
(Recording runs) never does: that one means the
plan's ingestion ceiling was reached, not that you should slow down.
Historical loads belong on the backfill endpoint: a whole batch is one request processed as a background job, so imports do not burn the live window.
JSON or XML (content negotiation)¶
The API speaks JSON by default and XML on request, on the same endpoints. The response format is chosen by content negotiation:
Acceptheader (preferred): it sets the response format. SendAccept: application/xmlfor XML, orAccept: application/jsonfor JSON. An explicitAcceptalways wins, even over the request'sContent-Type.- No
Acceptheader (orAccept: */*): the response mirrors the request'sContent-Type. An XML request body (Content-Type: application/xml) gets an XML reply; otherwise the reply is JSON. - Request bodies: send
Content-Type: application/xmland an XML body; otherwise the body is parsed as JSON.
In short: set Accept to pin the response format; omit it and the response
follows your Content-Type. JSON is the default when neither indicates XML.
The XML convention is simple: an object is child elements keyed by field name,
a list is repeated <item> elements, and responses are wrapped in a
<response> root. So this JSON run:
{ "automation": "os-patching", "status": "success" }
is equivalent to this XML body (with Content-Type: application/xml):
<run><automation>os-patching</automation><status>success</status></run>
and Accept: application/xml returns the result, and errors, as XML
(<response><error>...</error></response>). Webhook deliveries can be XML too;
see Webhooks.
Endpoint map¶
Base URL: /api/v1/.
| Endpoint | What it does |
|---|---|
POST /runs |
Record one run event (details) |
GET /runs |
List runs, newest first; filters: automation, status, since, limit |
POST /runs/backfill |
Bulk-import historical runs, CSV or JSONL (details) |
POST /mcp |
The MCP server (stateless Streamable HTTP; see AI workloads) |
POST /otel/v1/traces |
OTLP/HTTP gen_ai trace ingest (AI workloads) |
GET /automations |
List automations and their slugs |
POST /automations |
Create an automation (plan limits apply, exactly as in the UI) |
GET /automations/{slug} |
Full detail: baseline, costs, status, freshness |
PATCH /automations/{slug} |
Partial update; send only the fields you change |
GET /automations/{slug}/cost-components |
List itemized costs |
POST /automations/{slug}/cost-components |
Add an itemized cost (five kinds; see Concepts) |
GET /automations/{slug}/baseline-steps |
List the manual process's steps |
POST /automations/{slug}/baseline-steps |
Add a baseline step (per-role, per-occurrence; see Concepts) |
GET /automations/{slug}/oversight-steps |
List the per-run oversight steps |
POST /automations/{slug}/oversight-steps |
Add an oversight step (human time still spent per run) |
GET /automations/{slug}/value-streams |
List value beyond labor (deflection, downtime, ...) |
POST /automations/{slug}/value-streams |
Add a value stream (details) |
GET /automations/{slug}/projection-versions |
The automation's frozen forecast history |
GET /summary |
Portfolio value summary, the dashboard's numbers; optional workspace filter |
GET /analytics |
Chart-ready series, the same data the analytics page draws |
GET /workspaces |
List workspaces |
POST /workspaces |
Create a workspace |
GET /roles |
Job roles and loaded hourly rates |
POST /roles |
Create a job role |
GET /periods |
Closed (frozen) months |
GET /report-links |
List shareable report links |
POST /report-links |
Create a no-login, print-ready value report URL |
POST /report-links/{id}/revoke (rpt_...) |
Kill a shared report URL immediately |
GET /webhooks |
List webhook endpoints (details) |
POST /webhooks |
Register a signed webhook endpoint |
DELETE /webhooks/{id} (whk_...) |
Retire a webhook endpoint; deliveries stop |
GET /msp/clients |
List managed client organizations (MSP plan; details) |
POST /msp/clients |
Create a managed client organization (MSP plan) |
POST /msp/clients/{id}/api-keys |
Mint an API key for a managed client (shown once; default ingest-only) |
Analytics series¶
GET /api/v1/analytics?chart=... returns the same series the in-app
analytics page draws. chart is one of monthly_value,
cumulative_value, projected_vs_realized, reliability,
volume_by_workspace, workspace_totals, run_heatmap,
value_by_class. Filters: workspace (wsp_... id),
automation (slug), from / to (ISO dates), months (1 to 36). Held
runs never count, and months without runs come back as null in rate
series rather than a misleading zero.
What the API deliberately does not do¶
Everything the UI can do is exposed over the API unless there is a specific reason not to. The recorded exclusions:
- API key management. Credentials minting credentials weakens revocation.
- Member management. Invites belong to email and role review in the UI.
- Billing. Checkout is Stripe-hosted and cannot be meaningfully driven by an API key.
- DELETE endpoints. Destructive actions are role-gated in the UI; org-scoped keys carry no role, so deletion stays a human decision. (Two exceptions that prove the rule: revoking a report link and deleting a webhook endpoint are exposed, because a kill switch for a shared URL or a compromised endpoint must be automatable.)
Warehouse extracts (Business and Enterprise)¶
For BI and warehouse loading, two bulk endpoints stream the system of record with a stable schema and forward cursor:
GET /api/v1/extracts/ledger: every ledger row, in stable order. Page withafter=<the next_after cursor you received>untilnext_afteris null (up to 5,000 rows per page); cursors are opaque tokens, so store and echo them verbatim.updated_since=<ISO 8601>returns only rows recomputed since that watermark, so nightly incremental syncs re-pull at least (and only around) what changed; upserts byid(an opaqueled_...identifier) make re-pulls harmless.GET /api/v1/extracts/adjustments: every closed-period adjustment, same cursor and watermark semantics, so the warehouse can rebuild as-restated = ledger + adjustments.
Amounts and quantities are decimal strings, never floats. Both endpoints require a full-access key.
Sync contract. Record your watermark BEFORE a sync begins and use it as
the next sync's updated_since, so rows recomputed mid-sync are re-pulled
next time. Deletions are not tombstoned: rows disappear when an automation is
deleted or an open-month bucket no longer applies, so schedule a periodic
full resync (weekly is plenty) to drop ghost rows; upserts by id make it
cheap.
Versioning¶
The API is versioned in the path (/api/v1/). Additive changes (new
fields, new endpoints) happen within a version; breaking changes get a new
version prefix.