Tutorials · Oct 30, 2026 · 9 min · GhostConnect

Order proxies from code: a complete walkthrough of the GhostConnect API

Create a key, quote and buy with an Idempotency-Key, handle a short balance or a failed order, then list, renew, top up and terminate, in curl and Python.

The GhostConnect API is the dashboard without the browser: JSON over HTTPS at https://ghostconnect.com/api/v1/, authenticated by an API key and nothing else, paid from your balance. This walkthrough buys a residential plan and two datacenter IPs from code, retries an order without buying it twice, handles the two errors that involve money, then lists, renews, tops up and terminates. Every path, field and error code below is the one the API serves; the full reference is at /docs/api/.

What you need: a key and a balance

Two things, both from the website. The first is an API key. Sign in, open Settings and create one with a label. It is shown once and only a SHA-256 hash of it is kept. An account can hold 5 keys, each revocable on its own, so the key in a script that leaks can be killed without touching the others. Replacing your access key revokes every API key at once, because anyone who saw the old access key could have created one.

The second is a balance. The API never takes a card or a coin. A paying call debits your balance at the moment it runs, and nothing else can pay for it. Add funds on the website with crypto (Monero, Bitcoin, Litecoin, Ethereum or USDT) or by card on Stripe's page; one deposit is between $5.00 and $5,000.00. How to pay for proxies anonymously covers what each method leaves behind. An order that costs more than the balance holds is refused before anything moves, so there is no harm in asking.

Send the key on every request as Authorization: Bearer <key>; X-Api-Key is accepted as well. Cookies are ignored: being signed in on the website does not authenticate an API call. Bodies are JSON with Content-Type: application/json, though form-encoded bodies also work. Every response is a JSON object: {"ok": true, "data": ...} on success, {"ok": false, "error": {"code", "message"}} with a matching HTTP status on failure. Money is a string of dollars, like "14.95", so no cent is lost to floating point, and times are Unix seconds.

The examples below keep the key in an environment variable, GT_KEY, and never write it into a file that gets committed.

Read the catalog and your account

GET /catalog needs no key. It returns the prices, every allowed value the order endpoint validates against, and live datacenter and ISP stock per country. Read it rather than hardcoding: the residential price is products.residential.price_per_gb_usd ($2.99 today), the datacenter terms are products.datacenter.days (7, 14, 31, 62, 93, 186, 365), the rotating thread range is products.rotating.threads (100 to 5,000, step 100), and ISP stock is products.isp.stock, keyed by country code. Stock is the one thing in the catalog that changes between calls.

curl https://ghostconnect.com/api/v1/catalog

GET /account is the first authenticated call to make, because it answers two questions at once: does the key work, and how much can it spend. The response has id, balance_usd, created_at and api_keys, the number of keys on the account.

curl -H "Authorization: Bearer $GT_KEY" https://ghostconnect.com/api/v1/account
import os, time, uuid
import requests

BASE = "https://ghostconnect.com/api/v1"
s = requests.Session()
s.headers["Authorization"] = "Bearer " + os.environ["GT_KEY"]

catalog = requests.get(BASE + "/catalog", timeout=30).json()["data"]
print(catalog["products"]["residential"]["price_per_gb_usd"])

account = s.get(BASE + "/account", timeout=30).json()["data"]
print(account["balance_usd"])

Quote first, then order

POST /quote prices an order, a renewal or a top-up without buying it. It takes kind, one of datacenter, residential, rotating, isp, renew_datacenter, renew_rotating, renew_isp or topup_residential, plus the same fields the matching purchase takes; renewals and top-ups also take the proxy id. The answer carries amount_usd, the lines the checkout page would show, your balance_usd and balance_covers, a boolean. Five gigabytes of residential bandwidth quote at $14.95: the price is $2.99 per GB and nothing is added to it.

curl -X POST -H "Authorization: Bearer $GT_KEY" -H "Content-Type: application/json" \
  -d '{"kind":"residential","gb":5}' \
  https://ghostconnect.com/api/v1/quote
{
  "ok": true,
  "data": {
    "kind": "residential",
    "description": "Residential, 5 GB",
    "amount_usd": "14.95",
    "lines": { "Bandwidth": "5 GB", "Expires": "Never", "Price per GB": "$2.99" },
    "balance_usd": "149.50",
    "balance_covers": true
  }
}

Quoting is free, is not rate-limited as a paying call, and is the right place to catch a bad field: an unknown country or a duration that is not on the list comes back as 400 invalid_order with a message that names it, before any key or money is involved.

POST /orders buys. The body is type plus the product's fields. Residential takes gb (1 to 1,000). Datacenter takes country (us, ca, uk, fr, nl, de), an optional ip_type (ipv4 or ipv6, default ipv4), days (7, 14, 31, 62, 93, 186, 365) and an optional quantity (1 to 100, limited by stock). Rotating takes country, threads and days (7, 14, 30, 60, 90). ISP takes country (us, uk, de, nl, sg, es, fr), months (1 to 24), an optional quantity and an optional use_case, whose only value is aio.

