Skip to content
Docs

API reference

Introduction

The Nitrous HTTP API creates disposable Linux machines. It uses resource-oriented URLs, JSON request bodies and API-key authentication.

Each machine is a dedicated virtual machine, booted from an image with the requested resources. It runs a command and is destroyed when the command exits, unless it is kept (keep_alive); a machine without a command stays idle. Every machine is destroyed when it is deleted or its time to live expires.

Authenticate with an API key in the Authorization header. Examples read it from $NITROUS_KEY.

Authorization: Bearer nx_…
Base URL
https://api.nitrous.sh
KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/machines \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"image": "ubuntu-26.04","command": ["sh", "-c", "uname -r && nproc"]}'
201Created· Abbreviated response · full object
{"id": "m_01m3kmnv02f4j8bdzeyt7p2fhx","object": "machine","image": "ubuntu-26.04","state": "running","size": "standard","created_at": "2026-09-28T09:14:02.114Z","expires_at": "2026-09-28T10:14:02.114Z",…}

Authentication

Authenticate every request with an API key in the Authorization header, with the Bearer scheme. The scheme is case-insensitive; the key is not.

Authorization: Bearer nx_…

Keys are created on the API keys page of the dashboard. A key starts with nx_ and is shown only once, at creation. It expires after 30, 90 or 365 days, or never, as chosen at creation, and can be revoked at any time. An account has at most 50 keys that are neither revoked nor expired.

A key acts on behalf of its account. All keys of an account see the same machines and share the account's limits and rate limit. Machines in other accounts are not visible: a request for one returns 404 machine_not_found.

When a key is refused

  • 401unauthorizedThe header is missing or uses another scheme, or the key does not exist, is revoked or has expired. The body is identical in every case. WWW-Authenticate is Bearer realm="nitrous", with error="invalid_token" when a Bearer key was sent.
  • 403account_disabledThe key is valid but its account is disabled. Every request with the account's keys returns this. Contact support@nitrous.sh.
curl https://api.nitrous.sh/v1/account \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
401 Unauthorized · a key was sent and refused
HTTP/1.1 401 Unauthorizedcontent-type: application/jsonwww-authenticate: Bearer realm="nitrous", error="invalid_token"cache-control: no-storex-request-id: req_01m3m85f4ae718vk3954qmbskr{"error": "A valid API key is required in the Authorization header (Bearer nx_…).","code": "unauthorized","request_id": "req_01m3m85f4ae718vk3954qmbskr"}
403Forbidden· the account is disabled
{"error": "This account is disabled. Contact support@nitrous.sh.","code": "account_disabled","request_id": "req_01m3m85f4ae718vk3954qmbskr"}

Requests and responses

The API is served over HTTPS. Every path starts with /v1.

  • Request bodies are JSON, at most 2 MiB (2,097,152 bytes); a larger one returns 400 invalid_request. The Content-Type header is ignored: every body is parsed as JSON.
  • Responses are JSON, except logs, which are plain text (the bytes as written) or NDJSON. Deleting an image returns 204 with no body.
  • Every response carries Cache-Control: no-store and an x-request-id.
Response headers
HTTP/1.1 201 Createdcontent-type: application/jsonlocation: /v1/machines/m_01m3kmnv02f4j8bdzeyt7p2fhxcache-control: no-storex-request-id: req_01m3m85f4ae718vk3954qmbskr

IDs and data types

Every object has an object field naming its type: machine, image, build, base_image, account, usage, sizes, exec_result or list.

FormatExample
IDsA type prefix and a TypeID. Opaque strings: compare them, never parse them.m_01m3kmnv02f4j8bdzeyt7p2fhx
TimestampsRFC 3339, UTC, milliseconds2026-09-28T09:14:02.114Z
Memory, diskIntegers, in MiBmemory_mib: 2048
PricesDecimal strings, in US dollars"0.15"

Exec results and builds use the identifiers described in their sections.

201Created· Abbreviated response · full object
{"id": "m_01m3kmnv02f4j8bdzeyt7p2fhx","object": "machine","memory_mib": 2048,"disk_mib": 10240,"price_per_hour_usd": "0.15","created_at": "2026-09-28T09:14:02.114Z",…}

Errors

A failed request returns an HTTP status and a JSON body. code is stable and intended for programs; error is one sentence for people, says what to do where that helps, and may change. Every code is snake_case; new codes can be added, so handle unknown ones. Quota limits (machines, images, builds) return 409; only the rate limit returns 429. A log stream that fails after its 200 has started ends without an error body; see Stream logs.

  • errorstring
    What was wrong, and where useful what to do, in one sentence.
  • codestring
    A machine-readable error code, one of those listed below.
  • request_idstring
    The request's ID, identical to the x-request-id header. See Request IDs.

Status codes and error codes

  • 400invalid_requestThe body is not valid JSON, exceeds 2 MiB, or does not match the endpoint's schema (an unknown or repeated field, a wrong type), or a value breaks a rule; a query parameter is unknown or out of range. error names the field.
  • 400invalid_nameThe image name is not 3 to 40 lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit.
  • 400invalid_dockerfileThe Dockerfile exceeds 64 KiB, has no FROM, or uses COPY or ADD of local files.
  • 401unauthorizedThe request has no valid API key.
  • 402subscription_inactiveThe subscription is not active. Start it, or retry its payment, on the Billing page.
  • 402included_usage_exhaustedThe period's included hours are used and the usage balance is empty. Add funds on the Billing page.
  • 402insufficient_usage_balanceThe usage balance cannot fund another machine. Add funds on the Billing page.
  • 402auto_recharge_failedAuto-recharge failed and the usage balance cannot fund another machine. Update the payment method or add funds on the Billing page.
  • 402billing_account_lockedBilling for the account is locked. Contact billing@nitrous.sh.
  • 402payments_unavailablePayments are not available yet, so machines cannot start.
  • 403account_disabledThe account is disabled: every request with its keys returns this. A machine or build asked for while it was being disabled did not start. Contact support@nitrous.sh.
  • 403size_not_allowedThe machine exceeds the account's limits (GET /v1/account).
  • 404machine_not_foundNo machine with this id exists in the account. Also returned by exec once the machine is torn down, and by logs past retention; error says which. See Lifecycle.
  • 404image_not_foundNo image with this id or name, or no such build of it, exists in the account.
  • 404build_not_foundThe image has no build with this number.
  • 404log_not_foundThe machine has no output for this exec.
  • 404not_foundThe path does not exist.
  • 405method_not_allowedThe path does not support this method. The Allow header lists the methods it does.
  • 409machine_conflictThe machine's state does not allow the operation, such as an exec while it is starting, or being torn down before its expires_at.
  • 409machine_expiredThe machine is past its expires_at and is being torn down: no exec, no followed logs. Once it is torn down, exec returns 404 and logs return the retained output.
  • 409machine_limit_reachedThe account has its maximum number of machines that have not ended (starting or running), image builds included. Delete one to start another.
  • 409image_existsThe account already has an image with this name, or the name belongs to a base image.
  • 409image_limit_reachedThe account has 10 images, its limit. Delete one to create another.
  • 409image_not_publishedThe image was requested by name and has no published build. Publish a build, or request name@n.
  • 409build_not_readyThe build is queued, building or failed. Only a ready build can be booted or published.
  • 409build_limit_reachedThe account is running 2 builds, its limit. Start another when one finishes.
  • 409idempotency_key_in_useA request with this Idempotency-Key is still running. Retry once it has finished.
  • 422idempotency_key_reusedThis Idempotency-Key was used for a different request. Use a new key for each new request.
  • 429rate_limitedThe account exceeded its rate limit. Retry after the number of seconds in Retry-After.
  • 500internal_errorAn error occurred on our side. Reads and deletes are safe to retry, and so are a create and a build under their Idempotency-Key. A publish may have been applied: reconcile before retrying (see Idempotency). Never retry an exec automatically: it may have run. Quote the request ID to support if it keeps failing.
  • 502internal_errorThe compute host failed the operation. error says which operation. Retry as for 500.
  • 502machine_unavailableThe machine did not respond in time (on create: it did not start in time). Retry as for 500.
  • 503capacity_exhaustedNo capacity is available for another machine. Nothing was created: retry in a few seconds.
  • 503service_unavailableMachines are temporarily unavailable. The request did not run: retry in a few seconds.
