Skip to content
extraorbital
Documentation ExtraOrbital
Browse documentation

Reference

API reference

HTTP endpoints for resources, credentials, projects, and usage.

Base URL: https://extraorbital.dev/api/v1.

Authentication

Identity comes from the credential, never the request body:

CredentialLooks likeUse
API keyeo_live_…CI and long-running agents. Scoped to one team, optionally one project — a project-scoped key uses that project when a request names none, and gets 403 for any other
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
Terminal
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 floating-point values
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

Returns identity, active team, plan, and budget information. freeTier is a legacy field name. Prototype credit is a one-time allowance on the person’s default team; additional Prototype teams, and every team on a paid plan, report a zero allowance: paid plans have no free tier.

json
{
  "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 },
  "billingMode": "postpaid",
  "balanceInCents": null,
  "windows": [
    { "interval": "lifetime", "budgetInCents": 500, "spentInCents": 460, "remainingInCents": 40 }
  ],
  "projectCount": 3,
  "resourceCount": 5,
  "monthToDateInCents": 540,
  "warnings": []
}

windows lists spending limits across all services. remainingInCents may be negative.

Billing modeSpending windows
Prototype or sandboxOne lifetime allowance.
Paid, postpaidMonthly budget cap; $500 if no cap is set.
Paid, prepaidLifetime credit balance, plus a monthly window if capped.

A first top-up switches a paid team to prepaid billing. Previously invoiced usage is not deducted from the new credit. Exhausted windows block new metered resources with 402. Top up depleted credit or adjust the monthly cap as indicated by the response.

For machine tokens, machine identifies the device and user identifies its linked owner. Unlinked machines use a Prototype sandbox (sbx_{hostId}), with a synthetic sandbox:{hostId} user. Sandboxes cannot be signed into, paid for, invited to, or deleted as ordinary accounts. Their 402 links request account linking.

json
"machine": {
  "hostId": "6a9a9248ccdac03c7cb454bb",
  "label": "keen-vole-0a8c",
  "linked": true,
  "owner": { "userId": "6a73…", "email": "severin@example.com" },
  "sandboxTeamId": "sbx_6a9a9248ccdac03c7cb454bb",
  "transferredTo": "team_6a73…_default",
  "movedProjects": [{ "from": "default", "to": "default-keen-vole-0a8c" }]
}

Linking transfers projects, resources, and ledger rows to the owner’s team without changing credentials. Transferred usage counts in its original time window and may make the balance negative. No duplicate debit is created.

Conflicting project IDs become {id}-{label}. movedProjects records renames; transferredTo identifies the destination while linked. The CLI uses both to update .extraorbital.json.

warnings contains budget and inactivity alerts for teams without email delivery, including all sandboxes. whoami, add, and provision print them. Teams receiving email alerts return an empty list.

GET /services

Returns available services, accepted aliases, recognized environment-variable names in credentials, and whether usage is measured in metered.

supply: "provisioned" means a project-specific resource. "shared" means shared credentials, currently Stripe test keys. Display the returned notice. Shared services without configured credentials are omitted.

json
{
  "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

Creates the project, service identity, and resource if needed, then returns credentials. Reusing (team, projectId, service, slug) returns the existing resource.

Body fieldTypeDescription
servicestringA service ID or alias from GET /services. Availability depends on the deployment.
slugstring?Instance name inside the project. Default default
projectIdstring?Default default. Created on first use. May be a domain, e.g. skills.dev
resourcestring?Alternative to separate 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
Terminal
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:

json
{
  "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

Lists stored resource records without returning credential values.

RoutePurpose
GET /resourcesList. Filters: projectId, service, status
GET /resources/:service/:slugOne resource, with metadata and month-to-date spend
GET /resources/:service/:slug/credentialsList the credential records, no secrets
POST /resources/:service/:slug/credentials/:id/revealReturn a credential's values (primary for the primary). Audit-logged credential reveal
POST /resources/:service/:slug/verifyLiveness probe for a provisioned resource. 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, to—ISO dates, inclusive
projectId, service, slug, machineallFilters
kindallprovision or usage
limit, cursor50Keyset pagination
json
{
  "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": []
}

Cost fields:

  • providerCostInCents: supplier charge.
  • listPriceInCents: price before allowances.
  • chargedInCents: amount billed, as the team's plan bills it. A paid plan is charged the list price from the first unit. On the Free plan it is zero while the allowance covers it; usage a linked machine brought from its sandbox counts as zero there until the team moves to a paid plan, whose next invoice bills it.

Provider cost and list price currently match.

syncedAt records the last sync. Data is not live; partial identifies failed service syncs.

GET /ledger/summary

Returns aggregated ledger data for charts and summaries.

QueryDefaultDescription
groupByprojectproject, resource, service, machine, day, metric
interval—day or hour. Adds a series array per group for sparklines
json
{
  "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 imports usage for one team and reports imported and corrected counts per service. Corrections preserve the row’s team and project. With the cron secret, it processes all teams across time-limited runs, resuming from the previous position.

Billing

RoutePurpose
POST /linkMint the short-lived URL a human opens. An optional projectId lets the page move that project into a team they already pay for, instead of only selling this team a plan. Returns { url, token, expiresAt }
GET /link/:tokenPublic. What the machine asked for, the project it was refused 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
POST /billing/topupBuy prepaid credit with { amountInCents }, from $5.00 to $10,000.00. For an admin of a team on a paid plan. Returns { url }, the hosted checkout; the credit is added when the payment arrives, and the first top-up makes the team prepaid
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
GET /billing/reconcileCron only, hourly. Pushes the AI Gateway policy of every team holding a model key again — at the version the gateway already holds where nothing changed, which repairs a ceiling that drifted and refreshes the small models
GET /billing/reconcile/sweepCron only, every minute. The same for the teams whose ceilings cannot wait an hour: keys in two namespaces or in two workspaces, a paid period that has just ended, or a push the gateway did not take
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 /machines/link · GET /machines/link/:codeA machine asks for the URL that links it to whoever opens it, then polls for the answer
GET /machines · DELETE /machines/:hostIdThe machines linked to the signed-in person, and unlinking one
POST /device/code · POST /device/tokenThe device flow behind extraorbital login --human
GET /snippets?resource=&platform=&action=Public. Code for a resource on a platform (nextjs, node, python, cloudflare-workers): without action, the setup — install command, variable names, client; with one, that task. { resource, platform, action, install, env, snippets: [{ language, file, code }], actions, notes, docs }. Names variables, never values
GET /snippets/actions?resource=&platform= · GET /snippets/platforms?resource=Public. The tasks, and the platforms, there is code for
POST /feedback{ message, email?, cliVersion?, team? }, signed in or not: behind extraorbital feedback. Stored, then emailed to the ExtraOrbital team. Returns { id, stored, notification }, 201 (200 for the same message resent the same day); 429 past 10 an hour per sender

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

Errors

json
{
  "error": {
    "code": "PAYMENT_REQUIRED",
    "message": "Your team has spent its Prototype allowance and cannot add vector. Have a human open the link below to move this project into a team they already pay for, or to move this team to a paid plan — 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, a key scoped to another team or project, 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