A successful order returns 201 with the invoice, the new proxies with their credentials, and balance_usd after the debit. A residential proxy comes back with host, port_http, port_socks5, username, password, a bandwidth object (total_gb, used_gb, remaining_gb), allowlist, an expires_at of null and synced_at. A datacenter proxy has ip, port_http, port_socks5, credentials, expires_at and an auto_renew object. Buying the 5 GB above leaves 134.55 on the example balance; two Dutch datacenter IPs for 31 days then cost 7.98 and leave 126.57.

curl -X POST -H "Authorization: Bearer $GT_KEY" \
  -H "Idempotency-Key: order-resi-2026-10-30-0001" \
  -H "Content-Type: application/json" \
  -d '{"type":"residential","gb":5}' \
  https://ghostconnect.com/api/v1/orders
quote = s.post(BASE + "/quote", json={"kind": "residential", "gb": 5}, timeout=30).json()["data"]
print(quote["amount_usd"], quote["balance_covers"])

body = {"type": "residential", "gb": 5}
key = "order-" + uuid.uuid4().hex        # one key per purchase; keep it until you have an answer
r = s.post(BASE + "/orders", json=body, headers={"Idempotency-Key": key}, timeout=60)
if r.status_code == 201:
    data = r.json()["data"]
    proxy = data["proxies"][0]
    print(proxy["id"], proxy["host"], proxy["port_http"], proxy["username"], proxy["password"])
    print("balance now", data["balance_usd"])
else:
    err = r.json()["error"]
    print(r.status_code, err["code"], err["message"])

Why the Idempotency-Key matters

A network can fail after an order went through. Your client sends the request, the server debits the balance and sets up the proxy, and the response is lost on the way back. All your client sees is a timeout. The natural reaction, trying again, buys a second plan. The Idempotency-Key header is what makes the second attempt harmless.

Send it on orders, renew and topup: any unique string of 8 to 64 letters, digits, dashes, underscores, colons or dots, one per purchase. The first response to a key is kept for 24 hours. Repeat the same request with the same key and you get that stored response back, with an Idempotent-Replayed: true header, and nothing is charged. The same key with a different request is refused with 422 idempotency_key_reused. A retry that lands while the first attempt is still running gets 409 idempotency_in_progress: wait and send it again. If the request was refused before any money moved, for bad input or a short balance, the key is released, so the corrected request can reuse it.

"The same request" means the same method, the same path and the same body fields with the same values. Generate the key before the first attempt and keep it for every retry of that purchase; a fresh key on each retry is the same as no key at all.

def buy(body, key, attempts=5):
    for attempt in range(attempts):
        try:
            r = s.post(BASE + "/orders", json=body, headers={"Idempotency-Key": key}, timeout=60)
        except requests.exceptions.RequestException:
            time.sleep(2 ** attempt)         # the order may have gone through: same key, try again
            continue
        if r.status_code == 409:             # idempotency_in_progress
            time.sleep(2 ** attempt)
            continue
        if r.status_code == 429:             # rate_limited
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        if r.headers.get("Idempotent-Replayed") == "true":
            print("replayed the first answer; nothing charged again")
        return r
    raise RuntimeError("no answer after retries: read GET /invoices before ordering with a new key")

r = buy({"type": "datacenter", "country": "nl", "days": 31, "quantity": 2}, "order-" + uuid.uuid4().hex)

Two things in that function are deliberate. The key is passed in rather than generated inside, so the caller owns it across attempts and across a restart of the script. And the last resort when every attempt fails is to read GET /invoices, not to order again: the newest invoice says whether the money moved. The record behind all this, the key and the response it produced, is the only API-specific thing the site stores, and the privacy page lists it with its 24-hour life.

The two errors that involve money

Most errors are plain. 400 invalid_order names the field that is wrong. 401 invalid_key means the key does not exist or was revoked. 404 not_found is no such endpoint, or no such proxy or invoice on this account. 429 rate_limited comes with a Retry-After header in seconds. Two errors need more care, because they sit on either side of the debit.

402 insufficient_balance is the near side. The order was priced, the balance did not cover it, nothing was charged and no invoice exists. The error object carries amount_usd and balance_usd, so the script can say exactly how much to add. If the request carried an Idempotency-Key, the key is released. Add funds on the website and send the same request again, same key and all.

{
  "ok": false,
  "error": {
    "code": "insufficient_balance",
    "message": "This costs $14.95 and the balance is $8.97. Add funds on the website.",
    "amount_usd": "14.95",
    "balance_usd": "$8.97"
  }
}