400Bad Request· a disk_mib that is not a tier
{"error": "disk_mib must be one of 10240, 20480, 40960, 81920, 163840 (10, 20, 40, 80 or 160 GiB).","code": "invalid_request","request_id": "req_01m3m85f4ae718vk3954qmbskr"}
402Payment Required· a create the balance cannot fund
{"error": "The usage balance cannot fund another machine. Add funds on the Billing page.","code": "insufficient_usage_balance","request_id": "req_01m3m85f4ae718vk3954qmbskr"}

Pagination

Every list endpoint returns a list object: one page of results in data, and whether more follow in has_more. To read the next page, send the cursor of this page's last object as starting_after: its id, number or name, per endpoint below.

The list object

  • objectstring
    list
  • dataarray
    The page's objects, in the list's order.
  • has_moreboolean
    More objects follow this page.

Query

  • limitintegeroptional
    How many objects a page holds.
    Default
    50
    Allowed
    1–100
  • starting_afterstring | integeroptional
    The cursor of the last object on the previous page. The page starts after it.
EndpointOrderstarting_afterIf the cursor resource is removed
GET /v1/machinescreated_at, then id, newest firstid (string)Never removed: an ended machine stays a valid cursor
GET /v1/imagescreated_at, then id, newest firstid (string)Deleted image: 400 invalid_request
GET /v1/images/{image}/buildsnumber, highest firstnumber (integer)Deleted with its image: 404 image_not_found
GET /v1/base-imagesname, A to Zname (string)Removed base image: 400 invalid_request

Each page is read after its cursor in that fixed order, when the page is requested: there is no snapshot. An object that is in the list from the first page to the last appears exactly once; one that leaves the list meanwhile (a deleted image, a machine that ended) is left out.

Added while pagingAppears
A machine, image or buildOn no later page: it sorts before the cursor.
A base imageOn a later page if its name sorts after the cursor.

starting_after must be a valid cursor for the account and the endpoint, as the table above says; any other value, or an unknown query parameter, returns 400 invalid_request. If a cursor is refused because its object was deleted, read again from the first page and skip the objects already seen.

curl "https://api.nitrous.sh/v1/machines?limit=2" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "list","data": [{"id": "m_01m3kmtt8reagbvwvkf5enc5xs","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960,"size": "large","price_per_hour_usd": "0.37324","keep_alive": true,"command": null,"created_at": "2026-09-28T09:16:45.208Z","started_at": "2026-09-28T09:16:45.455Z","destroyed_at": null,"expires_at": "2026-09-28T11:16:45.208Z","destroy_reason": null},{"id": "m_01m3km187afrrsj0h1v7kedp8r","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 2,"memory_mib": 2048,"disk_mib": 10240,"size": "standard","price_per_hour_usd": "0.15","keep_alive": false,"command": {"status": "running","exit_code": null,"exit_signal": null,"exited_at": null},"created_at": "2026-09-28T09:02:47.530Z","started_at": "2026-09-28T09:02:47.777Z","destroyed_at": null,"expires_at": "2026-09-28T10:02:47.530Z","destroy_reason": null}],"has_more": true}
curl "https://api.nitrous.sh/v1/machines?limit=2&starting_after=m_01m3km187afrrsj0h1v7kedp8r" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"

Idempotency

Supported POST operations take an Idempotency-Key header: POST /v1/machines, POST /v1/images, POST /v1/images/{image}/builds and POST /v1/images/{image}/publish. A key does two things: a request that succeeded (2xx) is not run again for 24 hours, and a retry gets its stored response; and two requests with the same key never run at once. A request that failed releases its key and a retry runs it again, unless it may have made its machine: the retry then returns that machine, and never makes a second. Generate a key once per operation, keep it, and send it with every retry of that operation.

The example generates its key once, into a variable, before the request. A retry is the same request sent again with that same key: after a timeout, a 429, 409 idempotency_key_in_use or a 5xx (see Retries), run it again without generating a new key. The key finds what an earlier attempt made, so a retry never makes a second machine.

Rule
Key1 to 255 printable ASCII characters, no spaces. Scoped to the account, across its API keys.
Same requestSame method, path (as sent, without the query string) and body bytes. Whitespace, key order and an explicit default make a different body: resend the stored bytes. /v1/images/web-runner/builds and /v1/images/img_…/builds are different paths.
Window24 hours from the first request with the key. Every guarantee here holds for those 24 hours; after them the key is free.
ReplayThe stored status and body, with Content-Type, Idempotent-Replayed: true, Location and Cache-Control, and a new x-request-id.
The first requestA retry with the same key and request
Still running, including after the caller disconnected or timed out: it runs to its end409 idempotency_key_in_use.
Succeeded (2xx)The stored response. Nothing runs again.
Refused (4xx, 503)Runs again: nothing was made.
Failed (500, 502), or its connection lostA create or build: the machine or build it made, replayed; 409 idempotency_key_in_use with Retry-After while the host cannot yet say; runs again only if nothing was made. A publish: runs again; the first may have published: reconcile first.
Interrupted (the API stopped mid-request)409 idempotency_key_in_use for up to 10 minutes, then as a failed request.
Same key, different request422 idempotency_key_reused

Retries

Retry automatically, under the same key, only what was not applied. Stop on anything else.

OutcomeRetryWhy
429 rate_limitedAfter Retry-AfterRefused before the request ran.
409 idempotency_key_in_useAfter Retry-AfterThe first request is still running; a retry after it succeeds returns its response.
503 capacity_exhausted, 503 service_unavailableWith backoffRefused before anything was made.
No connection made (DNS failure, connection refused, connect timeout)With backoffThe request was never sent.
Other 4xxNoNothing was made. Fix the request.
500, 502, a timeout, a connection lost after sendingA create or build: under the same key. Anything else: noA create or build under its key finds what the first made. Anything else: the outcome is unknown. Reconcile, then retry under a new key only if nothing was made.

Reconcile

There is no operation id: find what a request made from the resources themselves.

RequestFind what it made
POST /v1/machinesList machines and match image, vcpus, memory_mib, disk_mib and keep_alive, with created_at after the first attempt was sent. Lists omit ended machines: a machine whose command already exited is not found. For a command that must not run twice, create the machine without a command, which stays listed until it is deleted or expires, then run the command with exec.
POST /v1/imagesRetrieve the image by name: names are unique in the account.
POST …/buildsList the image's builds: a build with created_at after the first attempt was sent. Builds are never left out of the list.
POST …/publishRetrieve the image: published_build is the build its name boots. Another publish may have changed it since; publishing again sets it back.

Exec takes no key and returns 400 invalid_request with one. Never retry an exec automatically after a 5xx, a timeout or a dropped connection: the command may have run, and its exec_id comes only with the result. GET and DELETE requests are safe to repeat as they are.

KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/machines \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"image": "ubuntu-26.04","size": "standard"}'
A replay
HTTP/1.1 201 Createdcontent-type: application/jsonidempotent-replayed: truelocation: /v1/machines/m_01m3kmtt8reagbvwvkf5enc5xscache-control: no-storex-request-id: req_01m3nz1ag8fm5rj84acm7kxfek
422Unprocessable Content· the same key, another body
{"error": "This Idempotency-Key was used for a different request. Use a new key for each new request.","code": "idempotency_key_reused","request_id": "req_01m3m85f4ae718vk3954qmbskr"}

Request IDs

Every response, successful or not, includes an x-request-id header: an opaque string starting with req_. The API generates it; a value sent with the request is ignored. Error bodies repeat it as request_id. A replayed response has a new one.

Include the request ID when contacting support@nitrous.sh.

Response headers
HTTP/1.1 200 OKcontent-type: application/jsonx-request-id: req_01m3m85f4ae718vk3954qmbskr

Rate limits

Requests are limited per account, across all of its API keys, by a token bucket: 10 requests per second sustained, with a burst capacity of 60.

  • Every authenticated request to a /v1 endpoint takes one token, whatever its outcome: an error, a replay, a followed log (once, when it opens).
  • Not counted: 401, 403 account_disabled, 404 not_found for an unknown path, 405, and the 429 itself.
  • Beyond the limit: 429 rate_limited with Retry-After, the whole seconds until a token is available. Wait at least that long, and back off with jitter when retrying (see Idempotency).

Separately, each account has a limit on machines that have not ended, image builds included. A create or build beyond it returns 409 machine_limit_reached. The account's limits are returned by GET /v1/account.

429 Too Many Requests
HTTP/1.1 429 Too Many Requestscontent-type: application/jsonretry-after: 1x-request-id: req_01m3m85f4ae718vk3954qmbskr{"error": "Rate limit exceeded: 10 requests per second sustained, burst capacity of 60. Retry after 1 second.","code": "rate_limited","request_id": "req_01m3m85f4ae718vk3954qmbskr"}

