Blog / Tutorials

Deploying a Server From the API: Three Calls With curl

By the NoctHost TeamNovember 2, 2026Updated November 9, 20266 min read

Deploying a server from our API takes 3 calls: POST /servers returns 202 straight away, you poll GET /servers/{id} every 5 seconds until the status is ACTIVE, usually about a minute, and DELETE /servers/{id} stops billing in the same hour. On the $10/month entry plan ($0.013699 an hour) a deploy, ten minutes of testing and a destroy costs well under a cent.

Most hosting APIs are an afterthought bolted to a control panel. If you are automating anything, ephemeral build machines, a fleet that scales with a campaign, a bot that provisions its own compute, the API is the product and the panel is the afterthought.

Here is the entire flow in curl. The interesting parts are the three things at the end that are easy to get wrong.

Get a token

Create a personal API key in the dashboard under Profile. It starts with nh_, does not expire, and is revocable. Send it as a bearer token:

export NH=nh_your_token_here
export API=https://api.nocthost.com

You can also authenticate with a login-issued JWT, but those expire after fifteen minutes, which makes them a poor fit for anything unattended. Use the key.

Check the balance first

curl -s -H "Authorization: Bearer $NH" $API/billing
{"balance":"24.40","hourlySpend":"0.0274"}

Money values come back as strings. Parse them as decimals, never as floats. As the Python documentation puts it, "most decimal fractions cannot be represented exactly as binary fractions", so 0.1 + 0.1 + 0.1 == 0.3 is false, and a balance check that is off by a fraction of a cent will eventually refuse a deploy it should have allowed. hourlySpend is the hourly price of your running servers.

A deploy needs the balance to cover one hour of everything you would be running afterwards: your existing servers plus the new one. Below that you get a 402.

Deploy

curl -s -X POST $API/servers \
  -H "Authorization: Bearer $NH" \
  -H "Content-Type: application/json" \
  -d '{
        "label": "build-runner-1",
        "planId": "vc2-1c-2gb",
        "regionId": "fra",
        "providerId": "hetzner",
        "osId": 2136,
        "password": "a-long-root-password",
        "sshKey": "ssh-ed25519 AAAA... you@host"
      }'

You get a 202 and a server object with status: "PROVISIONING" and a null ipv4. That is the point of the whole design: the call returns immediately and the machine appears later. Nothing here blocks while the machine boots.

osId is optional and defaults to Ubuntu 22.04; the full list is in the API docs. providerId is optional and defaults to Vultr. sshKey takes one public key line and installs it for root at first boot. The password stays required regardless, because the provider console and account recovery both run on it.

Poll until it is up

until curl -s -H "Authorization: Bearer $NH" $API/servers/$ID \
      | grep -q '"status":"ACTIVE"'; do sleep 5; done

Every five seconds is a sensible interval. It is normally ACTIVE in about a minute, a little longer on some clouds; the terminal states are ACTIVE and ERROR, and a server that reaches ERROR is not billed further.

Destroy it when you are done

curl -s -X DELETE -H "Authorization: Bearer $NH" $API/servers/$ID

Billing stops the same hour. For anything ephemeral this call is the one that decides your bill, so put it in a trap or a defer rather than at the end of the happy path.

Ten at once

curl -s -X POST $API/servers/bulk \
  -H "Authorization: Bearer $NH" -H "Content-Type: application/json" \
  -d '{"label":"fleet","count":10,"planId":"vc2-1c-1gb","regionId":"fra","password":"..."}'

Up to 10 per call, labels get -1 through -N suffixes, one password across the set. The balance has to cover an hour of your existing fleet plus all of the new servers, or the whole call returns 402. Useful for scrape fleets and parallel test matrices.

The three things that bite people

Not every provider takes every combination. The clouds behind the API share one catalog, and each covers a subset of the regions, plans and images. Asking Hetzner for a city it does not serve returns a 400 with a readable message. Call GET /servers/providers before you pick. It reports which backends can take a deploy right now and which of their regions are temporarily out of capacity, which is worth checking in an automated flow where nobody is watching.

Rate limits are real. 300 requests a minute when authenticated, 100 a minute anonymous, 10 a minute on the login routes. A polling loop across a fleet of ten at one request every 5 seconds is already 120 a minute. On a 429, which RFC 6585 defines as too many requests in a given amount of time, back off exponentially, and honour a Retry-After header if one is present, rather than retrying immediately.

The password is never stored. It is written into the machine at first boot and we keep no copy. If your automation generates one, it has to save it somewhere before the call, because there is no endpoint that will tell you what it was. You can set a new one later with POST /servers/{id}/root-password, but an SSH key avoids the problem entirely, which is the main argument for passing one.

Tip: If you are writing an agent rather than a script, /llms.txt is the same documentation in a form a model can consume in one request, plans, regions, images, endpoints and the error codes.

What it costs to experiment

Hourly billing from a prepaid balance you top up with USDT, other crypto or a card. Deploying a server, poking at it for ten minutes and destroying it costs a fraction of a cent, which makes the API worth learning against a real account rather than a sandbox. Entry plans are $10 a month, $0.013699 an hour; every plan and price is on the pricing page.

Spin one up in about a minute

Email signup, pay with crypto or card, hourly billing. Trying a box costs cents: destroy it when you are done.

Deploy a server

Frequently asked

Do API keys expire?
No. They are revocable from the dashboard but do not time out. Login-issued JWTs expire after fifteen minutes and are the wrong tool for automation.
Can I set the root password later?
Yes. POST /servers/{id}/root-password with {"password": "..."} sets a new one, and on most servers nothing is erased. What you cannot do is read the old one back, because it is never stored.
How do I know which provider to ask for?
GET /servers/providers returns what each backend can take right now, including regions temporarily out of capacity. Omit providerId entirely and you get Vultr.
Is there a sandbox environment?
No. Hourly billing makes the real API cheap enough to develop against. A test deploy and destroy costs well under a cent.
What happens if my balance runs out mid-run?
Servers are archived rather than deleted: the disk is kept in a snapshot for seven days, and topping up in that time restores them automatically with a new IP. GPU and dedicated servers are removed right away instead. Deploy calls start returning 402 before that point.

Keep reading