Introduction
The Customer API is the same platform the dashboard drives, exposed for your own code. Every account has it, you do not need to be a reseller, and there is nothing to apply for.
You can:
- Read your balance and confirm a key works
- List the plan catalogue with live prices
- Place orders, paid from your account balance
- List your active plans and their remaining bandwidth or time
- Renew or top up a plan, read its daily usage, and rotate, suspend or lock down its credentials
- Generate ready-to-use proxy lines with country targeting
- Read your invoice history
Authentication
Every request carries your API key in a header. There is no OAuth flow, no token exchange and no expiry to handle.
X-API-Key: VP_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCreate one in the dashboard under Account → API Keys. The key is displayed once, at creation, we store only a SHA-256 hash, so it can never be shown again. Lose it and you delete the key and make another.
A key authenticates as you and carries no additional privilege. It cannot change your password, and it cannot create other API keys, that deliberately requires a dashboard session, so one leaked key cannot quietly multiply itself.
Managing keys
Key management is done from the dashboard, or from these endpoints using a normal logged-in session (not an API key):
| Endpoint | Does |
|---|---|
| GET /api/user/keys | List your keys (prefixes only, never the secret) |
| POST /api/user/keys/create | Mint a key, the only response that contains the secret |
| POST /api/user/keys/update | Rename, or disable/re-enable |
| POST /api/user/keys/delete | Permanently revoke |
You may hold up to 10 active keys. Name them after where they run (scraper-prod, staging) so you know which one to revoke without guessing. Disabling or deleting takes effect immediately on the next request.
Base URL
https://vaultproxies.net/api/customerAll endpoints below live under /api/customer/* and are authenticated with X-API-Key. Requests and responses are JSON; send Content-Type: application/json on anything with a body.
Money is always integer cents, never floats. A $2.50 price is 250.
Pricing
The API charges exactly what the website charges. There is no API surcharge and no API discount.
Applied in this order:
- List price for the plan, from the catalogue
- Your loyalty tier discount, applied automatically from lifetime spend
- A coupon, if you pass one
What you actually paid is always invoice.amount_cents in the order response, read that rather than recomputing it yourself.
Check your balance
/api/customer/meThe cheapest call in the API, and the right health check: it proves the key works and tells you what you have to spend.
curl -s https://vaultproxies.net/api/customer/me \
-H "X-API-Key: $KEY"{
"id": 42,
"email": "[email protected]",
"username": "acme",
"balance_cents": 5000,
"balance_locked": false,
"wholesale_pricing": false
}| Field | Meaning |
|---|---|
| balance_cents | What orders draw from |
| balance_locked | True = orders refused pending a support review; the balance shows but cannot be spent |
| wholesale_pricing | True only for approved resellers |
If an order is refused while you believe there is money on the account, check balance_locked first. A locked balance is refused with 403 and a message asking you to contact support, not with insufficient balance.
List plans
/api/customer/plansThe live catalogue with current retail pricing. Read it rather than hardcoding prices, they change.
{
"categories": [
{
"id": 1,
"key": "resi_pergb",
"display_name": "Residential /GB",
"pricing_type": "pergb",
"unit": "GB",
"price_per_unit_cents": 100,
"min_units": 1,
"max_units": 1000
}
]
}| Field | Meaning |
|---|---|
| key | What you pass as category_key when ordering |
| pricing_type | pergb = billed per GB · unlim = billed per unit of time · perip = billed per dedicated IP for a fixed term |
| unit | GB, HOUR, DAY or IP, tells you what units means |
| min_units / max_units | Accepted range for a single order |
Create an order
/api/customer/ordercurl -s -X POST https://vaultproxies.net/api/customer/order \
-H "X-API-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{"category_key":"resi_pergb","units":10}'{
"service": {
"id": 5567,
"category_id": 1,
"purchased_units": 10,
"status": "active",
"remaining_gb": 10,
"username": "vp_a1b2c3",
"password": "s3cr3t"
},
"invoice": { "id": 8822, "amount_cents": 1000, "status": "paid" },
"coupon_applied": "SAVE10",
"discount_cents": 100,
"original_cents": 1100
}The service object carries no price. What you paid is invoice.amount_cents in the same response, as Pricing above says.
Residential Unlimited is cheaper by the hour the longer you buy: an order (or an extension, by the hours it adds) long enough to reach a bulk rung is discounted on the whole order, before any loyalty discount or coupon. The response then also carries duration_discount_percent, duration_discount_cents and list_cents. GET /api/customer/unlimited/speeds lists the rungs as duration_discounts (min_hours, percent_off), and each pool says whether they apply to you in duration_discount.
To top up or renew an existing plan instead of creating a second one, use Renew or top up. It keeps the plan's credentials, and its IPs on dedicated ISP.
List your plans
/api/customer/services{
"services": [
{
"id": 5567,
"category_id": 1,
"status": "active",
"purchased_units": 10,
"remaining_gb": 7.4,
"end_at": null,
"username": "vp_a1b2c3",
"password": "s3cr3t"
}
]
}| Field | Meaning |
|---|---|
| remaining_gb | Set on per-GB plans, bandwidth left |
| end_at | Set on time-based plans, when it expires |
| status | active, expired, suspended, recycled, restoring or forfeited |
| username / password | The plan's proxy credentials |
Only active passes traffic; every other status is the reason a line stopped working. suspended is reversible and leaves the plan intact, but the clock keeps running: a time-based plan carries on expiring while it is suspended. recycled means the credentials were reclaimed while the plan sat unused, with the unspent units still on it: the response adds restorable, restorable_units and a plain-English status_message, and the re-issue is free from the dashboard. restoring is that re-issue in flight, and forfeited is a reclaimed plan that can no longer be restored.
Manage a plan
Everything a customer needs after the sale: renewing, topping up, usage, credentials and access. Every call below takes the service_id from List your plans, and acts only on plans that belong to your key.
Renew or top up
/api/customer/services/extendcurl -s -X POST https://vaultproxies.net/api/customer/services/extend \
-H "X-API-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{"service_id":5567,"units":10}'| Plan type | What units means | Result |
|---|---|---|
| Per-GB plans | GB to add | Added to remaining_gb on the same credentials |
| Unlimited (time) plans | Hours, or pass time_unit day / week / month | Added to end_at; an expired plan comes back with the same credentials |
| Dedicated ISP | Ignored: every IP on the plan is renewed | Same IPs; term_days, a multiple of 30 up to 360 (default 30), added to end_at |
It is charged from your balance at the same price as a new order of that size, and answers like Create an order. A dedicated ISP plan can be renewed while it is active; once it has ended its IPs are released and it needs a new order.
/api/customer/services/extendable?service_id=5567{
"service_id": 5567,
"extendable": true,
"reason": "",
"category_key": "resi_pergb",
"unit": "GB",
"min_units": 1,
"max_units": 10000,
"price_per_unit_cents": 100
}Ask this before showing a renew button: when extendable is false, reason says why in plain English.
Usage per day
/api/customer/services/usage?service_id=5567&days=30{
"from": "2026-09-03",
"to": "2026-10-02",
"days": 30,
"total_gb": 41.2,
"history_starts": "2026-08-14",
"points": [ { "date": "2026-10-01", "gb": 1.8 }, { "date": "2026-10-02", "gb": 0.6 } ]
}Days are UTC and a day without traffic is a zero, not a gap. Use from and to (YYYY-MM-DD) instead of days for a fixed range. Leave out service_id to get the whole account, with each plan's total for the range.
Credentials and access
All of these are POST with a JSON body containing service_id.
| Endpoint | Body | What it does |
|---|---|---|
| /api/customer/services/password | password (optional) | Rotates the plan's password; leave it out to have a strong one generated. Returns the new username and password. The old ones stop working. |
| /api/customer/services/suspend | none | Stops the plan authenticating, for example when an end customer stops paying. Nothing is refunded and a time-based plan keeps its clock running. |
| /api/customer/services/resume | none | Undoes a suspend. |
| /api/customer/services/allowed-ips | ips (array, up to 10) | Replaces the list of IPs allowed to use the plan; an empty array goes back to username and password only. Not available on plans served through our gateway, where the credentials are the access control. |
| /api/customer/services/update | label | Your own name for the plan, for example your customer's order number. Send an empty string to clear it. |
| /api/customer/services/delete | confirm: true | Deletes the plan and forfeits whatever is left on it. Irreversible, which is why it needs confirm. |
Generate proxy lines
/api/customer/proxies/generateTurns a plan into ready-to-paste proxy lines with the targeting you ask for.
curl -s -X POST https://vaultproxies.net/api/customer/proxies/generate \
-H "X-API-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{
"service_id": 5567,
"plan_key": "resi_pergb",
"country": "us",
"protocol": "HTTP",
"format": "ip:port:user:pass"
}'| Field | Notes |
|---|---|
| service_id | Required. From /api/customer/services |
| plan_key | Required. The plan's category key |
| country | ISO-2, lowercase. Mutually exclusive with region |
| region | Continent code (EU, NA, SA, AS, AF, OC) or a grouping like nordics, dach, apac, latam |
| state, city | Finer targeting, where the plan supports it |
| protocol | HTTP (default) or SOCKS5 |
| format | Default ip:port:user:pass |
| gate | Optional. auto (default) or an id from the gates array of GET /api/plans/catalog: that array is the live list, and an id not in it is refused. Moves the lines to that gate; ports and credentials stay the same, and so does the exit IP. auto is the plan's recommended gate, closest to the plan's infrastructure, and is fastest for almost everyone. Refused on plans without a gate choice (gate_selectable in the catalog) |
| ips | Static IP list, IP-targeted shared_isp plans only |
Targeting tokens
Proxy usernames carry their targeting inline, as -key-value pairs appended to the base username. The generator builds these for you; this table is for hand-built credentials.
ca3ddbbd85591e7f-geo-us-st-california-sess-drfse9co-life-21600| Token | Selects | Example |
|---|---|---|
| geo- | Country, ISO-3166 alpha-2 | geo-us |
| st- | State or province | st-california |
| reg- | A group of COUNTRIES, not a state | reg-nordics |
| pc- | Postal / ZIP code | pc-90001 |
| dma- | Metro area (Nielsen DMA) | dma-501 |
| loc- | City, by name from the locations endpoint | loc-dallas |
| gid- | City, by GeoNames id | gid-5368361 |
| asn- | Autonomous system number | asn-7922 |
| isp- | ISP or mobile carrier | isp-comcast |
| tz- | Timezone | tz-america_new_york |
| near- | Radius search, lat, long, km | near-34.05N118.24W50 |
| ip- | Pin one static IP (dots as underscores) | ip-203_0_113_7 |
| sess- | Sticky session id, same exit IP while it lives | sess-a1b2c3d4 |
| life- | Session lifetime, in seconds | life-600 |
A plan only honours the tokens its pool can actually act on, and a token it does not support is normally refused outright rather than dropped, so a credential never quietly lands somewhere coarser than you asked for. The one deliberate exception is a city name on a pool that accepts cities only by id: if a country or state on the same credential still gives something to target with, the city is dropped and the line is served at that level instead of failing the whole batch. A city name with nothing else alongside it has nothing to fall back to, and is still refused. On resi_lite_pergb a city is gid-; on resi_pergb and the Main pool it is loc-, a name from the locations endpoint.
| Plan | Tokens it honours |
|---|---|
| resi_pergb | geo, reg, loc, asn, sess, life, a catalogued city by name, sticky up to 3 hours |
| resi_lite_pergb | geo, reg, st, pc, dma, gid, asn, isp, tz, near, sess, life, a city is gid, never a name, sticky up to 6 hours |
| mobile_pergb | geo, reg, asn, sess, life |
| dc_pergb | geo, reg, sess, life, US-only pool |
| shared_isp | geo, ip, sess, life, US-only pool |
| ipv6_pergb | None, US-only pool, nothing left to choose |
| eu_isp | geo, NL-only pool; no sticky, it rotates regardless of session |
| dedicated_isp | None, the IP you were assigned is the targeting |
| resi_unlim (Main pool) | geo, loc, asn, sess, life, a catalogued city by name, sticky up to 3 hours |
| resi_unlim (Legacy pool), resi_unlim_budget, dc_unlim | A different username grammar, none of the tokens above apply; let the dashboard generator or POST /api/customer/proxies/generate build these credentials for you |
GET /api/plans/catalog is public and publishes the targeting dimensions each plan supports, alongside each plan’s gateway host and ports. It names dimensions rather than the short tokens above, and is narrower than this table in places, so read it from your code rather than hard-coding either.
Sticky usernames, per plan
The tokens above are the grammar of the plans served through our own gateway. The rest each speak their own, and an unknown keyword is ignored rather than refused, so a credential that looks right can quietly hold no session at all. If you build usernames by hand, copy the shape for your plan exactly; the keyword and the unit both change from plan to plan.
| Plan | Sticky username | Session length |
|---|---|---|
| resi_pergb | user-geo-us-loc-<city>-sess-<id>-life-<seconds> | Seconds, up to 10800 (3 h). The city is optional and must be one the locations endpoint lists |
| resi_lite_pergb | user-geo-us-st-<state>-sess-<id>-life-<seconds> | Seconds, up to 21600 (6 h). The state is optional |
| pool4_pergb | user-geo-us-sess-<id> | Fixed hold of about 10 minutes, no duration token |
| mobile_pergb | user-country-us-session-<id>-time-<minutes> | MINUTES, up to 120 (2 h) |
| shared_isp | user-country-us-time-<seconds>-session-<id> | Seconds, up to 86400 (24 h). Or pin one IP with -ip-203_0_113_10, which cannot be combined with a session |
| eu_isp | user-country-nl | No sticky sessions: this pool rotates whatever you send |
| ipv6_pergb | user | No targeting and no sticky sessions |
| resi_unlim (Main pool) | user-geo-us-loc-chicago-sess-<id>-life-<seconds> | Seconds, up to 10800 (3 h) |
| resi_unlim (Legacy pool) | user-country-us-session-<8 digits>-time-<minutes> | MINUTES, not seconds, up to 1440 (24 h). The session id must be digits only |
| resi_unlim_budget | user-session-<8 digits>-ttl-<seconds>-country-us | Seconds, up to 86400 (24 h). Country is optional and may be omitted |
| dc_unlim | user-session-<id>-duration-<seconds> | Seconds, up to 86400 (24 h). The keyword is duration, not ttl |
| dedicated_isp | user | The IP you were assigned is the targeting |
POST /api/customer/proxies/generate builds all of these for you from mode, extra.session_seconds and a country, and converts the unit each plan expects. Use it and you never have to track the differences above.
Invoices
/api/customer/invoicesYour payment history, top-ups and plan purchases alike. Internal anti-fraud fields are stripped before the response leaves the server.
Order parameters
| Field | Type | Notes |
|---|---|---|
| category_key | string | Required. From /api/customer/plans |
| units | int | Required. GB, or time units for unlimited plans |
| time_unit | string | hour | day | week | month, time-based plans only. Anything else is rejected |
| coupon | string | Optional discount code |
| update_service_id | int | Extend an existing plan instead of creating a new one |
| term_days | int | dedicated_isp only. Any whole multiple of 30 days, up to 360. Leave it out and you get 30; send a value that is not a whole multiple in range and the order is rejected |
| pool | string | dedicated_isp only, and required there: the order is rejected without it, and also if the pool is unknown or short of stock for the IP count you asked for |
Pool ids come from GET /api/proxy/isp-pools, which lists each pool with its live stock. That endpoint takes a logged-in session rather than an API key, the same as the key endpoints above, so pick the pool from the dashboard or from a session call and pass the id you get back.
Error responses
Errors are a JSON object with an error string and a 4xx/5xx status. The message is safe to show a user.
{ "error": "insufficient balance" }| Status | Error | What to do |
|---|---|---|
| 401 | invalid api key | Key is wrong, disabled or deleted |
| 402 | insufficient balance | The order costs more than balance_cents, top up |
| 400 | coupon is no longer valid | Expired, used up, or wrong category |
| 400 | category_key and units required | Missing a required field |
| 400 | pool is required for dedicated ISP orders… | A problem in your payload, named in full. No support reference, because there is nothing for support to look up, fix the field and resend |
| 403 | account suspended | Contact support |
| 403 | Your balance has been locked for security reasons… | The balance shows but cannot be spent, contact support |
| 403 | reseller access not approved | You hit /api/reseller/* with an ordinary key |
| 429 | rate limit exceeded, please try again later | Back off and retry |
| 500 | order could not be completed — you have not been charged. Contact support with reference … | A new plan could not be provisioned. Nothing was deducted; quote the reference |
| 502 | extend could not be completed — the plan was not charged. Contact support with reference … | An update_service_id top-up could not be applied. The plan is untouched and unbilled; quote the reference |
| 500 | order failed | Transient, retry with backoff |
Rate limits
| Surface | Limit |
|---|---|
| /api/customer/* | 7,200 requests / minute, per API key, a burst budget of about 120 a second |
| Order creation | No extra cap, it draws on the same per-key budget as every other call |
| Top-ups (dashboard) | 3 payment invoices / minute, per account. This one is not on the API surface |
| /api/plans/catalog (public) | 60 / minute per IP, shared with the other public endpoints |
The customer surface is metered per key and shared across every /api/customer/* endpoint, so a noisy integration cannot rate-limit your other keys. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, so you can pace yourself without guessing. Exceeding a limit returns 429 with Retry-After, back off and retry rather than hammering.
Quick start
End to end: check the key, buy 10 GB, pull proxy lines.
BASE=https://vaultproxies.net
KEY=VP_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
H="X-API-Key: $KEY"
# 1. Does the key work, and what can I spend?
curl -s $BASE/api/customer/me -H "$H" | jq '{balance_cents, balance_locked}'
# 2. What can I buy?
curl -s $BASE/api/customer/plans -H "$H" \
| jq '.categories[] | {key, price_per_unit_cents}'
# 3. Buy 10 GB of residential
SVC=$(curl -s -X POST $BASE/api/customer/order -H "$H" \
-H 'Content-Type: application/json' \
-d '{"category_key":"resi_pergb","units":10}' | jq -r .service.id)
# 4. Generate proxy lines for it
curl -s -X POST $BASE/api/customer/proxies/generate -H "$H" \
-H 'Content-Type: application/json' \
-d "{\"service_id\":$SVC,\"plan_key\":\"resi_pergb\",\"country\":\"us\",\"protocol\":\"HTTP\"}"import requests
BASE = "https://vaultproxies.net"
H = {"X-API-Key": "VP_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
me = requests.get(f"{BASE}/api/customer/me", headers=H).json()
print("balance:", me["balance_cents"] / 100)
order = requests.post(
f"{BASE}/api/customer/order",
headers={**H, "Content-Type": "application/json"},
json={"category_key": "resi_pergb", "units": 10},
).json()
proxies = requests.post(
f"{BASE}/api/customer/proxies/generate",
headers={**H, "Content-Type": "application/json"},
json={
"service_id": order["service"]["id"],
"plan_key": "resi_pergb",
"country": "us",
"protocol": "HTTP",
},
).json()
print(proxies)