Versioning

The API is versioned in the path (/v1). Within v1, changes are additive: new endpoints, fields, enum values and error codes. Clients must ignore unknown fields and handle unknown enum values and codes. Nitrous is in early access: until general availability, v1 may also change in ways that are not additive.

The machine object

Returned when creating, retrieving or deleting a machine, and in machine lists. Every machine has every field; a field is null until what it records happens, such as destroyed_at before the machine is destroyed.

  • idstring
    The machine's id (m_…). Treat it as an opaque string.
  • objectstring
    machine
  • imagestring
    The booted image: a base image's name, or name@n for a build of a custom image.
  • statestring
    starting, running, destroying, destroyed or failed: the machine's, not its command's (command.status). A kept machine whose command exited is running. failed is a platform or boot failure. See Lifecycle.
  • vcpusinteger
    Virtual CPUs.
  • memory_mibinteger
    Memory, in MiB.
  • disk_mibinteger
    Root disk size, in MiB.
  • sizestring | null
    The preset matching the machine's vCPUs and memory, regardless of disk; null if none matches.
  • price_per_hour_usdstring
    Hourly price in US dollars, as a decimal string.
  • keep_aliveboolean
    The machine is kept after its command exits. Always true for a machine without a command.
  • commandobject | null
    The machine's command, apart from the machine. null for a machine created without a command.
  • command.statusstring
    running until the command exits, then exited. A command whose machine ended first is exited, with exit_code null.
  • command.exit_codeinteger | null
    The command's exit status. When signal N ended it, 128 + N (SIGKILL: 137), as in a shell or Docker. null while it runs. null with status exited means the exit was not observed: destroy_reason says how the machine ended.
  • command.exit_signalstring | null
    The signal that ended the command, such as SIGKILL or SIGTERM. null when the command exited by itself, and whenever exit_code is null.
  • command.exited_atstring | null
    When the command exited. null while it runs, or if its exit was not observed.
  • created_atstring
    When the machine was requested.
  • started_atstring | null
    When the machine started running, with its command. Billing starts at this time. null if it never started.
  • destroyed_atstring | null
    When teardown completed, or for a machine that did not start, when its start failed. Retention counts from this time.
  • expires_atstring
    created_at plus ttl_seconds. From this time the machine takes no exec, billing stops, and it is torn down.
  • destroy_reason"deleted" | "command_exited" | "ttl_expired" | "billing" | "out_of_memory" | "crashed" | "host_cleanup" | "host_lost" | null
    Why the machine was destroyed; null until it is. See Lifecycle.
