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.