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.
- 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"] }'
{ "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.
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
- 401
unauthorizedThe 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-AuthenticateisBearer realm="nitrous", witherror="invalid_token"when a Bearer key was sent. - 403
account_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"
HTTP/1.1 401 Unauthorized content-type: application/ json www-authenticate: Bearer realm="nitrous", error="invalid_token" cache-control: no-store x-request-id: req_01m3m85f4ae718vk3954qmbskr { "error": "A valid API key is required in the Authorization header (Bearer nx_…).", "code": "unauthorized", "request_id": "req_01m3m85f4ae718vk3954qmbskr" }
{ "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. TheContent-Typeheader 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
204with no body. - Every response carries
Cache-Control: no-storeand an x-request-id.
HTTP/1.1 201 Created content-type: application/ json location: / v1/ machines/ m_01m3kmnv02f4j8bdzeyt7p2fhx cache-control: no-store x-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.
| Format | Example | |
|---|---|---|
| IDs | A type prefix and a TypeID. Opaque strings: compare them, never parse them. | m_01m3kmnv02f4j8bdzeyt7p2fhx |
| Timestamps | RFC 3339, UTC, milliseconds | 2026-09-28T09:14:02.114Z |
| Memory, disk | Integers, in MiB | memory_mib: 2048 |
| Prices | Decimal strings, in US dollars | "0.15" |
Exec results and builds use the identifiers described in their sections.
{ "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.
errorstringWhat was wrong, and where useful what to do, in one sentence.codestringA machine-readable error code, one of those listed below.request_idstringThe request's ID, identical to thex-request-idheader. See Request IDs.
Status codes and error codes
- 400
invalid_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. - 400
invalid_nameThe image name is not 3 to 40 lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit. - 400
invalid_dockerfileThe Dockerfile exceeds 64 KiB, has no FROM, or uses COPY or ADD of local files. - 401
unauthorizedThe request has no valid API key. - 402
subscription_inactiveThe subscription is not active. Start it, or retry its payment, on the Billing page. - 402
included_usage_exhaustedThe period's included hours are used and the usage balance is empty. Add funds on the Billing page. - 402
insufficient_usage_balanceThe usage balance cannot fund another machine. Add funds on the Billing page. - 402
auto_recharge_failedAuto-recharge failed and the usage balance cannot fund another machine. Update the payment method or add funds on the Billing page. - 402
billing_account_lockedBilling for the account is locked. Contact billing@nitrous.sh. - 402
payments_unavailablePayments are not available yet, so machines cannot start. - 403
account_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. - 403
size_not_allowedThe machine exceeds the account's limits (GET /v1/account). - 404
machine_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. - 404
image_not_foundNo image with this id or name, or no such build of it, exists in the account. - 404
build_not_foundThe image has no build with this number. - 404
log_not_foundThe machine has no output for this exec. - 404
not_foundThe path does not exist. - 405
method_not_allowedThe path does not support this method. The Allow header lists the methods it does. - 409
machine_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. - 409
machine_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. - 409
machine_limit_reachedThe account has its maximum number of machines that have not ended (starting or running), image builds included. Delete one to start another. - 409
image_existsThe account already has an image with this name, or the name belongs to a base image. - 409
image_limit_reachedThe account has 10 images, its limit. Delete one to create another. - 409
image_not_publishedThe image was requested by name and has no published build. Publish a build, or request name@n. - 409
build_not_readyThe build is queued, building or failed. Only a ready build can be booted or published. - 409
build_limit_reachedThe account is running 2 builds, its limit. Start another when one finishes. - 409
idempotency_key_in_useA request with this Idempotency-Key is still running. Retry once it has finished. - 422
idempotency_key_reusedThis Idempotency-Key was used for a different request. Use a new key for each new request. - 429
rate_limitedThe account exceeded its rate limit. Retry after the number of seconds in Retry-After. - 500
internal_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. - 502
internal_errorThe compute host failed the operation. error says which operation. Retry as for 500. - 502
machine_unavailableThe machine did not respond in time (on create: it did not start in time). Retry as for 500. - 503
capacity_exhaustedNo capacity is available for another machine. Nothing was created: retry in a few seconds. - 503
service_unavailableMachines are temporarily unavailable. The request did not run: retry in a few seconds.
{ "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" }
{ "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
objectstringlistdataarrayThe page's objects, in the list's order.has_morebooleanMore objects follow this page.
Query
limitintegeroptionalHow many objects a page holds.- Default
- 50
- Allowed
- 1–100
starting_afterstring | integeroptionalThe cursor of the last object on the previous page. The page starts after it.
| Endpoint | Order | starting_after | If the cursor resource is removed |
|---|---|---|---|
| GET /v1/machines | created_at, then id, newest first | id (string) | Never removed: an ended machine stays a valid cursor |
| GET /v1/images | created_at, then id, newest first | id (string) | Deleted image: 400 invalid_request |
| GET /v1/images/{image}/builds | number, highest first | number (integer) | Deleted with its image: 404 image_not_found |
| GET /v1/base-images | name, A to Z | name (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 paging | Appears |
|---|---|
| A machine, image or build | On no later page: it sorts before the cursor. |
| A base image | On 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"
{ "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 | |
|---|---|
| Key | 1 to 255 printable ASCII characters, no spaces. Scoped to the account, across its API keys. |
| Same request | Same 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. |
| Window | 24 hours from the first request with the key. Every guarantee here holds for those 24 hours; after them the key is free. |
| Replay | The stored status and body, with Content-Type, Idempotent-Replayed: true, Location and Cache-Control, and a new x-request-id. |
| The first request | A retry with the same key and request |
|---|---|
| Still running, including after the caller disconnected or timed out: it runs to its end | 409 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 lost | A 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 request | 422 idempotency_key_reused |
Retries
Retry automatically, under the same key, only what was not applied. Stop on anything else.
| Outcome | Retry | Why |
|---|---|---|
429 rate_limited | After Retry-After | Refused before the request ran. |
409 idempotency_key_in_use | After Retry-After | The first request is still running; a retry after it succeeds returns its response. |
503 capacity_exhausted, 503 service_unavailable | With backoff | Refused before anything was made. |
| No connection made (DNS failure, connection refused, connect timeout) | With backoff | The request was never sent. |
| Other 4xx | No | Nothing was made. Fix the request. |
500, 502, a timeout, a connection lost after sending | A create or build: under the same key. Anything else: no | A 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.
| Request | Find what it made |
|---|---|
POST /v1/machines | List 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/images | Retrieve the image by name: names are unique in the account. |
POST …/builds | List the image's builds: a build with created_at after the first attempt was sent. Builds are never left out of the list. |
POST …/publish | Retrieve 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" }'
HTTP/1.1 201 Created content-type: application/ json idempotent-replayed: true location: / v1/ machines/ m_01m3kmtt8reagbvwvkf5enc5xs cache-control: no-store x-request-id: req_01m3nz1ag8fm5rj84acm7kxfek
{ "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.
HTTP/1.1 200 OK content-type: application/ json x-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
/v1endpoint 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_foundfor an unknown path,405, and the429itself. - Beyond the limit:
429 rate_limitedwithRetry-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.
HTTP/1.1 429 Too Many Requests content-type: application/ json retry-after: 1 x-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.
idstringThe machine's id (m_…). Treat it as an opaque string.objectstringmachineimagestringThe booted image: a base image's name, or name@n for a build of a custom image.statestringstarting,running,destroying,destroyedorfailed: the machine's, not its command's (command.status). A kept machine whose command exited isrunning.failedis a platform or boot failure. See Lifecycle.vcpusintegerVirtual CPUs.memory_mibintegerMemory, in MiB.disk_mibintegerRoot disk size, in MiB.sizestring | nullThe preset matching the machine's vCPUs and memory, regardless of disk; null if none matches.price_per_hour_usdstringHourly price in US dollars, as a decimal string.keep_alivebooleanThe machine is kept after its command exits. Always true for a machine without a command.commandobject | nullThe machine's command, apart from the machine.nullfor a machine created without a command.command.statusstringrunninguntil the command exits, thenexited. A command whose machine ended first isexited, withexit_codenull.command.exit_codeinteger | nullThe command's exit status. When signal N ended it, 128 + N (SIGKILL:137), as in a shell or Docker.nullwhile it runs.nullwithstatusexitedmeans the exit was not observed:destroy_reasonsays how the machine ended.command.exit_signalstring | nullThe signal that ended the command, such asSIGKILLorSIGTERM.nullwhen the command exited by itself, and wheneverexit_codeisnull.command.exited_atstring | nullWhen the command exited. null while it runs, or if its exit was not observed.created_atstringWhen the machine was requested.started_atstring | nullWhen the machine started running, with its command. Billing starts at this time. null if it never started.destroyed_atstring | nullWhen teardown completed, or for a machine that did not start, when its start failed. Retention counts from this time.expires_atstringcreated_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" | nullWhy the machine was destroyed;nulluntil it is. See Lifecycle.
{ "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:
| Machine | States | command.status |
|---|---|---|
| With a command | starting → running → destroying → destroyed | running → exited |
With a command and keep_alive | starting → running → destroying → destroyed | running → exited, while the machine is running |
| Without a command | starting → running → destroying → destroyed | command is null |
| Did not start | starting → failed | exited |
| State | Meaning | Billed |
|---|---|---|
| starting | Being created and booted. | no |
| running | Booted; the command, if any, has started. A kept machine (keep_alive) stays running after its command exits, and accepts exec. | yes |
| destroying | Being 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 |
| destroyed | Torn down (destroyed_at). The record and output remain; see Retention. | no |
| failed | The 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.
| Phase | state | Exec | Logs | Logs, follow=true |
|---|---|---|---|---|
| Active | running | Runs | Output so far | Streams until the command exits |
| Ending before expires_at: deleted, command exited, crashed | destroying | 409 machine_conflict | Output so far | Streams what is left, then closes |
| Past expires_at, being torn down | destroying | 409 machine_expired | Output so far | 409 machine_expired |
| Torn down, for 7 days from destroyed_at | destroyed | 404 machine_not_found | Retained output | Retained output, then closes |
| Past retention | destroyed | 404 machine_not_found | 404 machine_not_found | 404 machine_not_found |
When billing stops
| End | destroy_reason | Billing stops at |
|---|---|---|
| The command exits, keep_alive false | command_exited | The exit (command.exited_at) |
| Deleted | deleted | When the platform records the machine as ending, no later than destroyed_at |
| Time to live | ttl_expired | expires_at |
| Paid time runs out | billing | When the platform records the machine as ending, no later than destroyed_at |
| Crash, out of memory, host maintenance, host restart | crashed, out_of_memory, host_cleanup, host_lost | When the platform records the machine as ending, no later than destroyed_at |
| Did not start | null (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:
| Value | Meaning |
|---|---|
| deleted | Destroyed with DELETE /v1/machines/{id}. |
| command_exited | The command exited and keep_alive was not set. |
| ttl_expired | The machine reached expires_at. |
| billing | Usage could no longer be paid for: the usage balance ran out or the subscription is not active. |
| out_of_memory | The machine exceeded its memory. |
| crashed | The machine stopped unexpectedly while its command was running. |
| host_cleanup | Removed during host maintenance. |
| host_lost | The 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.
{ "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.
| size | vCPU | Memory | Price per hour |
|---|---|---|---|
| small | 1 | 1 GiB | $0.075 |
| standard | 2 | 2 GiB | $0.15 |
| medium | 2 | 4 GiB | $0.185 |
| large | 4 | 8 GiB | $0.37 |
| xlarge | 8 | 16 GiB | $0.74 |
Rates
| Resource | Price 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_mib | Size | Added per hour |
|---|---|---|
| 10240 | 10 GiB | included |
| 20480 | 20 GiB | $0.00108 |
| 40960 | 40 GiB | $0.00324 |
| 81920 | 80 GiB | $0.00756 |
| 163840 | 160 GiB | $0.0162 |
Billing
- Billed per second from
started_atuntil 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
standardmachine (2 vCPU, 2 GiB, 10 GiB). A machine uses standard-hours in proportion to its price: an hour ofsmall($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.
vCPU 4 × $0.0575 $0.23 memory 8 × $0.0175 $0.14 disk 30 × $0.000108 $0.00324 ──────── $0.37324 disk: 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 }'
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.
objectstringsizesratesobjectvcpu_hour_usd,memory_gib_hour_usdanddisk_gib_hour_usd, as decimal strings, anddisk_free_gib, the disk included with every machine.limitsobjectvcpus(min,max),memory_mib(min,max,step,max_per_vcpu) anddisk_mib(tiers, andfree, in MiB).defaultsobjectThe vcpus, memory_mib and disk_mib used when a create specifies none.presetsobject[]Each preset'sname,vcpus,memory_mibandprice_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"
{ "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" } ] }
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-KeystringoptionalDefined in Idempotency. After a2xx, 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
imagestringrequiredThe image to boot, by name: a base image, a custom image (its published build), or a specific build asname@n. For a custom image, the machine'simageisname@n, the build it booted. An image's id is not accepted here.sizestringoptional- Default
- standard
- Allowed
- small | standard | medium | large | xlarge
vcpusintegeroptionalVirtual CPUs.- Default
- the size's
- Allowed
- 1–8
memory_mibintegeroptionalMemory, in MiB.- Default
- the size's
- Allowed
- 1024–16384, ×1024, ≤ 4096 per vCPU
disk_mibintegeroptionalRoot 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[]optionalThe 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
envobjectoptionalEnvironment 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
cwdstringoptionalThe directory the command runs in.- Default
- /root, or the image's WORKDIR
- Allowed
- 1–4096 bytes, no NUL
stdinstringoptionalText written to the command's standard input. The place for secrets: never stored or returned.- Allowed
- ≤ 1 MiB (UTF-8 bytes)
keep_alivebooleanoptionalKeep the machinerunningafter its command exits (command.statusexited), until it is destroyed or expires. A machine without a command is always kept.- Default
- false
ttl_secondsintegeroptionalSeconds 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
- 400
invalid_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.
- 403
account_disabledThe account is disabled, or was disabled while the request ran: nothing was started. - 403
size_not_allowedThe machine exceeds the account's limits. - 404
image_not_foundNo image with this name, or no such build of it. - 409
image_not_publishedThe image has no published build. Publish one, or request name@n. - 409
build_not_readyThe requested build is not ready. - 409
machine_limit_reachedThe account has its maximum number of machines that have not ended, image builds included. - 409
idempotency_key_in_useA request with this Idempotency-Key is still running. - 422
idempotency_key_reusedThis Idempotency-Key was used for a different request. - 502
machine_unavailableThe machine did not start in time. - 503
capacity_exhaustedNo capacity is available; nothing was created. Retry in a few seconds. - 503
service_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 }'
{ "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 }
HTTP/1.1 201 Created content-type: application/ json location: / v1/ machines/ m_01m3kmtt8reagbvwvkf5enc5xs cache-control: no-store x-request-id: req_01m3m85f4ae718vk3954qmbskr
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
limitintegeroptionalHow many machines a page holds.- Default
- 50
- Allowed
- 1–100
starting_afterstringoptionalThe id of the last machine on the previous page. Any of the account's machines, ended or not, keeps its place.
Errors
- 400
invalid_requestlimit is out of range, or starting_after is not a machine of the account. - 503
service_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"
{ "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 }
Retrieve a machine
Returns the current machine object. After the machine ends, returns its final record, permanently (see Retention).
Path
idstringrequiredThe machine's id (m_…). Treat it as an opaque string.
Errors
- 404
machine_not_foundNo machine with this id exists in the account. - 503
service_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"
{ "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 }
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
idstringrequiredThe machine's id (m_…). Treat it as an opaque string.
Body
commandstring[]requiredThe program and its arguments, executed directly without a shell; use["sh", "-c", "…"]for shell syntax. Commands are kept in exec history; pass secrets inenv, not in the command.- Allowed
- 1–4096 entries, 128 KiB, no NUL
envobjectoptionalEnvironment 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
cwdstringoptionalThe directory the command runs in.- Allowed
- 1–4096 bytes
stdinstringoptionalText written to the command's standard input. The place for secrets: never stored or returned.- Allowed
- ≤ 1 MiB (UTF-8 bytes)
timeout_secondsintegeroptionalMaximum run time, counted from when the process has started. When it is exceeded,SIGKILLis sent to the command's process group, the result hastimed_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'sexpires_at. The API waits for the host up totimeout_seconds+ 30 s, then returns500; 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
objectstringexec_resultexec_idstringThe command's id:x-followed by 12 characters. Use it to read the command's output from logs.exit_codeinteger | nullThe command's exit status. When signal N ended it, 128 + N (SIGKILL:137).nullif the command timed out and its end was not confirmed within 5 s.exit_signalstring | nullThe signal that ended the command, such asSIGKILL.nullwhen the command exited by itself, and wheneverexit_codeisnull.timed_outbooleantimeout_seconds was exceeded and SIGKILL was sent. exit_code says whether the end was confirmed.stdoutstringThe first 1 MiB of standard output, as UTF-8 with invalid bytes replaced (U+FFFD). The exact bytes are in the text-format log.stderrstringStandard error, with the same encoding and limit.stdout_truncatedbooleanstdout contains only the first 1 MiB, or output was dropped.stderr_truncatedbooleanThe same, for stderr.duration_msintegerMilliseconds from the host's request to start the command to its exit. Withexit_codenull, to when the host stopped waiting:timeout_secondsplus the 5 s confirmation wait.
Errors
- 400
invalid_requestA field breaks its rule, the program cannot be started (it does not exist), or the request has an Idempotency-Key. - 404
machine_not_foundNo machine with this id exists in the account, or the machine has been torn down; error says which. - 409
machine_conflictThe machine is not running: it is starting, or being torn down before its expires_at. - 409
machine_expiredThe machine is past its expires_at and not yet torn down. - 500
internal_errorNo answer from the host within timeout_seconds + 30 s. The command may have run. - 502
machine_unavailableThe machine did not respond in time. - 502
internal_errorThe host failed to run the command. It may have started. - 503
service_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 }'
{ "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 }
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_linesat its last lines: what is retained first, then, withfollow=true, new output as it arrives. There is no resume position: a reconnect reads again; NDJSONseqtells 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_codeline), or when the machine can no longer write (destroyed, expired or crashed), then without anexit_codeline. In text, the two look alike: read the machine'scommand, or use NDJSON. A transport error after the200aborts 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
idstringrequiredThe machine's id (m_…). Treat it as an opaque string.
Query
execstringoptional- Default
- main
followbooleanoptionalStream until the command exits.- Default
- false
- Allowed
- true | false
tail_linesintegeroptionalStart at the lasttail_lineslines 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\nand every line left is whole. Withfollow=true, new output follows.- Allowed
- 1 – 100000
formatstringoptionaltext: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"}outputAn 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;stdoutorstderr; 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"}droppedEarlier output was dropped to keep the machine withinlimit_bytes(104857600, 100 MiB). Precedes the retained output.{"dropped_bytes"}droppedThe total number of output bytes dropped. At most once, before exit.{"exit_code", "exit_signal"}exitThe final line, after the command exits: the exit status and the signal, as in the machine object. Bothnullif 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
- 400
invalid_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. - 404
machine_not_foundNo machine with this id exists in the account, or its output is past retention; error says which. - 404
log_not_foundNo output exists for this exec. A machine created without a command has no main output. - 409
machine_expiredfollow=true on a machine past its expires_at and not yet torn down. - 503
service_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"
6.1.128 2
{"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}
{"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}
{"seq":0, "line":0, "stream":"stdout", "text":"step 1 of 3\n"}{"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 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
idstringrequiredThe machine's id (m_…). Treat it as an opaque string.
Errors
- 404
machine_not_foundNo machine with this id exists in the account. - 502
internal_errorThe host could not confirm the teardown. The machine remains destroying until it does. Billing stops as in When billing stops. - 503
service_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"
{ "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:
COPYandADDof local files are rejected. UseRUN curlorgit clone, or bring your own image. - linux/amd64. Public registries only.
- Any Linux image boots, from any base,
FROM scratchincluded: nothing needs to be installed. Commands and exec run as argv, with no shell;sh -cand the terminal need a shell (/bin/sh) in the image. An image whose/sbin/initis systemd boots systemd.
A machine booted from a custom image can use any size, with a disk of at least 10 GiB.
FROM debian:13 RUN 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.
idstringThe image's id (img_…). Treat it as an opaque string.objectstringimagenamestringThe image's name: the value of image on machine create. It cannot be changed.published_buildinteger | nullThe build the image's name boots. null until a build is published.latest_buildobject | nullThe newest build's{"number", "status"}, whatever its status: it can differ frompublished_build. null until the first build. The builds themselves are a list of their own.created_atstringWhen the image was created.updated_atstringWhen the image last changed: a build was started, a build ended, or a build was published.
{ "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" }
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
limitintegeroptionalHow many images a page holds.- Default
- 50
- Allowed
- 1–100
starting_afterstringoptionalThe id of the last image on the previous page.
Errors
- 400
invalid_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"
{ "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 }
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-KeystringoptionalDefined in Idempotency. After a2xx, 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
namestringrequired3 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
- 400
invalid_nameThe name does not match the rule. - 409
image_existsThe account already has an image with this name, or a base image uses it. - 409
image_limit_reachedThe account has 10 images, its limit. - 409
idempotency_key_in_useA request with this Idempotency-Key is still running. - 422
idempotency_key_reusedThis Idempotency-Key was used for a different request. - 503
service_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" }'
{ "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" }
{ "error": "An image named \"web-runner\" already exists.", "code": "image_exists", "request_id": "req_01m3m85f4ae718vk3954qmbskr" }
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
imagestringrequiredThe image's id (img_…) or its name.
Errors
- 404
image_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"
{ "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 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
imagestringrequiredThe image's id (img_…) or its name.
Errors
- 404
image_not_foundNo image with this id or name in the account. - 502
internal_errorThe image was not deleted. Running builds may have been stopped. Retry the request. - 503
service_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"
No response body
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
imagestringrequiredThe image's id (img_…) or its name.
Headers
Idempotency-KeystringoptionalDefined in Idempotency. After a2xx, 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
buildintegerrequiredThe build's number.
Errors
- 404
image_not_foundNo image with this id or name in the account. - 404
build_not_foundThe image has no build with this number. - 409
build_not_readyThe build is not ready. - 409
idempotency_key_in_useA request with this Idempotency-Key is still running. - 422
idempotency_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 }'
{ "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" }
{ "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.
objectstringbase_imagenamestringThe image's name: the value of image on machine create.descriptionstringThe distribution and version, e.g. Ubuntu 26.04 LTS.updated_atstring | nullWhen the image was last built. null if the host does not record it.
{ "object": "base_image", "name": "ubuntu-26.04", "description": "Ubuntu 26.04 LTS", "updated_at": "2026-09-24T08:02:11.000Z" }
List base images
Returns the base images, by name from A to Z, as a list of base image objects.
Query
limitintegeroptionalHow many images a page holds.- Default
- 50
- Allowed
- 1–100
starting_afterstringoptionalThe name of the last image on the previous page.
Errors
- 400
invalid_requestlimit is out of range, or starting_after is not a base image. - 503
service_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"
{ "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.
objectstringbuildnumberinteger1 for the image's first build, then 2, 3…imagestringThe image's id.statusstringqueued,building,readyorfailed.errorstring | nullWhy the build failed. null unless failed.size_mibinteger | nullSpace used by the image content, in MiB, once ready.created_atstringWhen the build was requested.started_atstring | nullWhen the build machine started. null if the build failed before.finished_atstring | nullWhen the build became ready or failed.duration_secondsnumber | nullFrom 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.dockerfilestringThe Dockerfile. Only on Retrieve a build, not in a list.
{ "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" }
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
imagestringrequiredThe image's id (img_…) or its name.
Headers
Idempotency-KeystringoptionalDefined in Idempotency. After a2xx, 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
dockerfilestringrequiredThe Dockerfile, as text.- Allowed
- ≤ 64 KiB (UTF-8 bytes)
Errors
- 400
invalid_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.
- 403
account_disabledThe account is disabled, or was disabled while the request ran: nothing was started. - 403
size_not_allowedThe account's limits do not allow a standard machine (2 vCPUs, 2048 MiB of memory) to run the build. - 404
image_not_foundNo image with this id or name in the account. - 409
build_limit_reachedThe account is running 2 builds, its limit. - 409
machine_limit_reachedThe account is running its maximum number of machines; a build uses one. - 409
idempotency_key_in_useA request with this Idempotency-Key is still running. - 422
idempotency_key_reusedThis Idempotency-Key was used for a different request. - 503
capacity_exhaustedNo capacity is available. Retry in a few seconds. - 503
service_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" }'
{ "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 }
{ "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" }
List builds
Returns the image's builds, newest first, as a list of build objects without dockerfile; retrieve a build to read it.
Path
imagestringrequiredThe image's id (img_…) or its name.
Query
limitintegeroptionalHow many builds a page holds.- Default
- 50
- Allowed
- 1–100
starting_afterintegeroptionalThe number of the last build on the previous page.
Errors
- 400
invalid_requestlimit is out of range, or starting_after is not a build of the image. - 404
image_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"
{ "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 }
Retrieve a build
Returns the build object, including dockerfile. Poll until the status is ready or failed.
Path
imagestringrequiredThe image's id (img_…) or its name.numberintegerrequiredThe build's number: 1 for the image's first build, then 2, 3…
Errors
- 404
image_not_foundNo image with this id or name in the account. - 404
build_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"
{ "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" }
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
imagestringrequiredThe image's id (img_…) or its name.numberintegerrequiredThe build's number: 1 for the image's first build, then 2, 3…
Query
followbooleanoptionalStream until the build ends.- Default
- false
- Allowed
- true | false
Errors
- 400
invalid_requestfollow is neither true nor false, or the query has a parameter other than follow. - 404
image_not_foundNo image with this id or name in the account. - 404
build_not_foundThe image has no build with this number. - 503
service_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"
#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.2s nitrous-build: done: 812 MiB used, in 188s
The account object
The account an API key belongs to, and its limits.
idstringThe account's id.objectstringaccountnamestringThe account's name.machinesintegerThe number of the account's machines that have not ended (starting or running), image builds included.limitsobjectmachines: 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 returns409 machine_limit_reachedor403 size_not_allowed.
{ "id": "acc_01m1k32987fdkt4tvzcarrabs7", "object": "account", "name": "Acme", "machines": 2, "limits": { "machines": 10, "vcpus": 8, "memory_mib": 16384, "disk_mib": 163840 } }
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"
{ "id": "acc_01m1k32987fdkt4tvzcarrabs7", "object": "account", "name": "Acme", "machines": 2, "limits": { "machines": 10, "vcpus": 8, "memory_mib": 16384, "disk_mib": 163840 } }
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
monthstringoptionalThe month.- Default
- this month
- Allowed
- YYYY-MM
objectstringusageaccount_idstringThe account.from, tostringThe 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.machinesintegerMachines that ran in the month.machine_secondsnumberThe total running time of those machines, to the millisecond.vcpu_seconds, memory_gib_seconds, disk_gib_secondsnumberRunning time multiplied by each resource, to 3 decimals.machine_hoursnumbermachine_seconds in hours, to 6 decimals.standard_hoursnumberThe machine time in standard-hours: its list price divided by thestandardmachine'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_usdstringThe 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
- 400
invalid_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"
{ "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" }