The machine object
{"id": "m_01m3kmtt8reagbvwvkf5enc5xs","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960,"size": "large","price_per_hour_usd": "0.37324","keep_alive": true,"command": null,"created_at": "2026-09-28T09:16:45.208Z","started_at": "2026-09-28T09:16:45.455Z","destroyed_at": null,"expires_at": "2026-09-28T11:16:45.208Z","destroy_reason": null}

Lifecycle

A machine's state is the machine's own; its command's is command.status. A machine moves forward through its states and never returns to an earlier one:

MachineStatescommand.status
With a commandstarting → running → destroying → destroyedrunning → exited
With a command and keep_alivestarting → running → destroying → destroyedrunning → exited, while the machine is running
Without a commandstarting → running → destroying → destroyedcommand is null
Did not startstarting → failedexited
StateMeaningBilled
startingBeing created and booted.no
runningBooted; the command, if any, has started. A kept machine (keep_alive) stays running after its command exits, and accepts exec.yes
destroyingBeing torn down: after a delete, the command's exit (not kept), a crash, or expires_at, until teardown completes.to the end under When billing stops
destroyedTorn down (destroyed_at). The record and output remain; see Retention.no
failedThe machine did not start (a platform or boot failure); the create returned an error. destroyed_at is when the start failed. After retention its record reads destroyed, with started_at null.no

POST /v1/machines returns once the machine is running. starting is visible only to concurrent requests. To wait for a kept machine's command, wait until command.status is exited.

Time to live

Every machine has expires_at: ttl_seconds after created_at, startup included. The default is 3600 (1 hour); the maximum is 86400 (24 hours). A running exec does not extend it. At expires_at, billing stops and teardown starts, with destroy_reason ttl_expired.

Ending

One order decides what exec and logs return, whatever ended the machine: torn down wins over expiry, and expiry over the machine's state. A machine deleted before its expires_at goes from ending to torn down; its expiry passing later changes nothing. Retrieve returns the machine in every phase.

PhasestateExecLogsLogs, follow=true
ActiverunningRunsOutput so farStreams until the command exits
Ending before expires_at: deleted, command exited, crasheddestroying409 machine_conflictOutput so farStreams what is left, then closes
Past expires_at, being torn downdestroying409 machine_expiredOutput so far409 machine_expired
Torn down, for 7 days from destroyed_atdestroyed404 machine_not_foundRetained outputRetained output, then closes
Past retentiondestroyed404 machine_not_found404 machine_not_found404 machine_not_found

When billing stops

Enddestroy_reasonBilling stops at
The command exits, keep_alive falsecommand_exitedThe exit (command.exited_at)
DeleteddeletedWhen the platform records the machine as ending, no later than destroyed_at
Time to livettl_expiredexpires_at
Paid time runs outbillingWhen the platform records the machine as ending, no later than destroyed_at
Crash, out of memory, host maintenance, host restartcrashed, out_of_memory, host_cleanup, host_lostWhen the platform records the machine as ending, no later than destroyed_at
Did not startnull (state failed)Never billed

A kept machine is billed after its command exits, until it is deleted or expires. Nothing is billed past expires_at. The platform records the end when it first sees the machine destroying or destroyed, so teardown is never billed past destroyed_at. A teardown the host cannot confirm leaves destroyed_at null; billing stops when the platform next sees the machine destroying, and does not resume.

Destroy reasons

destroy_reason is one of:

ValueMeaning
deletedDestroyed with DELETE /v1/machines/{id}.
command_exitedThe command exited and keep_alive was not set.
ttl_expiredThe machine reached expires_at.
billingUsage could no longer be paid for: the usage balance ran out or the subscription is not active.
out_of_memoryThe machine exceeded its memory.
crashedThe machine stopped unexpectedly while its command was running.
host_cleanupRemoved during host maintenance.
host_lostThe host restarted while the machine was running.

Retention

The disk, and everything written to it, is deleted when the machine is destroyed. For 7 days after destroyed_at (for a machine that did not start, when its start failed), kept machines included, the output of its command and of every exec remains readable, and GET /v1/machines/{id} returns its full final record.

After 7 days the output is deleted. The machine object remains permanently, with every field unchanged: its image, resources, price, command, created_at, started_at, destroyed_at, expires_at and destroy_reason. For machines destroyed before 1 October 2026, destroy_reason, command.exited_at, command.exit_signal, and command.exit_code for a command a signal ended, may be null. A machine that failed to start reads destroyed, with started_at null: an archival record, not a state it passed through.

A machine after its command exited
{"id": "m_01m3kmnv02f4j8bdzeyt7p2fhx","object": "machine","image": "ubuntu-26.04","state": "destroyed","vcpus": 2,"memory_mib": 2048,"disk_mib": 10240,"size": "standard","price_per_hour_usd": "0.15","keep_alive": false,"command": {"status": "exited","exit_code": 0,"exit_signal": null,"exited_at": "2026-09-28T09:14:02.905Z"},"created_at": "2026-09-28T09:14:02.114Z","started_at": "2026-09-28T09:14:02.361Z","destroyed_at": "2026-09-28T09:14:03.188Z","expires_at": "2026-09-28T10:14:02.114Z","destroy_reason": "command_exited"}

Sizes and pricing

Machines are priced per second by their resources. Presets name common combinations of vCPUs and memory; any combination within the limits can be requested. GET /v1/sizes returns the rates, limits and presets below.

Presets

Prices include the default 10 GiB disk. standard is the default size.

sizevCPUMemoryPrice per hour
small11 GiB$0.075
standard22 GiB$0.15
medium24 GiB$0.185
large48 GiB$0.37
xlarge816 GiB$0.74

Rates

ResourcePrice per hour
vCPU$0.0575
Memory, per GiB$0.0175
Disk, per GiB beyond the first 10 GiB$0.000108

Limits

  • vcpus: 1 to 8.
  • memory_mib: 1024 to 16384 (1 GiB to 16 GiB), in steps of 1024, and at most 4096 (4 GiB) per vCPU.
  • disk_mib: one of 10240, 20480, 40960, 81920, 163840. The first 10 GiB are included in every machine.
  • Output: the latest 100 MiB per machine, its command and every exec together; the oldest is dropped first. See Stream logs.
  • Output rate: up to 16 MiB/s per machine after a 25 MiB burst. Faster output is dropped, and the log marks how much.
  • An account may have lower limits; see GET /v1/account.

Disk

disk_mib is the size of the machine's root disk. The machine boots from a copy of its image, expanded to this size, and the root filesystem spans the whole disk. Everything the machine writes is stored there: installed packages, cloned repositories, build output. The disk is destroyed with the machine.

Choose the smallest tier that fits the job's output: every image fits the smallest. The default, 10 GiB, has no additional cost.

disk_mibSizeAdded per hour
1024010 GiBincluded
2048020 GiB$0.00108
4096040 GiB$0.00324
8192080 GiB$0.00756
163840160 GiB$0.0162

Billing

  • Billed per second from started_at until the machine ends. Creation and boot are not billed. A machine kept after its command exits (keep_alive) is billed until it is deleted or expires. See When billing stops.
  • The subscription, $59 a month excluding VAT, includes 400 standard-hours. A standard-hour is $0.15 of machine time: an hour of the standard machine (2 vCPU, 2 GiB, 10 GiB). A machine uses standard-hours in proportion to its price: an hour of small ($0.075) uses half of one.
  • Beyond the included hours, machine time is charged to the prepaid usage balance at each machine's price.
  • A machine starts only if its first minute of running time, at its price, can be paid for from the included hours and the usage balance not already set aside for the account's running machines. Otherwise the create returns 402; with auto-recharge on, a recharge is requested first. See Errors.
An hour of 4 vCPU, 8 GiB, 40 GiB disk
vCPU    4 × $0.0575       $0.23memory  8 × $0.0175       $0.14disk   30 × $0.000108  $0.00324────────$0.37324disk: the first 10 GiB included
curl -X POST https://api.nitrous.sh/v1/machines \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Content-Type: application/json" \-d '{"image": "ubuntu-26.04","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960}'
GET/v1/sizes

Retrieve sizes

Returns the rates, limits, defaults and presets for machines. See Sizes and pricing. These are the limits for every account; an account's own, which can be lower, are on GET /v1/account.

  • objectstring
    sizes
  • ratesobject
    vcpu_hour_usd, memory_gib_hour_usd and disk_gib_hour_usd, as decimal strings, and disk_free_gib, the disk included with every machine.
  • limitsobject
    vcpus (min, max), memory_mib (min, max, step, max_per_vcpu) and disk_mib (tiers, and free, in MiB).
  • defaultsobject
    The vcpus, memory_mib and disk_mib used when a create specifies none.
  • presetsobject[]
    Each preset's name, vcpus, memory_mib and price_per_hour_usd, priced with the default disk.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/sizes \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "sizes","rates": {"vcpu_hour_usd": "0.0575","memory_gib_hour_usd": "0.0175","disk_gib_hour_usd": "0.000108","disk_free_gib": 10},"limits": {"vcpus": {"min": 1,"max": 8},"memory_mib": {"min": 1024,"max": 16384,"step": 1024,"max_per_vcpu": 4096},"disk_mib": {"tiers": [10240,20480,40960,81920,163840],"free": 10240}},"defaults": {"vcpus": 2,"memory_mib": 2048,"disk_mib": 10240},"presets": [{"name": "small","vcpus": 1,"memory_mib": 1024,"price_per_hour_usd": "0.075"},{"name": "standard","vcpus": 2,"memory_mib": 2048,"price_per_hour_usd": "0.15"},{"name": "medium","vcpus": 2,"memory_mib": 4096,"price_per_hour_usd": "0.185"},{"name": "large","vcpus": 4,"memory_mib": 8192,"price_per_hour_usd": "0.37"},{"name": "xlarge","vcpus": 8,"memory_mib": 16384,"price_per_hour_usd": "0.74"}]}
POST/v1/machines

Create a machine

Creates a machine and returns it once it is running: booted, with its command started. Returns the machine object with 201, and its URL in Location. The request waits up to 180 s for the machine to start. A command that exits before the response is sent still returns running; the next read shows its end.

size sets vcpus and memory_mib; each of them given explicitly replaces the size's value for that field only. With neither, the machine is standard. size in the response names the preset matching the machine's vCPUs and memory, or is null. A field sent as null is the same as leaving it out, except env and keep_alive, which return 400. Unknown and repeated fields are rejected.

Headers

  • Idempotency-Keystringoptional
    Defined in Idempotency. After a 2xx, a retry with the same key and body returns the stored response. Retry automatically only the outcomes listed under Retries.
    Allowed
    1–255 printable ASCII, no spaces

Body

  • imagestringrequired
    The image to boot, by name: a base image, a custom image (its published build), or a specific build as name@n. For a custom image, the machine's image is name@n, the build it booted. An image's id is not accepted here.
  • sizestringoptional
    A preset for vcpus and memory_mib. See Sizes and pricing.
    Default
    standard
    Allowed
    small | standard | medium | large | xlarge
  • vcpusintegeroptional
    Virtual CPUs.
    Default
    the size's
    Allowed
    1–8
  • memory_mibintegeroptional
    Memory, in MiB.
    Default
    the size's
    Allowed
    1024–16384, ×1024, ≤ 4096 per vCPU
  • disk_mibintegeroptional
    Root disk size, in MiB, holding everything the machine writes. The first 10 GiB are included; see Disk.
    Default
    10240
    Allowed
    10240 | 20480 | 40960 | 81920 | 163840
  • commandstring[]optional
    The program and its arguments, executed directly without a shell; use ["sh", "-c", "…"] for shell syntax. Without a command, the machine starts idle. 128 KiB is the sum of the entries' UTF-8 bytes.
    Allowed
    1–4096 entries, ≤ 128 KiB, no NUL
  • envobjectoptional
    Environment variables for the command, as a name-to-value map. Names consist of letters, digits and _, and do not start with a digit. 128 KiB is the sum of name + value + 2 bytes per variable. The place for secrets: never stored or returned.
    Allowed
    ≤ 1024 variables, ≤ 128 KiB, no NUL
  • cwdstringoptional
    The directory the command runs in.
    Default
    /root, or the image's WORKDIR
    Allowed
    1–4096 bytes, no NUL
  • stdinstringoptional
    Text written to the command's standard input. The place for secrets: never stored or returned.
    Allowed
    ≤ 1 MiB (UTF-8 bytes)
  • keep_alivebooleanoptional
    Keep the machine running after its command exits (command.status exited), until it is destroyed or expires. A machine without a command is always kept.
    Default
    false
  • ttl_secondsintegeroptional
    Seconds from creation, startup included, until the machine is destroyed regardless of state (expires_at).
    Default
    3600
    Allowed
    1–86400

command, env, cwd and stdin together must be at most 1,800,000 bytes, measured as the compact JSON object {"command", "env", "cwd", "stdin"}: keys included, an absent field as null, escapes as written (\", \\, \n 2 bytes; other control characters 6).

Errors

  • 400invalid_requestA field breaks its rule (ttl_seconds outside 1–86400, for example), the size is unknown, the requested resources are not supported, or the body exceeds 2 MiB. error names the field.
  • 402(billing)The account cannot pay for the machine. See the 402 codes in Errors.
  • 403account_disabledThe account is disabled, or was disabled while the request ran: nothing was started.
  • 403size_not_allowedThe machine exceeds the account's limits.
  • 404image_not_foundNo image with this name, or no such build of it.
  • 409image_not_publishedThe image has no published build. Publish one, or request name@n.
  • 409build_not_readyThe requested build is not ready.
  • 409machine_limit_reachedThe account has its maximum number of machines that have not ended, image builds included.
  • 409idempotency_key_in_useA request with this Idempotency-Key is still running.
  • 422idempotency_key_reusedThis Idempotency-Key was used for a different request.
  • 502machine_unavailableThe machine did not start in time.
  • 503capacity_exhaustedNo capacity is available; nothing was created. Retry in a few seconds.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/machines \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"image": "ubuntu-26.04","size": "medium","command": ["sh", "-c", "uname -a && df -h /"]}'
KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/machines \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"image": "ubuntu-26.04","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960,"keep_alive": true,"ttl_seconds": 7200}'
201Created· explicit resources
{"id": "m_01m3kmtt8reagbvwvkf5enc5xs","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960,"size": "large","price_per_hour_usd": "0.37324","keep_alive": true,"command": null,"created_at": "2026-09-28T09:16:45.208Z","started_at": "2026-09-28T09:16:45.455Z","destroyed_at": null,"expires_at": "2026-09-28T11:16:45.208Z","destroy_reason": null}
Response headers
HTTP/1.1 201 Createdcontent-type: application/jsonlocation: /v1/machines/m_01m3kmtt8reagbvwvkf5enc5xscache-control: no-storex-request-id: req_01m3m85f4ae718vk3954qmbskr
GET/v1/machines

List machines

Returns the account's machines that have not ended, newest first, as a list of machine objects. A machine that ended since the API last checked the host may appear once more, in its final state. Image builds' machines are not listed. Retrieve ended machines individually with GET /v1/machines/{id}.

Query

  • limitintegeroptional
    How many machines a page holds.
    Default
    50
    Allowed
    1–100
  • starting_afterstringoptional
    The id of the last machine on the previous page. Any of the account's machines, ended or not, keeps its place.

Errors

  • 400invalid_requestlimit is out of range, or starting_after is not a machine of the account.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl "https://api.nitrous.sh/v1/machines?limit=2" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "list","data": [{"id": "m_01m3kmtt8reagbvwvkf5enc5xs","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960,"size": "large","price_per_hour_usd": "0.37324","keep_alive": true,"command": null,"created_at": "2026-09-28T09:16:45.208Z","started_at": "2026-09-28T09:16:45.455Z","destroyed_at": null,"expires_at": "2026-09-28T11:16:45.208Z","destroy_reason": null},{"id": "m_01m3km187afrrsj0h1v7kedp8r","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 2,"memory_mib": 2048,"disk_mib": 10240,"size": "standard","price_per_hour_usd": "0.15","keep_alive": false,"command": {"status": "running","exit_code": null,"exit_signal": null,"exited_at": null},"created_at": "2026-09-28T09:02:47.530Z","started_at": "2026-09-28T09:02:47.777Z","destroyed_at": null,"expires_at": "2026-09-28T10:02:47.530Z","destroy_reason": null}],"has_more": true}
GET/v1/machines/{id}

Retrieve a machine

Returns the current machine object. After the machine ends, returns its final record, permanently (see Retention).

Path

  • idstringrequired
    The machine's id (m_…). Treat it as an opaque string.

Errors

  • 404machine_not_foundNo machine with this id exists in the account.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/machines/m_01m3kmnv02f4j8bdzeyt7p2fhx \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"id": "m_01m3kmnv02f4j8bdzeyt7p2fhx","object": "machine","image": "ubuntu-26.04","state": "running","vcpus": 2,"memory_mib": 2048,"disk_mib": 10240,"size": "standard","price_per_hour_usd": "0.15","keep_alive": false,"command": {"status": "running","exit_code": null,"exit_signal": null,"exited_at": null},"created_at": "2026-09-28T09:14:02.114Z","started_at": "2026-09-28T09:14:02.361Z","destroyed_at": null,"expires_at": "2026-09-28T10:14:02.114Z","destroy_reason": null}
