Buying Plans and Top-Ups with the Node4 API

Pick a plan with GET /plans, buy it by saved card or Bitcoin with POST /orders, add bandwidth with POST /topups, and poll the order until delivery.

A script buys in three calls: GET /plans to find the plan's id, POST /orders with that id, a paymentMethod and an Idempotency-Key header, then GET /orders/{id} until its provisioning status reads delivered. Card orders charge your default saved card and usually come back delivered in the same response; Bitcoin orders return a checkout URL to pay. Bandwidth top-ups and extra IP slots go through POST /topups and are paid with Bitcoin. The reference lists every field; this page is the order of operations and what to do at each answer.

Before you buy

Payment Methods card in Account Settings, Profile & Billing tab: two saved cards, the Visa marked Default.

Examples below assume NODE4_API_KEY is set in the environment.

1. Find the plan

GET /plans returns the plans currently on sale, the same set the pricing page shows:

curl -s -H "Authorization: Bearer $NODE4_API_KEY" https://node4.io/api/v1/plans
{
  "success": true,
  "data": {
    "plans": [
      {
        "id": 12,
        "slug": "dc-10",
        "name": "Premium Datacenter 10",
        "productFamily": "premium_datacenter",
        "priceCents": "…",
        "currency": "usd",
        "billingCycle": "monthly",
        "proxyCount": 10,
        "bandwidthLimitMb": 0,
        "countrySelectable": true,
        "countries": [{ "code": "US", "label": "United States" }]
      }
    ]
  }
}

(Values are illustrative and the price is elided here; priceCents is a whole number of US cents in the live response.) Select a plan by slug or productFamily in code and read its id from the response each time, rather than hardcoding an id you saw once. Plans that are not publicly listed are never returned and cannot be bought through the API, even by an account they were made for; those are bought from the dashboard link support sends.

Where countrySelectable is true (dedicated datacenter), you may pass country with one of the listed codes when you order. Leave it out for any country.

2. Place the order

Generate one Idempotency-Key per purchase, keep it, and send it with the order:

KEY=$(uuidgen)
curl -s -X POST https://node4.io/api/v1/orders \
  -H "Authorization: Bearer $NODE4_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"planId": 12, "paymentMethod": "card", "country": "US"}'
import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // store this with your own order record
const res = await fetch("https://node4.io/api/v1/orders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NODE4_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify({ planId: 12, paymentMethod: "card" }),
});
const body = await res.json();
console.log(res.status, body.code ?? "", body.data ?? body.error);
import os
import uuid
import requests

idempotency_key = str(uuid.uuid4())  # store this with your own order record
res = requests.post(
    "https://node4.io/api/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['NODE4_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={"planId": 12, "paymentMethod": "bitcoin"},
    timeout=60,
)
print(res.status_code, res.json())

The body accepts planId, paymentMethod (card or bitcoin), and optionally discountCode and country. Any other field is refused with 400 invalid_request.

Card or Bitcoin: what each one does

cardbitcoin
What is chargedYour default saved cardA Bitcoin invoice you pay
PriceFull price, less any valid discount codeFull price, less any discount code, then less account credit
Account creditNever spentApplied automatically; if it covers the whole order, no invoice is created
Fraud and risk checksThe same checks as the dashboard checkoutThe same checks as the dashboard checkout
Typical response201, order already delivered201 with a checkoutUrl, order awaiting_payment
Available for top-ups and IP slotsNoYes

A successful card purchase:

{
  "success": true,
  "data": {
    "paymentMethod": "card",
    "order": {
      "id": 9301,
      "type": "subscription",
      "status": "completed",
      "paymentMethod": "card",
      "planId": 12,
      "subscriptionId": 4410,
      "creditAppliedCents": 0,
      "provisioning": { "status": "delivered", "message": null },
      "delivered": { "subscriptionId": 4410, "activeProxies": 10 }
    }
  }
}

A Bitcoin purchase returns the invoice to pay:

{
  "success": true,
  "data": {
    "paymentMethod": "bitcoin",
    "checkoutUrl": "https://btcpay.node4.io/i/AbC123xYz",
    "paidWithStoreCredit": false,
    "order": { "id": 9302, "status": "pending", "provisioning": { "status": "awaiting_payment", "message": null } }
  }
}

