API reference

Provisioning, resources, ledger, teams and billing — one bearer token, one error shape.

AI/LLM: this page is available in plain markdown at /docs/api.md

Base URL: https://extraorbital.dev/api/v1. Everything the CLI does is one of these endpoints: extraorbital add is a single POST /provision, and extraorbital provision is a GET /services to learn the variable names followed by one POST /provision per resource it decided on.

Authentication

Send a bearer token. Three kinds are accepted, and the caller's identity always comes from the credential, never from the request body:

CredentialLooks likeUse
API keyeo_live_…CI and long-running agents. Scoped to one team, optionally one project
Agent tokena short-lived Ed25519 JWTA machine that registered through the Agent Auth Protocol
Sessiona cookie, or its token as a bearerThe dashboard, and the CLI after extraorbital login
curl https://extraorbital.dev/api/v1/me \
  -H "Authorization: Bearer $EXTRAORBITAL_TOKEN"

Missing or invalid credentials return 401. A team past its Prototype allowance with no human attached returns 402 with the link URL in both the body and the Location header — see When a human is needed.

Conventions

TopicRule
Team?team=<slug or id> on GET/DELETE, team or teamId in the body on POST. Defaults to your default team
Resource address<service>/<slug>, e.g. s3/default. Send service + slug, or the combined resource string. An alias like mongodb is canonicalised to mongo before anything is stored
Identifiersslug and team slugs match ^[a-z0-9][a-z0-9-]{0,62}$; projectId also allows dots, so skills.dev is valid. Both default to default
MoneyAlways integer cents, in fields ending InCents. No floats on the wire
TimeISO 8601 UTC. Ranges are from and to, both inclusive
PaginationKeyset: ?limit= (1–200, default 50) and ?cursor=; responses carry hasMore and nextCursor
ErrorsAlways { "error": { "code", "message", "status" } }, with linkUrl on 402 and retryAfter on 409

GET /me

Who the token belongs to, which team it is acting in, and how much of the Prototype allowance is gone. The freeTier field keeps its name for compatibility; on a free team usedInCents counts the account's whole history, because the allowance is granted once rather than every month.

{
  "user": { "id": "6a73…", "label": "severin", "email": "severin@example.com" },
  "credential": "apiKey",
  "machine": "brave-fox-a3f2",
  "team": { "teamId": "team_6a73…_default", "slug": "default", "role": "owner", "isDefault": true },
  "namespace": "swift-heron-9c1f",
  "plan": "prototype",
  "humanLinked": false,
  "freeTier": { "limitInCents": 500, "usedInCents": 460 },
  "projectCount": 3,
  "resourceCount": 5,
  "monthToDateInCents": 540
}

GET /services

The resource catalog, generated from the registry, so a new service becomes available without a CLI release. credentials is also the vocabulary autopilot scans a directory for, aliases lists other accepted names, and metered says whether the owning service measures usage at all.

supply is "provisioned" for everything created for your project alone, and "shared" for a resource that is one account handed to every team that asks — today only stripe. A shared entry carries a notice you should surface rather than swallow, and it is absent from the catalog entirely on a deployment that holds no account for it.