POST/v1/machines/{id}/exec

Run a command

Runs a command in a running machine and waits for it to exit or time out. The machine's state does not change; files the command writes remain on disk for later commands. A non-zero exit is returned as a result with 200, not as an error.

Execs run concurrently: each call starts its own process, and nothing queues them. On a base image the command runs as root in /root, with PATH, HOME=/root and LANG=C.UTF-8; a custom image's ENV, WORKDIR and USER apply instead (see Custom images). The request's env and cwd override both.

Exec takes no Idempotency-Key. After a 5xx, a timeout or a dropped connection the command may have run, and without the result there is no exec_id: never retry an exec automatically. The full output of each exec is retained under its exec_id and can be read through GET …/logs?exec=. An exec whose machine is destroyed while it runs returns an error once timeout_seconds has passed. What exec returns in each phase of a machine's end is in Ending.

Path

  • idstringrequired
    The machine's id (m_…). Treat it as an opaque string.

Body

  • commandstring[]required
    The program and its arguments, executed directly without a shell; use ["sh", "-c", "…"] for shell syntax. Commands are kept in exec history; pass secrets in env, not in the command.
    Allowed
    1–4096 entries, 128 KiB, no NUL
  • envobjectoptional
    Environment variables, as a name-to-value map. Names consist of letters, digits and _, and do not start with a digit; values contain no NUL. Counted as for a create. The place for secrets: never stored or returned.
    Allowed
    ≤ 1024 variables, ≤ 128 KiB
  • cwdstringoptional
    The directory the command runs in.
    Allowed
    1–4096 bytes
  • stdinstringoptional
    Text written to the command's standard input. The place for secrets: never stored or returned.
    Allowed
    ≤ 1 MiB (UTF-8 bytes)
  • timeout_secondsintegeroptional
    Maximum run time, counted from when the process has started. When it is exceeded, SIGKILL is sent to the command's process group, the result has timed_out: true, and the host waits up to 5 s more for the exit. A process that left the group (setsid) is not killed. It does not extend the machine's expires_at. The API waits for the host up to timeout_seconds + 30 s, then returns 500; give a client deadline longer than that.
    Default
    60
    Allowed
    1–3600

command, env, cwd and stdin together must be at most 1,800,000 bytes, measured as for a create. cwd, stdin and timeout_seconds sent as null are the same as leaving them out; command and env as null return 400. Unknown and repeated fields are rejected.

The exec result

  • objectstring
    exec_result
  • exec_idstring
    The command's id: x- followed by 12 characters. Use it to read the command's output from logs.
  • exit_codeinteger | null
    The command's exit status. When signal N ended it, 128 + N (SIGKILL: 137). null if the command timed out and its end was not confirmed within 5 s.
  • exit_signalstring | null
    The signal that ended the command, such as SIGKILL. null when the command exited by itself, and whenever exit_code is null.
  • timed_outboolean
    timeout_seconds was exceeded and SIGKILL was sent. exit_code says whether the end was confirmed.
  • stdoutstring
    The first 1 MiB of standard output, as UTF-8 with invalid bytes replaced (U+FFFD). The exact bytes are in the text-format log.
  • stderrstring
    Standard error, with the same encoding and limit.
  • stdout_truncatedboolean
    stdout contains only the first 1 MiB, or output was dropped.
  • stderr_truncatedboolean
    The same, for stderr.
  • duration_msinteger
    Milliseconds from the host's request to start the command to its exit. With exit_code null, to when the host stopped waiting: timeout_seconds plus the 5 s confirmation wait.

Errors

  • 400invalid_requestA field breaks its rule, the program cannot be started (it does not exist), or the request has an Idempotency-Key.
  • 404machine_not_foundNo machine with this id exists in the account, or the machine has been torn down; error says which.
  • 409machine_conflictThe machine is not running: it is starting, or being torn down before its expires_at.
  • 409machine_expiredThe machine is past its expires_at and not yet torn down.
  • 500internal_errorNo answer from the host within timeout_seconds + 30 s. The command may have run.
  • 502machine_unavailableThe machine did not respond in time.
  • 502internal_errorThe host failed to run the command. It may have started.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl -X POST https://api.nitrous.sh/v1/machines/m_01m3kmtt8reagbvwvkf5enc5xs/exec \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Content-Type: application/json" \-d '{"command": ["python3", "-c", "import sys; print(sys.stdin.read().upper())"],"stdin": "hello from stdin","env": {"CI": "1"},"timeout_seconds": 30}'
200OK
{"object": "exec_result","exec_id": "x-d8144042000c","exit_code": 0,"exit_signal": null,"timed_out": false,"stdout": "HELLO FROM STDIN\n","stderr": "","stdout_truncated": false,"stderr_truncated": false,"duration_ms": 41}
GET/v1/machines/{id}/logs

Stream logs