When account credit covers the full amount, paidWithStoreCredit is true, checkoutUrl is null, and the order completes without a payment step. Card renewals and card purchases never draw on credit, so credit is only useful to an integration that pays by Bitcoin.

Card refusals arrive as 402, and in each of them nothing was captured: no_saved_card, card_expired, card_declined (the bank or a risk rule said no; the error text is safe to show), and payment_held (the payment is authorized and under review; do not try again, the order resolves on its own and you are emailed).

3. Buy a bandwidth top-up or IP slots

GET /addons tells you what this account can add right now:

curl -s -H "Authorization: Bearer $NODE4_API_KEY" https://node4.io/api/v1/addons

The response has two parts. bandwidth says whether top-ups are eligible (only metered plans can buy them; reason explains a refusal), lists ready-made packs such as topup_50gb, and gives bounds (minGb, maxGb) for a custom size written topup_<gb>gb. ipSlots lists ip_1, ip_5 and ip_10 where they still fit under the account's maximum, and needs an active paid plan.

Then buy with POST /topups, again with a fresh Idempotency-Key:

res = requests.post(
    "https://node4.io/api/v1/topups",
    headers={
        "Authorization": f"Bearer {os.environ['NODE4_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"addonId": "topup_75gb", "paymentMethod": "bitcoin"},
    timeout=60,
)
print(res.status_code, res.json())

Top-ups are Bitcoin only. "paymentMethod": "card" returns 422 payment_method_unsupported and charges nothing; card top-ups happen only through auto top-up, which you configure in the dashboard (see bandwidth top-ups). Discount codes apply to bandwidth packs but not to IP slots. An add-on outside the bounds or not offered on your plan is 422 addon_unavailable, and a plan that cannot take top-ups at all is 422 topup_not_eligible.

4. Poll the order until it is delivered

GET /orders/{id} (scope read:orders) reports payment and delivery in provisioning.status:

provisioning.statusMeaningWhat to do
awaiting_paymentBitcoin invoice not paid yetPay the checkoutUrl, keep polling
payment_unconfirmedA card payment's outcome is still being confirmedDo not pay again; wait for the update and our email
under_reviewA card payment is held for review, authorized but not capturedDo not pay again; wait for the decision
in_progressPaid; setup needs a manual step on our sideNothing; it will finish, and you do not pay again
deliveredPaid and provisionedRead delivered for the subscription, proxy count, GB or slots
not_deliveredThe payment did not complete; nothing was deliveredStart a new purchase with a new key if you still want it
revokedThe order was refundedNothing
async function waitForDelivery(orderId) {
  for (;;) {
    const res = await fetch(`https://node4.io/api/v1/orders/${orderId}`, {
      headers: { Authorization: `Bearer ${process.env.NODE4_API_KEY}` },
    });
    const { data } = await res.json();
    const state = data.order.provisioning.status;
    if (["delivered", "not_delivered", "revoked"].includes(state)) return data.order;
    await new Promise((r) => setTimeout(r, 30_000)); // stay well inside the rate limit
  }
}

Polling every 30 seconds uses a small fraction of the 60 requests per minute each key gets. A webhook avoids polling altogether: order.completed fires when a paid order settles, as described in the webhooks section of the reference. An order belonging to another account answers 404, the same as an order that does not exist.

The dashboard's Orders page lists the same orders. A Bitcoin order waiting on its invoice reads Pending; pay from the invoice page you opened. A day later the row offers Complete payment, which starts a new checkout. A delivered order reads Completed with its PDF invoice.

Orders list on the Orders page: a pending Bitcoin order with Complete payment, above completed card orders.

Once delivered, GET /proxies returns the new static proxies; getting started shows the call.

Retrying a purchase safely

Purchases are the one place where a blind retry can cost money, which is why POST /orders and POST /topups refuse to run without an Idempotency-Key. The rules:

Three answers mean stop and check GET /orders, never retry with a new key, because the earlier purchase may have gone through:

The complete table of codes and the reasoning behind each is in API errors, rate limits and retries and the idempotency section of the reference.

What the API does not buy or change