Five conditions, and nothing else. Each one is checkable in a single call, and each has an error code that names it — you will never have to guess which one is missing.
1. An account, with a verified e-mail
The API acts as you: it wants the same account as the console, and the same
verified address. Until it is verified, every authenticated call answers
403 email_not_verified.
A suspended account answers 403 account_suspended. Only support
can reopen that door — no key will.
2. Credit, worth 24 hours
Hourly rental is paid in advance. An order is accepted only if the balance
covers the machine's first 24 hours: 0.44 CAD for a Nano, 0.80 CAD for a
Starter. Below that, 402 insufficient_credit, with the missing
amount to the cent.
Credit is topped up in the console, not through the API: committing a spend is one thing, charging a payment method is another. Automatic top-up is set once in the console and then works without you.
3. A key, with the right scopes
Created under Account → API keys, shown once. Tick only
what the tool needs: a monitoring key has no business holding
vms.destroy. A call outside a key's scopes answers
403 insufficient_scope and names the missing one.
4. Room in the quota
An account holds five machines by default. The ones that count are those
holding resources — pending, active, suspended — not the deleted ones. Past
that, 409 quota_reached. The cap is raised on request, by
ticket; it is not a commercial limit, it is a guard against a script running
away.
GET /v1/account returns the quota at any time, along with the
balance and the runway. A well-behaved script reads it before ordering.
5. A budget you have not blown
If you set a monthly budget in the console, passing 120 % of it stops your
machines and refuses new orders (402 budget_cap_reached). With
no budget set, nothing cuts.
Checking all five at once
dry_run answers the five questions in one request, creating
nothing and charging nothing:
curl -s https://api.ffxf.net/v1/vms \
-H "Authorization: Bearer $FFXF_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"plan":"nano","region":"montreal","image":"debian-13",
"hostname":"probe","billing":"hourly","dry_run":true}' | jq .data.checks
Every check is there with its verdict: quota,
stock, billing_mode, credit,
budget. A single fail and the order would be
refused, with that same code.