Returns the output of the machine's command or of one exec: stdout and stderr, as written. Without follow, the response contains the output so far and closes. With follow=true, the response stays open and streams output as it arrives, until the command exits or the machine is destroyed.

  • A read starts at the beginning of the output, or with tail_lines at its last lines: what is retained first, then, with follow=true, new output as it arrives. There is no resume position: a reconnect reads again; NDJSON seq tells the chunks already seen.
  • Closing the connection ends the read, not the command.
  • A followed read ends after the command exits (in NDJSON, after the exit_code line), or when the machine can no longer write (destroyed, expired or crashed), then without an exit_code line. In text, the two look alike: read the machine's command, or use NDJSON. A transport error after the 200 aborts the body; there is no error line.
  • Each machine retains the latest 100 MiB of output, its command and every exec together. Past it, the oldest output is dropped and what remains begins with a marker: {"dropped_earlier":true,"limit_bytes":104857600} in NDJSON, the line [nitrous: earlier output was dropped (100 MiB limit)] in text.
  • An exec that writes more than 256 MiB, stdout and stderr together, has the rest dropped and counted once, with the exit. A command that has not exited has no count yet, and one whose machine ended first never gets one: its dropped bytes are not reported.
  • Output is retained for 7 days after destroyed_at.
  • What a read returns in each phase of the machine's end is in Ending.

Path

  • idstringrequired
    The machine's id (m_…). Treat it as an opaque string.

Query

  • execstringoptional
    Which output to read: main, the command the machine was created with, or an exec_id from exec.
    Default
    main
  • followbooleanoptional
    Stream until the command exits.
    Default
    false
    Allowed
    true | false
  • tail_linesintegeroptional
    Start at the last tail_lines lines instead of the beginning. The read starts at the chunk that ends the line before them, so that chunk may begin partway through an earlier line and hold whole lines before them: drop its text through its first \n and every line left is whole. With follow=true, new output follows.
    Allowed
    1 – 100000
  • formatstringoptional
    text: text/plain, the bytes as written, stdout and stderr interleaved. UTF-8 is not guaranteed: invalid bytes pass through, and no charset is declared. ndjson: application/x-ndjson, one JSON object per line.
    Default
    text
    Allowed
    text | ndjson

NDJSON lines

  • {"seq", "line", "stream", "text"}output
    An output chunk: seq, from 0 per exec, the same on every read; line, the line its text starts on, from 0: the lines before it, stdout and stderr together, each ended by \n, lines dropped at the machine's limit included, so the same on every read; stdout or stderr; and its text. Each stream is decoded as UTF-8 in order: a character split across chunks is whole in the chunk that completes it, and invalid bytes are U+FFFD. A character left incomplete at the end of the output is U+FFFD, on a last line numbered after the last chunk, before the exit.
  • {"dropped_earlier", "limit_bytes"}dropped
    Earlier output was dropped to keep the machine within limit_bytes (104857600, 100 MiB). Precedes the retained output.
  • {"dropped_bytes"}dropped
    The total number of output bytes dropped. At most once, before exit.
  • {"exit_code", "exit_signal"}exit
    The final line, after the command exits: the exit status and the signal, as in the machine object. Both null if its end was not observed.

In text format, dropped output appears as a line: [nitrous: earlier output was dropped (100 MiB limit)] before the retained output, [nitrous: N bytes of output were dropped] for an exec past 256 MiB; everything else is the bytes as written. Responses carry Cache-Control: no-store. A read without follow=true also carries Content-Disposition: attachment, named for the machine: {id}.log, {id}-{exec}.log for an exec, or .ndjson.

Errors

  • 400invalid_requestexec contains characters other than letters, digits, - and _; format is neither text nor ndjson; follow is neither true nor false; or tail_lines is not an integer from 1 to 100000.
  • 404machine_not_foundNo machine with this id exists in the account, or its output is past retention; error says which.
  • 404log_not_foundNo output exists for this exec. A machine created without a command has no main output.
  • 409machine_expiredfollow=true on a machine past its expires_at and not yet torn down.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl -N "https://api.nitrous.sh/v1/machines/m_01m3kmnv02f4j8bdzeyt7p2fhx/logs?follow=true" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200text/plain
6.1.1282
200application/x-ndjson· format=ndjson
{"seq":0,"line":0,"stream":"stdout","text":"6.1.128\n"}{"seq":1,"line":1,"stream":"stdout","text":"2\n"}{"exit_code":0,"exit_signal":null}
200application/x-ndjson· stderr, dropped output, exit 1
{"seq":0,"line":0,"stream":"stdout","text":"compiling\n"}{"seq":1,"line":1,"stream":"stderr","text":"error: out of space\n"}{"dropped_bytes":1048576}{"exit_code":1,"exit_signal":null}
200application/x-ndjson· the machine ended first: no exit_code line
{"seq":0,"line":0,"stream":"stdout","text":"step 1 of 3\n"}
200application/x-ndjson· tail_lines=2
{"seq":7119,"line":40211,"stream":"stdout","text":"....... [100%]\n"}{"seq":7120,"line":40212,"stream":"stdout","text":"40214 passed\n"}{"seq":7121,"line":40213,"stream":"stdout","text":"== done in 8m 12s ==\n"}{"exit_code":0,"exit_signal":null}
DELETE/v1/machines/{id}

Delete a machine

Deletes the machine immediately, regardless of state, and returns it in the state destroyed. The disk is deleted; the output is retained for 7 days (see Retention). Running execs and followed logs end with the machine. Deleting a machine that has already ended has no effect and returns its final state.

Path

  • idstringrequired
    The machine's id (m_…). Treat it as an opaque string.

Errors

  • 404machine_not_foundNo machine with this id exists in the account.
  • 502internal_errorThe host could not confirm the teardown. The machine remains destroying until it does. Billing stops as in When billing stops.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl -X DELETE https://api.nitrous.sh/v1/machines/m_01m3kmtt8reagbvwvkf5enc5xs \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"id": "m_01m3kmtt8reagbvwvkf5enc5xs","object": "machine","image": "ubuntu-26.04","state": "destroyed","vcpus": 4,"memory_mib": 8192,"disk_mib": 40960,"size": "large","price_per_hour_usd": "0.37324","keep_alive": true,"command": null,"created_at": "2026-09-28T09:16:45.208Z","started_at": "2026-09-28T09:16:45.455Z","destroyed_at": "2026-09-28T09:31:40.527Z","expires_at": "2026-09-28T11:16:45.208Z","destroy_reason": "deleted"}

Custom images

Custom images are built from a Dockerfile, tested in a machine, and published. A custom image is private to its account: no other account can see or boot it. The images every account can boot are the base images.

Build a custom image walks through the whole path, and covers what a build does and how the Dockerfile's ENV, WORKDIR and USER apply to machines. Dockerfiles and build logs are kept for the image's lifetime, and ENV values are part of the image. Don't put secrets in them.

Bring your own image

For anything that needs local files, build the image yourself, locally or in CI, with docker build --platform linux/amd64, and push it to a registry. Then create a build whose Dockerfile is one line: FROM ghcr.io/you/app:tag. Public registries only.

Commands run as argv, so the image needs no shell. A shell (/bin/sh) is needed only for sh -c commands and the terminal.

Limits

  • 10 images per account; 2 builds running at once.
  • Dockerfiles of at most 64 KiB. A build is stopped after 30 minutes; the image content must fit in 10 GiB.
  • Build logs: the last 1 MiB is retained once the build ends. See Stream build logs.
  • No build context: COPY and ADD of local files are rejected. Use RUN curl or git clone, or bring your own image.
  • linux/amd64. Public registries only.
  • Any Linux image boots, from any base, FROM scratch included: nothing needs to be installed. Commands and exec run as argv, with no shell; sh -c and the terminal need a shell (/bin/sh) in the image. An image whose /sbin/init is systemd boots systemd.

A machine booted from a custom image can use any size, with a disk of at least 10 GiB.

Dockerfile
FROM debian:13RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git
KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/images/web-runner/builds \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"dockerfile": "FROM debian:13\nRUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git\n"}'

The image object

One of the account's custom images. Returned by list, create, retrieve and publish.

  • idstring
    The image's id (img_…). Treat it as an opaque string.
  • objectstring
    image
  • namestring
    The image's name: the value of image on machine create. It cannot be changed.
  • published_buildinteger | null
    The build the image's name boots. null until a build is published.
  • latest_buildobject | null
    The newest build's {"number", "status"}, whatever its status: it can differ from published_build. null until the first build. The builds themselves are a list of their own.
  • created_atstring
    When the image was created.
  • updated_atstring
    When the image last changed: a build was started, a build ended, or a build was published.
The image object
{"id": "img_01m3kn0phjfxka50m2f7x78fdk","object": "image","name": "web-runner","published_build": 1,"latest_build": {"number": 1,"status": "ready"},"created_at": "2026-09-28T09:19:58.002Z","updated_at": "2026-09-28T09:24:05.311Z"}
GET/v1/images

List images

