GPUwerk API reference
Everything the console itself calls is reachable at https://api.gpuwerk.com/v1/. This page documents the surface a customer can call with their own credential: how to authenticate, every endpoint and what it returns, the error codes it can actually produce, the rate limits in front of it, and what a /v1/ version guarantee means here.
Inclusion rule. This page lists only endpoints reachable with a customer's own bearer token or scoped API key, derived directly from backend/control-plane/app/routers/. It excludes anything gated by ADMIN_TOKEN, the dashboard or idle-registry tokens, dev-only login routes, the Stripe webhook, and the internal archive-transfer callbacks node agents use to move a workspace between hosts. If an endpoint is not on this page, treat it as unsupported for direct use even if you can find it in a console network trace.
Authentication
Every authenticated call sends Authorization: Bearer <token>. Three kinds of token are accepted:
| Token | Obtained | Scope |
|---|---|---|
| Supabase session JWT | Signed in through the console (GitHub, Google or email) | Full account access; cannot be scoped down |
sy_key_… API key | POST /v1/api-keys, signed in only | Whichever scopes you grant it at creation |
sy_live_… legacy token | Issued to accounts created before SSO; nothing new issues one | Full account access |
An API key is created from the console (or by calling POST /v1/api-keys with a session token) and shown exactly once at creation; only a hash is stored afterward, so a lost key cannot be recovered, only revoked and replaced. A key carries a subset of four scopes: read, instances, billing, inference. Each endpoint below states which scope it needs. A key can never manage other API keys or SSH keys on the account, those two areas require a full session token, so a leaked key cannot mint itself broader access.
A request with a missing or invalid token gets 401. A request with a valid token that lacks the required scope gets 403.
Error shape
Errors are JSON with a single detail field and an HTTP status that means what it says:
| Status | Meaning |
|---|---|
401 | Missing or invalid bearer token |
403 | Token is valid but lacks the scope, or the resource belongs to a different account |
404 | No such resource, or a resource that exists but belongs to someone else (deliberately indistinguishable from the caller's side) |
409 | The request is well-formed but the resource is in the wrong state for it (an instance mid-operation, a reservation already deposited, insufficient credit) |
413 | A workspace too large to move within the operation's limits |
422 | Request body failed validation (unknown type, out-of-range amount, malformed field) |
429 | Rate limited; see below |
502 | The node or an upstream payment call did not respond as expected; usually safe to retry |
503 | A dependent feature is not configured on this deployment (for example card payments) |
Rate limits
Every route is metered by a token bucket, keyed by API key or session token where one is sent, by IP address otherwise. A request over the limit gets 429 with a JSON body of {"detail": "too many requests"} and a Retry-After header giving the number of seconds to wait before trying again.
| Path prefix | Limit |
|---|---|
/v1/ (default, everything not listed below) | 300 requests / 60 s |
/v1/instances | 60 / 60 s |
/v1/instances/<id>/terminal-sessions | 10 / 60 s |
/v1/instances/<id>/vnc-sessions | 30 / 60 s |
/v1/api-keys | 30 / 60 s |
/v1/billing/checkout | 10 / 60 s |
/v1/signup | 5 / 3600 s |
/v1/reservations (public submit) | 5 / 3600 s |
/v1/reservations/terms, /v1/reservations/quote | 60 / 60 s |
/v1/reservations/me | 20 / 3600 s |
Where two prefixes match, the longer one wins, so an instance's terminal or VNC session mint uses its own tighter bucket rather than the general instances one. Twenty failed authentication attempts (a 401, or a 403 from a bad admin token) from one address in ten minutes also gets that address refused outright until the window passes, independent of the per-route limits above, a valid credential does not undo that.
Public endpoints
These need no token. They exist for the marketing site and the console's pre-login screens, and are safe to call directly.
| Endpoint | Returns |
|---|---|
GET /v1/pricing | Per-hour rate, units and hardware class for each instance type |
GET /v1/config | Public console configuration: Supabase project, top-up bonus terms, hold-rate fraction, archive retention |
GET /v1/images | The container flavors currently offered |
GET /v1/reservations/terms | Cluster sizes, discount ladder and self-serve limits the Reserve configurator offers |
GET /v1/reservations/quote?cluster_size&quantity&term_months | Effective hourly rate, monthly price and deposit for a reservation configuration; 400 on an invalid combination |
POST /v1/reservations | Submit an unauthenticated reservation request; 202, rate-limited at 5/hour |
Instances
All paths below are relative to /v1/instances. Every one operates only on instances the caller's account owns; asking about another account's instance returns 404, not 403, so a scan cannot learn an id exists.
| Endpoint | Scope | Purpose |
|---|---|---|
GET /v1/instances | read | List every instance on the account |
POST /v1/instances | instances | Deploy a new instance |
POST /v1/instances/<id>/start | instances | Bring a stopped or held instance back onto hardware |
POST /v1/instances/<id>/stop | instances | Quiesce the container, holding or releasing its node |
POST /v1/instances/<id>/restart | instances | Restart the running container in place |
POST /v1/instances/<id>/reset-access | instances | Reinstall account SSH keys and a known-good sshd config |
POST /v1/instances/<id>/rebuild | instances | Recreate the container from a different image |
DELETE /v1/instances/<id> | instances | Terminate the instance permanently |
GET /v1/instances/<id>/metrics?range | read | CPU/GPU/memory time series, live or historical |
GET /v1/instances/<id>/logs?tail | read | The tenant container's stdout/stderr |
GET /v1/instances/<id>/disk | read | Durable workspace bytes vs. container writable-layer bytes |
GET /v1/instances/<id>/workspace | read | Short-lived download URL for a stopped instance's archived workspace |
GET /v1/instances/<id>/events | read | Last 50 lifecycle events for the instance |
GET /v1/instances/<id>/authorize-model | inference | Gateway pre-check that a model endpoint is reachable; not meant to be called directly |
POST /v1/instances/<id>/vnc-sessions | instances | Mint a single-use token for the browser VNC viewer |
POST /v1/instances/<id>/terminal-sessions | instances | Mint a single-use token for the browser terminal |
DELETE /v1/instances/<id>/terminal-sessions/<session_id> | instances | Close a browser terminal session |
POST /v1/instances
Request body:
{
"name": "", # optional, autogenerated if blank
"type": "spark-1x", # a key from GET /v1/pricing
"image": "dgx-os", # a key from GET /v1/images
"ssh_public_key": "", # paste a key, or:
"ssh_key_id": "", # ...name one already on the account
"enable_vnc": false
}
Returns the instance object: id, name, type, image, status, rate, spent, host/https_endpoint for image flavors that serve HTTP, an ssh block (host, port, user), a vnc block when enabled, and nodes for a multi-node cluster type. Errors: 422 for an unknown type or image or no SSH key resolvable, 409 when the account is over budget, has insufficient available credit, or every capable node is busy.
POST /v1/instances/<id>/stop
Optional body {"hold": false}, only meaningful on a deployment with hold discounts turned off; ignored otherwise, since every stop holds the node by default so a restart takes seconds instead of a full workspace restore. Returns {"ok": true, "held": true, "rate_fraction": <number>} on a hold, or {"ok": true, "archived_bytes": <n>} on a full offload-and-release. 413 if the workspace is too large to offload; 502 if the node does not respond, in which case the instance and its data stay exactly where they were.
GET /v1/instances/<id>/metrics
range is one of live, 1h, 6h, 24h, 7d; anything else is a 422. live on a stopped instance falls back to the last hour of stored history rather than an empty series.
API keys
Managing keys always requires a full session token (the legacy or SSO account credential), never another API key, a key can never mint or see other keys.
| Endpoint | Purpose |
|---|---|
GET /v1/api-keys | List keys on the account, optionally filtered by instance_id |
POST /v1/api-keys | Create a key; the token is returned once, in this response only |
DELETE /v1/api-keys/<id> | Revoke a key |
POST /v1/api-keys body: {"name": "", "scopes": ["read","instances"], "instance_id": null}. Passing instance_id creates a key scoped to only the inference scope against that one instance, regardless of what scopes asked for; omit it for an account-wide key restricted to the requested subset of read, instances, billing, inference. 422 if the requested scopes intersect with none of those four.
SSH keys
Also session-token only, for the same reason as API keys: a credential that could register an SSH key would grant root on every future deploy.
| Endpoint | Purpose |
|---|---|
GET /v1/ssh-keys | List keys stored on the account |
POST /v1/ssh-keys | Register a public key: {"name": "", "public_key": "ssh-ed25519 ..."} |
DELETE /v1/ssh-keys/<id> | Remove a key from the account (does not revoke access already baked into a running instance's authorized_keys) |
Account
| Endpoint | Auth | Purpose |
|---|---|---|
GET /v1/me | session | Credit balance, available credit, burn rate, trial status, budget |
GET/PUT /v1/account/budget | session for PUT | Monthly spend cap |
GET/PUT /v1/account/billing-profile | session for PUT | Invoice address, VAT id, invoicing details. A VAT id is checked in the EU VIES register when saved; the response includes vat_check (status: verified, invalid, unavailable, unsupported or none, plus the registered name when verified) |
GET /v1/account/export | session | Machine-readable export of the account's own data |
GET/POST/DELETE /v1/account/closure | session | Check, request, or cancel account closure |
Billing
All paths below are relative to /v1/billing and need the billing scope.
| Endpoint | Purpose |
|---|---|
POST /checkout | Create a Stripe Checkout session for a top-up: {"amount_usd": 25, "save_card": false} → {"checkout_url": "..."}. 422 outside the configured min/max; 503 if card payments are not enabled |
POST /trial-card | Stripe setup session to verify a card for the welcome trial; nothing is charged |
GET /payments | Last 100 top-up attempts with status |
GET /ledger | Full account statement: every credit and debit, newest first, with the running balance |
GET/PUT /auto-topup | Read or configure automatic top-up (enabled, amount_usd, threshold_usd, mandate_accepted); enabling without a saved card returns needs_card: true rather than an error |
GET /auto-topup/mandate?amount_usd&threshold_usd | The exact mandate text for the chosen amounts, so what is shown and what is stored as evidence cannot drift apart |
GET /billing/transactions | Legacy transaction log; prefer GET /ledger |
Invoices
| Endpoint | Purpose |
|---|---|
GET /v1/invoices | List invoices with amounts and a ready flag |
GET /v1/invoices/<id>.pdf | PDF invoice; renders on demand if not yet generated, 503 while preparing |
GET /v1/invoices/<id>.xml | UBL 2.1 XML invoice for accounting software |
Both require the read scope.
Reservations
The authenticated half of the "Reserve Sparks" flow; the public quote and unauthenticated submit are listed under Public endpoints above. All need the billing scope.
| Endpoint | Purpose |
|---|---|
GET /v1/reservations/me | This account's reservation requests, including ones submitted before sign-in that matched by email |
POST /v1/reservations/me | Submit a reservation request as the signed-in account; 202 with the created request and its quote |
POST /v1/reservations/<id>/deposit | Create the Stripe Checkout session for the flat deposit; 403 if the request belongs to a different account, 409 if a deposit is already paid or availability is unconfirmed |
POST /v1/reservations/<id>/authorize-card | Stripe-hosted page to save or replace the card charged for monthly billing |
Waitlist
All paths relative to /v1/waitlist, needing instances scope for join/leave and read for status.
| Endpoint | Purpose |
|---|---|
POST /v1/waitlist | Join the line for a node type: {"type": "spark-1x", "expected_hours": null}; 409 if capacity is already free |
DELETE /v1/waitlist?type=spark-1x | Leave the line |
GET /v1/waitlist/me | This account's current waitlist entries |
Versioning and deprecation
Every path is under /v1/. While a route stays on /v1/, we do not remove a field from a response, change what an existing field means, or turn an optional request field into a required one. Fields are added over time, and a client that ignores fields it does not recognize keeps working.
A change that would break that contract, such as removing an endpoint or replacing it with a different shape, ships as a new version prefix rather than as a silent change to /v1/, with advance notice before /v1/ itself is retired. There is no fixed notice period published yet; check this page and the status page before relying on one.
This page does not promise an SLA, an uptime figure or a support response time; see the terms for what is and is not warranted.