{
  "data": [
    {
      "service": "s3",
      "aliases": [],
      "title": "S3 bucket",
      "status": "available",
      "credentials": ["S3_BUCKET", "S3_ENDPOINT", "S3_REGION", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"],
      "options": { "region": { "type": "string", "default": "us-east-1" }, "isPublic": { "type": "boolean", "default": false } },
      "freeTier": "1 GB, 10K simple + 2K advanced ops/mo",
      "metered": false,
      "stateful": true,
      "supply": "provisioned"
    },
    {
      "service": "stripe",
      "aliases": [],
      "title": "Stripe test keys",
      "status": "available",
      "credentials": ["STRIPE_SECRET_KEY", "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", "STRIPE_WEBHOOK_SECRET"],
      "options": {},
      "freeTier": "Shared test account, free on every plan",
      "metered": false,
      "stateful": false,
      "supply": "shared",
      "notice": "Shared test account: every ExtraOrbital project provisioning stripe gets these same keys. …"
    }
  ]
}

POST /provision

The one endpoint that matters. Creates the project if needed, mints the team's downstream identity if needed, provisions the resource if needed, and returns credentials. Idempotent on (team, projectId, service, slug).

Body fieldTypeDescription
servicestrings3, mongo (or mongodb), redis, vector, qstash, authlocker (or auth), openai, anthropic, git, stripe
slugstring?Instance name inside the project. Default default
projectIdstring?Default default. Created on first use. May be a domain, e.g. skills.dev
resourcestring?Sugar for service + slug, e.g. "s3/default"
team / teamIdstring?Defaults to your default team
optionsobject?Service-specific tuning. Applied at creation, ignored on reuse
machinestring?Caller label recorded for attribution
curl -X POST https://extraorbital.dev/api/v1/provision \
  -H "Authorization: Bearer $EXTRAORBITAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "projectId": "cosmic-otter", "service": "s3", "slug": "default" }'

201 when it created something, 200 when the resource already existed. Both return the same shape:

{
  "teamId": "team_6a73…_default",
  "projectId": "cosmic-otter",
  "service": "s3",
  "slug": "default",
  "resource": "s3/default",
  "status": "active",
  "resourceId": "prv_9c1f77a2b4e8",
  "createdBy": "brave-fox-a3f2",
  "createdAt": "2026-08-01T09:14:00.000Z",
  "metered": false,
  "meta": { "bucketName": "cosmic-otter-default", "region": "us-east-1" },
  "credentials": [{ "name": "S3_BUCKET", "value": "cosmic-otter-default" }]
}

409 PROVISION_IN_PROGRESS means a concurrent call holds the lease — retry with backoff. 402 PAYMENT_REQUIRED carries linkUrl.

Resources

GET /resources lists from the broker's own mirror, so it is one fast query rather than a fan-out — and it never returns credentials.

RoutePurpose
GET /resourcesList. Filters: projectId, service, status
GET /resources/:service/:slugOne resource, with metadata and month-to-date spend
GET /resources/:service/:slug/credentialsDecrypt and return credentials. Audit-logged
POST /resources/:service/:slug/verifyLiveness probe behind extraorbital check. Always 200; branch on ok
DELETE /resources/:service/:slugDeprovision. ?force=true skips the grace period

All take ?projectId=, defaulting to default.

Projects and teams

RoutePurpose
GET POST /projectsList with rollups, or create one
GET PATCH DELETE /projects/:projectIdDetail with resources inlined; rename; delete (?force=true)
GET POST /teamsList teams with rollups, or create one from a slug and a name
GET PATCH DELETE /teams/:teamIdDetail, budget cap and alert settings, deletion
GET POST /teams/:teamId/membersMembership. POST takes { email, role }; an address with no account yet becomes a pending invitation, honoured on its first sign-in
DELETE /teams/:teamId/members/:userIdRemove a member

GET /ledger

Line items across every service, served from the broker's synced mirror.

QueryDefaultDescription
monthcurrentyyyy-mm. Shorthand for a from/to pair
from, toISO dates, inclusive
projectId, service, slug, machineallFilters
kindallprovision or usage
limit, cursor50Keyset pagination
{
  "data": [
    {
      "id": "lg_4b21c9f7e8a3",
      "projectId": "cosmic-otter",
      "resource": "s3/default",
      "machine": "brave-fox-a3f2",
      "kind": "usage",
      "metric": "storage-gb-month",
      "quantity": 105.2,
      "providerCostInCents": 184,
      "listPriceInCents": 184,
      "chargedInCents": 0,
      "createdAt": "2026-08-04T00:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null,
  "syncedAt": "2026-08-04T16:31:00.000Z",
  "partial": []
}

Three cost fields, deliberately: providerCostInCents is what the upstream provider charges, listPriceInCents is what we would bill, and chargedInCents is what was actually billed — 0 for everything covered by the Prototype allowance. The first two match, because usage is passed on at cost; they are kept separate so a future discount is visible rather than silent.

syncedAt is on every response. The mirror is synced, not live; partial names any service whose sync failed, so a gap is visible rather than silent.

GET /ledger/summary

The same data aggregated. This single endpoint backs every chart in the dashboard and every graph in the CLI.

QueryDefaultDescription
groupByprojectproject, resource, service, machine, day, metric
intervalday or hour. Adds a series array per group for sparklines
{
  "groupBy": "machine",
  "from": "2026-08-01T00:00:00.000Z",
  "to": "2026-08-31T23:59:59.999Z",
  "data": [
    {
      "group": "brave-fox-a3f2",
      "count": 18,
      "resources": ["s3/default", "mongo/analytics"],
      "metered": true,
      "providerCostInCents": 362,
      "listPriceInCents": 362,
      "chargedInCents": 0,
      "series": [{ "t": "2026-08-01", "listPriceInCents": 98 }]
    }
  ],
  "total": { "count": 24, "listPriceInCents": 540, "chargedInCents": 40 },
  "projection": { "endOfMonthInCents": 589 },
  "syncedAt": "2026-08-04T16:31:00.000Z",
  "partial": []
}

POST /ledger/sync pulls each service's ledger for one team. Called with the deployment's cron secret it syncs every team, which is the scheduled job.

Billing

RoutePurpose
POST /linkMint the short-lived URL a human opens. Returns { url, token, expiresAt }
GET /link/:tokenPublic. What the machine asked for and what the team has spent
POST /link/:tokenStart payment capture. Records the cap, returns a hosted checkout URL
POST /link/:token/completeFinish it. Idempotent with the webhook
GET /invoicesClosed months, plus the current one as a draft
GET /invoices/:idOne statement with its line items
POST /invoices/closeClose a period. With the cron secret, every team's previous month
POST /payments/webhookThe processor's webhook. Signature is the only authentication

Agents, keys and activity

RoutePurpose
GET /agentsMachines that acted in this team, with what they provisioned and spent
GET /activityThe audit trail: provisioning, credential reads, team changes, alerts
GET POST /keys · DELETE /keys/:keyIdManage eo_live_ keys. A key is shown once
POST /device/code · POST /device/tokenThe device flow behind extraorbital login

Agents also have a protocol surface of their own under /api/auth, implementing the Agent Auth Protocol.

Errors

{
  "error": {
    "code": "PAYMENT_REQUIRED",
    "message": "Your team has spent its Prototype allowance and cannot add vector. Move to a paid plan by having a human open the link below — then you are billed only for what you use.",
    "status": 402,
    "linkUrl": "https://extraorbital.dev/link/fl_8a2c91d4e7b3"
  }
}
StatusCodeWhen
400VALIDATION_ERRORMalformed projectId, slug, options, or an unknown service
401UNAUTHORIZEDMissing, invalid, or expired credential
402PAYMENT_REQUIREDPast the Prototype allowance or resource count with no human linked, or a hard budget cap reached. Carries linkUrl
403FORBIDDENNot a member of that team, or an agent missing a capability
404NOT_FOUNDUnknown team, project or resource
409PROVISION_IN_PROGRESSA concurrent provision holds the lease. Retry with backoff
422RESOURCE_IN_USEDelete attempted while resources remain
429RATE_LIMITEDToo many calls. Respect Retry-After
502UPSTREAM_ERRORThe owning service failed. The resource is left retryable
503ENCRYPTION_UNAVAILABLEThe credential key ring is missing or invalid