Returns the account's custom images, newest first, as a list of image objects. The base images are listed by GET /v1/base-images.

Query

  • limitintegeroptional
    How many images a page holds.
    Default
    50
    Allowed
    1–100
  • starting_afterstringoptional
    The id of the last image on the previous page.

Errors

  • 400invalid_requestlimit is out of range, or starting_after is not an image of the account.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/images \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "list","data": [{"id": "img_01m3kn0phjfxka50m2f7x78fdk","object": "image","name": "web-runner","published_build": 1,"latest_build": {"number": 1,"status": "ready"},"created_at": "2026-09-28T09:19:58.002Z","updated_at": "2026-09-28T09:24:05.311Z"}],"has_more": false}
POST/v1/images

Create an image

Creates an image with no builds and returns it with 201, and its URL in Location. The name must be unique in the account and cannot match a base image.

Headers

  • Idempotency-Keystringoptional
    Defined in Idempotency. After a 2xx, a retry with the same key and body returns the stored response. Retry automatically only the outcomes listed under Retries.
    Allowed
    1–255 printable ASCII, no spaces

Body

  • namestringrequired
    3 to 40 lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit.
    Allowed
    ^[a-z][a-z0-9-]{1,38}[a-z0-9]$

Errors

  • 400invalid_nameThe name does not match the rule.
  • 409image_existsThe account already has an image with this name, or a base image uses it.
  • 409image_limit_reachedThe account has 10 images, its limit.
  • 409idempotency_key_in_useA request with this Idempotency-Key is still running.
  • 422idempotency_key_reusedThis Idempotency-Key was used for a different request.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/images \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"name": "web-runner"}'
201Created
{"id": "img_01m3kn0phjfxka50m2f7x78fdk","object": "image","name": "web-runner","published_build": null,"latest_build": null,"created_at": "2026-09-28T09:19:58.002Z","updated_at": "2026-09-28T09:19:58.002Z"}
409Conflict· the name is taken
{"error": "An image named \"web-runner\" already exists.","code": "image_exists","request_id": "req_01m3m85f4ae718vk3954qmbskr"}
GET/v1/images/{image}

Retrieve an image

Returns a custom image object, by its id or its name. Its builds are listed by GET /v1/images/{image}/builds.

Path

  • imagestringrequired
    The image's id (img_…) or its name.

Errors

  • 404image_not_foundNo image with this id or name in the account.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/images/img_01m3kn0phjfxka50m2f7x78fdk \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"id": "img_01m3kn0phjfxka50m2f7x78fdk","object": "image","name": "web-runner","published_build": 1,"latest_build": {"number": 2,"status": "building"},"created_at": "2026-09-28T09:19:58.002Z","updated_at": "2026-09-28T10:40:53.874Z"}
DELETE/v1/images/{image}

Delete an image

Deletes the image and all its builds. A running build is stopped first. Machines already running from the image are unaffected; new machines can no longer boot it. A second delete returns 404 image_not_found.

Path

  • imagestringrequired
    The image's id (img_…) or its name.

Errors

  • 404image_not_foundNo image with this id or name in the account.
  • 502internal_errorThe image was not deleted. Running builds may have been stopped. Retry the request.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl -X DELETE https://api.nitrous.sh/v1/images/web-runner \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
204No Content

No response body

POST/v1/images/{image}/publish

Publish a build

Sets a ready build as the one the image's name boots, and returns the image object. Running machines are unaffected. The last publish wins; publishing the published build again succeeds and moves updated_at.

Path

  • imagestringrequired
    The image's id (img_…) or its name.

Headers

  • Idempotency-Keystringoptional
    Defined in Idempotency. After a 2xx, a retry with the same key and body returns the stored response. Retry automatically only the outcomes listed under Retries.
    Allowed
    1–255 printable ASCII, no spaces

Body

  • buildintegerrequired
    The build's number.

Errors

  • 404image_not_foundNo image with this id or name in the account.
  • 404build_not_foundThe image has no build with this number.
  • 409build_not_readyThe build is not ready.
  • 409idempotency_key_in_useA request with this Idempotency-Key is still running.
  • 422idempotency_key_reusedThis Idempotency-Key was used for a different request.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/images/web-runner/publish \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"build": 1}'
200OK
{"id": "img_01m3kn0phjfxka50m2f7x78fdk","object": "image","name": "web-runner","published_build": 1,"latest_build": {"number": 1,"status": "ready"},"created_at": "2026-09-28T09:19:58.002Z","updated_at": "2026-09-28T09:24:05.311Z"}
409Conflict· publishing build 2 while it builds
{"error": "Build 2 of the image \"web-runner\" is building. Only a ready build can be published.","code": "build_not_ready","request_id": "req_01m3m85f4ae718vk3954qmbskr"}

The base image object

A built-in image every account can boot by name. Base images have no id and no builds. They are updated in place: a machine boots the current version, and updated_at says when it was built.

Built-in images include systemd, bash, curl, wget, git, jq, python3, tmux, zip and unzip.

  • objectstring
    base_image
  • namestring
    The image's name: the value of image on machine create.
  • descriptionstring
    The distribution and version, e.g. Ubuntu 26.04 LTS.
  • updated_atstring | null
    When the image was last built. null if the host does not record it.
The base image object
{"object": "base_image","name": "ubuntu-26.04","description": "Ubuntu 26.04 LTS","updated_at": "2026-09-24T08:02:11.000Z"}
GET/v1/base-images

List base images

Returns the base images, by name from A to Z, as a list of base image objects.

Query

  • limitintegeroptional
    How many images a page holds.
    Default
    50
    Allowed
    1–100
  • starting_afterstringoptional
    The name of the last image on the previous page.

Errors

  • 400invalid_requestlimit is out of range, or starting_after is not a base image.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/base-images \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "list","data": [{"object": "base_image","name": "ubuntu-26.04","description": "Ubuntu 26.04 LTS","updated_at": "2026-09-24T08:02:11.000Z"}],"has_more": false}

The build object

One build of a custom image, addressed by the image and its number.

  • objectstring
    build
  • numberinteger
    1 for the image's first build, then 2, 3…
  • imagestring
    The image's id.
  • statusstring
    queued, building, ready or failed.
  • errorstring | null
    Why the build failed. null unless failed.
  • size_mibinteger | null
    Space used by the image content, in MiB, once ready.
  • created_atstring
    When the build was requested.
  • started_atstring | null
    When the build machine started. null if the build failed before.
  • finished_atstring | null
    When the build became ready or failed.
  • duration_secondsnumber | null
    From started_at, when the build machine is running, to finished_at, after the image is imported; to the time of the response while the build runs. Time before started_at is not counted. null before started_at.
  • dockerfilestring
    The Dockerfile. Only on Retrieve a build, not in a list.
The build object
{"object": "build","number": 1,"image": "img_01m3kn0phjfxka50m2f7x78fdk","status": "ready","error": null,"size_mib": 812,"created_at": "2026-09-28T09:20:11.408Z","started_at": "2026-09-28T09:20:12.950Z","finished_at": "2026-09-28T09:23:40.117Z","duration_seconds": 207.167,"dockerfile": "FROM debian:13\nRUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git\n"}
POST/v1/images/{image}/builds

Create a build

Starts a build of the image from a Dockerfile and returns it as queued, with 202 and the build's URL in Location. 202 accepts the build; it does not mean it will succeed. The build becomes building once its machine runs, then ready or failed; a failed build has error set, and one still running 30 minutes after started_at is failed. The build machine's start, before started_at, is not counted. Poll it every few seconds, as the guide does. Builds have no build context; see Limits.