502 fulfilment_failed is the far side. The balance was debited, an invoice was written, and then the proxy could not be set up. By the time you read the response the full amount is already back on the balance; the error object includes the invoice, with a status of credited, and the new balance_usd. Nothing was provisioned. This response is stored under the Idempotency-Key, so repeating the request with the same key replays the failure rather than ordering again. To try afresh, use a new key. A 500 internal_error after a paying request is the one case where you do not know what happened: read GET /invoices before retrying without a key.

if r.status_code == 402:
    e = r.json()["error"]                # insufficient_balance: nothing was charged
    print("need", e["amount_usd"], "have", e["balance_usd"])
elif r.status_code == 502 and r.json()["error"]["code"] == "fulfilment_failed":
    e = r.json()["error"]                # paid, could not be set up, already credited back
    print("invoice", e["invoice"]["id"], e["invoice"]["status"], "balance", e["balance_usd"])
elif r.status_code == 201:
    for p in r.json()["data"]["proxies"]:
        print(p["id"], p["ip"], p["port_http"], p["port_socks5"], p["username"], p["password"])

List, renew and top up

GET /proxies lists every proxy on the account, oldest first, in the same shape orders returns; filter with ?type=. GET /proxies/{type}/{id} returns one as {"proxy": ...}. Ids are the 8-character lowercase ids in these responses, the same ones the dashboard shows. For ISP, the id of any IP in an order returns the whole order, with its ips array.

Renewing adds time: POST /proxies/{type}/{id}/renew with days for datacenter (7, 14, 31, 62, 93, 186, 365) or rotating (7, 14, 30, 60, 90), or months (1 to 24) for ISP, where one renewal covers every IP in the order. A 31-day datacenter renewal costs $3.99; renewing two ISP IPs for a month costs $10.00. Residential bandwidth never expires, so renew on it answers 400 not_renewable and points you to POST /proxies/residential/{id}/topup with gb; ten more gigabytes cost $29.90, and the plan keeps its credentials. Both calls pay from the balance, return the invoice and the updated proxy, and take an Idempotency-Key. A renewal is the clearest case for one: a retry does not undo it, it repeats it.

Paying calls have their own limit, 30 a minute per key within the 120 a minute for everything, so a script that renews a fleet should pace itself and honour Retry-After. If you would rather not script renewals at all, POST /proxies/{type}/{id}/auto-renew with enabled true and a period pays from the balance about a day before expiry.

proxies = s.get(BASE + "/proxies", params={"type": "datacenter"}, timeout=30).json()["data"]["proxies"]
for p in proxies:
    print(p["id"], p["ip"], p["expires_at"], p["auto_renew"]["enabled"])

dc_id = proxies[0]["id"]
r = s.post(BASE + "/proxies/datacenter/" + dc_id + "/renew", json={"days": 31},
           headers={"Idempotency-Key": "renew-" + dc_id + "-2026-11"}, timeout=60)
print(r.status_code, r.json()["data"]["proxy"]["expires_at"])

r = s.post(BASE + "/proxies/residential/obcl4126/topup", json={"gb": 10},
           headers={"Idempotency-Key": "topup-obcl4126-" + uuid.uuid4().hex}, timeout=60)
print(r.json()["data"]["proxy"]["bandwidth"]["remaining_gb"])
curl -X POST -H "Authorization: Bearer $GT_KEY" -H "Idempotency-Key: renew-k2m9x4qa-2026-11" \
  -H "Content-Type: application/json" -d '{"days":31}' \
  https://ghostconnect.com/api/v1/proxies/datacenter/k2m9x4qa/renew

Terminate, and what a retry may do

POST /proxies/{type}/{id}/terminate deletes a datacenter, residential or rotating proxy for good; DELETE /proxies/{type}/{id} does the same. It stops working at once and unused time or bandwidth is not refunded, so a script should terminate only what it is certain it has finished with. ISP proxies cannot be ended early and answer 400 not_supported; they run to their expiry date. If the deletion cannot be confirmed the proxy is kept and you get 502 terminate_failed, so retrying a terminate is always safe, and no Idempotency-Key is needed because nothing is charged.

r = s.post(BASE + "/proxies/datacenter/" + dc_id + "/terminate", timeout=60)
if r.status_code == 200:
    print(r.json()["data"]["terminated"])
elif r.status_code == 502:
    print("kept; try again shortly")      # terminate_failed: nothing was removed

Put together, a safe purchasing loop is short: read the catalog for the allowed values, quote to learn the exact amount and whether the balance covers it, order with one Idempotency-Key per purchase and keep it through every retry, treat 402 as a request to add funds and 502 fulfilment_failed as money already returned, and read GET /invoices whenever you are unsure. The rest of the API, allowlists, credential resets, connection strings and the usage series, follows the same shape and is in the reference. None of it asks who you are: a key, a balance, and the residential, datacenter, ISP or rotating IPv6 product you need.

Related

An account is a key. No name, no email, no verification: GhostConnect hands you a 32-character key and that is the whole sign-up. Pay in crypto, by card, or with cash in an envelope.

Get a key All posts