Docs/Reference
Reference

GPUwerk API reference

By Samuel Seidel · Updated September 19, 2026 · 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.

On this page
  1. Authentication
  2. Error shape
  3. Rate limits
  4. Public endpoints
  5. Instances
  6. API keys
  7. SSH keys
  8. Account
  9. Billing
  10. Invoices
  11. Reservations
  12. Waitlist
  13. Versioning and deprecation

Authentication

Every authenticated call sends Authorization: Bearer <token>. Three kinds of token are accepted:

TokenObtainedScope
Supabase session JWTSigned in through the console (GitHub, Google or email)Full account access; cannot be scoped down
sy_key_… API keyPOST /v1/api-keys, signed in onlyWhichever scopes you grant it at creation
sy_live_… legacy tokenIssued to accounts created before SSO; nothing new issues oneFull 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:

StatusMeaning
401Missing or invalid bearer token
403Token is valid but lacks the scope, or the resource belongs to a different account
404No such resource, or a resource that exists but belongs to someone else (deliberately indistinguishable from the caller's side)
409The request is well-formed but the resource is in the wrong state for it (an instance mid-operation, a reservation already deposited, insufficient credit)
413A workspace too large to move within the operation's limits
422Request body failed validation (unknown type, out-of-range amount, malformed field)
429Rate limited; see below
502The node or an upstream payment call did not respond as expected; usually safe to retry
503A 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 prefixLimit
/v1/ (default, everything not listed below)300 requests / 60 s
/v1/instances60 / 60 s
/v1/instances/<id>/terminal-sessions10 / 60 s
/v1/instances/<id>/vnc-sessions30 / 60 s
/v1/api-keys30 / 60 s
/v1/billing/checkout10 / 60 s
/v1/signup5 / 3600 s
/v1/reservations (public submit)5 / 3600 s
/v1/reservations/terms, /v1/reservations/quote60 / 60 s
/v1/reservations/me20 / 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.

EndpointReturns
GET /v1/pricingPer-hour rate, units and hardware class for each instance type
GET /v1/configPublic console configuration: Supabase project, top-up bonus terms, hold-rate fraction, archive retention
GET /v1/imagesThe container flavors currently offered
GET /v1/reservations/termsCluster sizes, discount ladder and self-serve limits the Reserve configurator offers
GET /v1/reservations/quote?cluster_size&quantity&term_monthsEffective hourly rate, monthly price and deposit for a reservation configuration; 400 on an invalid combination
POST /v1/reservationsSubmit 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.

EndpointScopePurpose
GET /v1/instancesreadList every instance on the account
POST /v1/instancesinstancesDeploy a new instance
POST /v1/instances/<id>/startinstancesBring a stopped or held instance back onto hardware
POST /v1/instances/<id>/stopinstancesQuiesce the container, holding or releasing its node
POST /v1/instances/<id>/restartinstancesRestart the running container in place
POST /v1/instances/<id>/reset-accessinstancesReinstall account SSH keys and a known-good sshd config
POST /v1/instances/<id>/rebuildinstancesRecreate the container from a different image
DELETE /v1/instances/<id>instancesTerminate the instance permanently
GET /v1/instances/<id>/metrics?rangereadCPU/GPU/memory time series, live or historical
GET /v1/instances/<id>/logs?tailreadThe tenant container's stdout/stderr
GET /v1/instances/<id>/diskreadDurable workspace bytes vs. container writable-layer bytes
GET /v1/instances/<id>/workspacereadShort-lived download URL for a stopped instance's archived workspace
GET /v1/instances/<id>/eventsreadLast 50 lifecycle events for the instance
GET /v1/instances/<id>/authorize-modelinferenceGateway pre-check that a model endpoint is reachable; not meant to be called directly
POST /v1/instances/<id>/vnc-sessionsinstancesMint a single-use token for the browser VNC viewer
POST /v1/instances/<id>/terminal-sessionsinstancesMint a single-use token for the browser terminal
DELETE /v1/instances/<id>/terminal-sessions/<session_id>instancesClose 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.

EndpointPurpose
GET /v1/api-keysList keys on the account, optionally filtered by instance_id
POST /v1/api-keysCreate 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.

EndpointPurpose
GET /v1/ssh-keysList keys stored on the account
POST /v1/ssh-keysRegister 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

EndpointAuthPurpose
GET /v1/mesessionCredit balance, available credit, burn rate, trial status, budget
GET/PUT /v1/account/budgetsession for PUTMonthly spend cap
GET/PUT /v1/account/billing-profilesession for PUTInvoice 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/exportsessionMachine-readable export of the account's own data
GET/POST/DELETE /v1/account/closuresessionCheck, request, or cancel account closure

Billing

All paths below are relative to /v1/billing and need the billing scope.

EndpointPurpose
POST /checkoutCreate 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-cardStripe setup session to verify a card for the welcome trial; nothing is charged
GET /paymentsLast 100 top-up attempts with status
GET /ledgerFull account statement: every credit and debit, newest first, with the running balance
GET/PUT /auto-topupRead 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_usdThe exact mandate text for the chosen amounts, so what is shown and what is stored as evidence cannot drift apart
GET /billing/transactionsLegacy transaction log; prefer GET /ledger

Invoices

EndpointPurpose
GET /v1/invoicesList invoices with amounts and a ready flag
GET /v1/invoices/<id>.pdfPDF invoice; renders on demand if not yet generated, 503 while preparing
GET /v1/invoices/<id>.xmlUBL 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.

EndpointPurpose
GET /v1/reservations/meThis account's reservation requests, including ones submitted before sign-in that matched by email
POST /v1/reservations/meSubmit a reservation request as the signed-in account; 202 with the created request and its quote
POST /v1/reservations/<id>/depositCreate 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-cardStripe-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.

EndpointPurpose
POST /v1/waitlistJoin the line for a node type: {"type": "spark-1x", "expected_hours": null}; 409 if capacity is already free
DELETE /v1/waitlist?type=spark-1xLeave the line
GET /v1/waitlist/meThis 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.

NextCreate an SSH key and connect Back toAll docs

Nothing to unbox.

A dedicated DGX Spark in EU-Central, available through the console, with $20 in credit for your first $10 top-up.

Deploy a Spark