A build runs on a machine in the account: 2 vCPU, 2 GiB and a 40 GiB disk (or the largest disk tier the account's limit allows, if smaller), billed like any machine at $0.15324 an hour (see Disk). It is admitted like a machine create: it can be refused for billing reasons or by the account's machine limit.

Path

  • imagestringrequired
    The image's id (img_…) or its name.

Headers

  • Idempotency-Keystringoptional
    Defined in Idempotency. After a 2xx, a retry with the same key and body returns the stored response. Retry automatically only the outcomes listed under Retries.
    Allowed
    1–255 printable ASCII, no spaces

Body

  • dockerfilestringrequired
    The Dockerfile, as text.
    Allowed
    ≤ 64 KiB (UTF-8 bytes)

Errors

  • 400invalid_dockerfileThe Dockerfile exceeds 64 KiB, has no FROM, or uses COPY or ADD of local files. error names the line.
  • 402(billing)The account cannot pay for the build machine. See the 402 codes in Errors.
  • 403account_disabledThe account is disabled, or was disabled while the request ran: nothing was started.
  • 403size_not_allowedThe account's limits do not allow a standard machine (2 vCPUs, 2048 MiB of memory) to run the build.
  • 404image_not_foundNo image with this id or name in the account.
  • 409build_limit_reachedThe account is running 2 builds, its limit.
  • 409machine_limit_reachedThe account is running its maximum number of machines; a build uses one.
  • 409idempotency_key_in_useA request with this Idempotency-Key is still running.
  • 422idempotency_key_reusedThis Idempotency-Key was used for a different request.
  • 503capacity_exhaustedNo capacity is available. Retry in a few seconds.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

KEY=$(uuidgen)curl -X POST https://api.nitrous.sh/v1/images/web-runner/builds \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY" \-H "Idempotency-Key: $KEY" \-H "Content-Type: application/json" \-d '{"dockerfile": "FROM debian:13\nRUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git\n"}'
202Accepted
{"object": "build","number": 2,"image": "img_01m3kn0phjfxka50m2f7x78fdk","status": "queued","error": null,"size_mib": null,"created_at": "2026-09-28T10:40:52.310Z","started_at": null,"finished_at": null,"duration_seconds": null}
400Bad Request· a Dockerfile whose line 2 is COPY app.js .
{"error": "Line 2: COPY and ADD of local files are rejected: there is no build context. Use RUN curl or git clone, or push your own image to a public registry and use FROM <image>.","code": "invalid_dockerfile","request_id": "req_01m3m85f4ae718vk3954qmbskr"}
GET/v1/images/{image}/builds

List builds

Returns the image's builds, newest first, as a list of build objects without dockerfile; retrieve a build to read it.

Path

  • imagestringrequired
    The image's id (img_…) or its name.

Query

  • limitintegeroptional
    How many builds a page holds.
    Default
    50
    Allowed
    1–100
  • starting_afterintegeroptional
    The number of the last build on the previous page.

Errors

  • 400invalid_requestlimit is out of range, or starting_after is not a build of the image.
  • 404image_not_foundNo image with this id or name in the account.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl "https://api.nitrous.sh/v1/images/web-runner/builds?limit=2" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "list","data": [{"object": "build","number": 3,"image": "img_01m3kn0phjfxka50m2f7x78fdk","status": "building","error": null,"size_mib": null,"created_at": "2026-09-28T10:47:15.520Z","started_at": "2026-09-28T10:47:16.981Z","finished_at": null,"duration_seconds": 41.2},{"object": "build","number": 2,"image": "img_01m3kn0phjfxka50m2f7x78fdk","status": "failed","error": "The Dockerfile did not build: RUN \"apt-get install -y nodejs\" exited with code 100.","size_mib": null,"created_at": "2026-09-28T10:40:52.310Z","started_at": "2026-09-28T10:40:53.874Z","finished_at": "2026-09-28T10:42:01.236Z","duration_seconds": 67.362}],"has_more": true}
GET/v1/images/{image}/builds/{number}

Retrieve a build

Returns the build object, including dockerfile. Poll until the status is ready or failed.

Path

  • imagestringrequired
    The image's id (img_…) or its name.
  • numberintegerrequired
    The build's number: 1 for the image's first build, then 2, 3…

Errors

  • 404image_not_foundNo image with this id or name in the account.
  • 404build_not_foundThe image has no build with this number.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/images/web-runner/builds/1 \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "build","number": 1,"image": "img_01m3kn0phjfxka50m2f7x78fdk","status": "ready","error": null,"size_mib": 812,"created_at": "2026-09-28T09:20:11.408Z","started_at": "2026-09-28T09:20:12.950Z","finished_at": "2026-09-28T09:23:40.117Z","duration_seconds": 207.167,"dockerfile": "FROM debian:13\nRUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git\n"}
GET/v1/images/{image}/builds/{number}/logs

Stream build logs

Returns the build output as text: the docker build output and the builder's own steps. While the build runs, a read starts at the beginning of the output, and follow=true keeps the response open and streams output as it arrives, until the build ends. After the build ends, the last 1 MiB of the log is retained. A log missing its earlier output, cut at 1 MiB or past the machine's output limit, begins with the line [nitrous: earlier output was dropped (1 MiB limit)], with the limit that applied, as machine logs do. The builder's own timing covers its steps; duration_seconds, from started_at to finished_at, also covers the import after them. It does not cover the build machine's start.

Path

  • imagestringrequired
    The image's id (img_…) or its name.
  • numberintegerrequired
    The build's number: 1 for the image's first build, then 2, 3…

Query

  • followbooleanoptional
    Stream until the build ends.
    Default
    false
    Allowed
    true | false

Errors

  • 400invalid_requestfollow is neither true nor false, or the query has a parameter other than follow.
  • 404image_not_foundNo image with this id or name in the account.
  • 404build_not_foundThe image has no build with this number.
  • 503service_unavailableMachines are temporarily unavailable. Retry in a few seconds.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl -N "https://api.nitrous.sh/v1/images/web-runner/builds/1/logs?follow=true" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200text/plain; charset=utf-8
#1 [internal] load build definition from Dockerfile#5 [1/2] FROM docker.io/library/debian:13#6 [2/2] RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git#6 DONE 58.2snitrous-build: done: 812 MiB used, in 188s

The account object

The account an API key belongs to, and its limits.

  • idstring
    The account's id.
  • objectstring
    account
  • namestring
    The account's name.
  • machinesinteger
    The number of the account's machines that have not ended (starting or running), image builds included.
  • limitsobject
    machines: the maximum number of machines that have not ended, image builds included. vcpus, memory_mib, disk_mib: the maximum for a single machine. A create that exceeds them returns 409 machine_limit_reached or 403 size_not_allowed.
The account object
{"id": "acc_01m1k32987fdkt4tvzcarrabs7","object": "account","name": "Acme","machines": 2,"limits": {"machines": 10,"vcpus": 8,"memory_mib": 16384,"disk_mib": 163840}}
GET/v1/account

Retrieve the account

Returns the key's account object.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl https://api.nitrous.sh/v1/account \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"id": "acc_01m1k32987fdkt4tvzcarrabs7","object": "account","name": "Acme","machines": 2,"limits": {"machines": 10,"vcpus": 8,"memory_mib": 16384,"disk_mib": 163840}}
GET/v1/usage

Retrieve usage

Returns machine time for one calendar month (UTC): the machines that ran, for how long in total and per resource, and that time in standard-hours and at list price. Image builds are included. Charges (included hours used, the usage balance) are on the dashboard's Billing page.

The usage object covers one month for one account, so it is an object, not a list.

Query

  • monthstringoptional
    The month.
    Default
    this month
    Allowed
    YYYY-MM
  • objectstring
    usage
  • account_idstring
    The account.
  • from, tostring
    The month's start and the next month's start. A machine counts for its running time inside [from, to); a running machine up to the time of the response.
  • machinesinteger
    Machines that ran in the month.
  • machine_secondsnumber
    The total running time of those machines, to the millisecond.
  • vcpu_seconds, memory_gib_seconds, disk_gib_secondsnumber
    Running time multiplied by each resource, to 3 decimals.
  • machine_hoursnumber
    machine_seconds in hours, to 6 decimals.
  • standard_hoursnumber
    The machine time in standard-hours: its list price divided by the standard machine's hourly price, rounded half up to 2 decimals. The unit of the subscription's included hours. Priced as billing meters it: whole seconds per machine, the last one rounded up.
  • cost_usdstring
    The machine time at list price, in US dollars as a decimal string, rounded half up to the cent. Not the amount charged: included hours are not deducted.

Errors

  • 400invalid_requestmonth is not YYYY-MM, or a query parameter other than month.

Every endpoint can also return 401 unauthorized, 403 account_disabled, 429 rate_limited and 500 internal_error.

curl "https://api.nitrous.sh/v1/usage?month=2026-09" \--fail-with-body \-H "Authorization: Bearer $NITROUS_KEY"
200OK
{"object": "usage","account_id": "acc_01m1k32987fdkt4tvzcarrabs7","from": "2026-09-01T00:00:00.000Z","to": "2026-10-01T00:00:00.000Z","machines": 42,"machine_seconds": 58839.75,"vcpu_seconds": 125954.5,"memory_gib_seconds": 172812,"disk_gib_seconds": 894685,"machine_hours": 16.344375,"standard_hours": 19.07,"cost_usd": "2.86"}