Programmatic access to buy, manage, and monitor UK mobile proxies. This API covers UK stock only — it does not expose USA inventory.
Base URL: https://t-proxy.net/api/v1
Every request must include your API key in the x-api-key header. Generate or revoke your key from My Account — reseller-tier accounts only, one active key per account.
curl -H "x-api-key: tpk_live_..." https://t-proxy.net/api/v1/account
Keys never expire on their own. Regenerating a key immediately invalidates the previous one. If your account is downgraded from reseller status, the key stops working even though the row itself still exists.
A typical integration: check what's in stock, buy one, then keep it running.
KEY="tpk_live_..."
BASE="https://t-proxy.net/api/v1"
# 1. See what's available right now
curl -s -H "x-api-key: $KEY" "$BASE/stock"
# 2. Buy a 4G EE proxy for a week
curl -s -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"carrier":"EE","planCode":"4g_week"}' "$BASE/proxies"
# → { "proxies": [ { "id": 501, "proxy_username": "...", ... } ] }
# (Need WireGuard or Vless? Check "$BASE/stock?wireguard=1" and add
# "deviceFilter":{"wireguard":true} to the claim - see WireGuard & Vless below.)
# 3. Check on it later
curl -s -H "x-api-key: $KEY" "$BASE/proxies/501"
# 4. Rotate its IP whenever you need a fresh one
curl -s -X POST -H "x-api-key: $KEY" "$BASE/proxies/501/rotate"
# 5. Extend it before it expires
curl -s -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"planCode":"4g_week"}' "$BASE/proxies/501/extend"
Stock counts are a snapshot, not a reservation — a claim can still 409 if another order takes the last modem between your stock check and your claim. Treat 409 on claim as "retry", not an error to surface to your own users unchanged.
All errors return JSON: { "error": "..." }, with a relevant HTTP status code:
| Status | Meaning |
|---|---|
| 400 | Bad request — missing/invalid field |
| 401 | Missing, invalid, or revoked API key |
| 402 | Insufficient account balance |
| 403 | Key is valid but the account no longer has reseller access |
| 404 | Proxy not found, or not owned by this account |
| 409 | Conflict — e.g. stock changed mid-order, or the action isn't available for this proxy yet |
| 429 | Rate limit exceeded — see below |
| 502 | The UK database (upstream provider) failed to respond as expected |
Limits apply per account (i.e. per API key), not per IP. Each tier below is tracked independently, so exhausting one doesn't affect the others. Standard RateLimit-* response headers report your remaining quota on every call.
| Tier | Endpoints | Limit |
|---|---|---|
| Read | GET /account, /plans, /stock, /proxies, /proxies/:id, /proxies/:id/ovpn | 120 / minute |
| Action | POST /proxies/:id/rotate, /reboot, /spoof, /ip-mode, /credentials, /auto-renew | 20 / minute |
| Purchase | POST /proxies, /proxies/:id/extend, /coupons/validate | 10 / minute |
A 429 includes how long to wait via the RateLimit-Reset header (seconds). If your integration needs a higher ceiling, get in touch rather than working around this.
Returns your balance and reseller status.
{ "email": "you@example.com", "balance": 42.50, "is_reseller": true }
Lists every UK fixed-term plan and its price at your account's tier (reseller pricing is applied automatically). Pay & Go is discontinued and never appears here.
{
"plans": [
{ "code": "4g_day", "label": "4G — 1 day", "generation": "4g", "term": "day", "price_amount": 2.5, "price_unit": "/day" },
{ "code": "5g_month", "label": "5G — 1 month", "generation": "5g", "term": "month", "price_amount": 50, "price_unit": "/month" }
]
}
Live availability per carrier. Counts reflect modems free right now — they change constantly and aren't reserved by checking them.
{
"carriers": [
{ "carrier": "EE", "available_4g": 0, "available_5g": 1, "has_ipv6": true, "has_vless": true, "has_wireguard": true, "has_phone": true, "has_router": true,
"next_available_4g": "2026-10-05T21:30:00.000Z", "next_available_5g": null,
"pending": [ { "generation": "4g", "device": "router", "ipv6": true, "vless": true, "wireguard": true, "available_at": "2026-10-05T21:30:00.000Z" } ] }
]
}
Carriers: EE, O2, Three, Sky, Lyca.
| Parameter | Values | Notes |
|---|---|---|
ipv6 | 1 / true | Only count modems with IPv6 |
vless | 1 / true | Only count modems with VLESS (Xray) |
wireguard | 1 / true | Only count modems with WireGuard |
device | phone / router | Only count that device type |
Example: GET /api/v1/stock?wireguard=1&device=router. Filters narrow available_4g, available_5g, next_available_* and pending — the same as the filter chips on the Browse page. The has_* fields always describe what the carrier can do at all, whatever the filters.
next_available_4g / next_available_5g are the earliest time (ISO, UTC) a currently rented proxy of that generation is due to free up, or null when none is scheduled — the same estimate as the "until next slot" countdown on the Browse page. pending lists every rented proxy for the carrier, soonest first, each with its device, IPv6, VLESS and WireGuard flags. These are estimates: a proxy that is extended or auto-renews before then won't free up.
Previews a coupon's discount without using it. Coupons are applied by passing couponCode to POST /proxies or POST /proxies/:id/extend; a coupon is only used up when that order goes through.
| Field | Type | Required | Notes |
|---|---|---|---|
code | string | yes | The coupon code |
planCode | string | yes | From GET /plans |
quantity | integer | no | Default 1; ignored for "extend" |
context | string | no | "claim" (default) or "extend" |
{ "valid": true, "code": "BETA25", "discount_type": "percent", "discount_value": 25, "order_total": 40, "discount_amount": 10, "final_amount": 30 }
An invalid, expired or used-up code returns 400 with { "valid": false, "error": "..." }. Prices are your own account's prices, and the discount comes off the whole order.
Trial coupons (discount_type: "trial") make a new order free, but each proxy in it runs for 2 hours, whatever the plan's term. They can't be used on extensions.
Buys one or more proxies on a given carrier and plan. Charges your account balance immediately; fails with 402 if your balance is too low.
| Field | Type | Required | Notes |
|---|---|---|---|
carrier | string | yes | One of the five carriers above |
planCode | string | yes | From GET /plans |
quantity | integer | no | Default 1, max 10 — number of separate proxies to claim |
deviceFilter | object | no | { "phone": bool, "router": bool, "ipv6": bool, "vless": bool, "wireguard": bool } — require a capability; omit a field to accept either. Use the same filters on GET /stock to check there's a match first |
ipMode | string | no | "" (default, dual-stack) or "64" (prefer IPv6 first) |
couponCode | string | no | A coupon code (not case-sensitive), applied to the whole order. See coupons |
curl -X POST https://t-proxy.net/api/v1/proxies \
-H "x-api-key: tpk_live_..." -H "Content-Type: application/json" \
-d '{"carrier":"EE","planCode":"4g_week","quantity":2}'
Returns 201 with { "proxies": [...], "charged": 30, "discount_amount": 10, "coupon": "BETA25" } (coupon is null without one). For the proxy shape, see proxy detail. If stock runs out mid-order, whatever was already claimed is rolled back and you're charged nothing; retry with a smaller quantity.
Lists every active proxy on your account.
{ "proxies": [ { "id": 501, "plan": "4g_week", "carrier": "EE", "proxy_username": "...", "proxy_password": "...", "http_port": 8123, "socks_port": 5123, "connect_host": "203.0.113.10", "rotation_url": "https://...", "vless_link": "vless://…", "wireguard_link": "wireguard://…", "ovpn_link": "https://t-proxy.net/api/proxies/public/ovpn/…", "auto_renew": false, "expires_at": "2026-09-24T12:00:00.000Z", "created_at": "2026-09-17 10:00:00" } ] }
vless_link and wireguard_link are null on proxies whose modem doesn't support them. See WireGuard & Vless for how to get them and turn wireguard_link into a config file.
ovpn_link is a shareable OpenVPN profile download that needs no login, so you can pass it straight to your own customer. Anyone with the link can download the profile; the proxy's username and password are still needed to connect.
Same shape as above, plus live fields fetched from the UK database on each call:
{ "proxy": { ...as above,
"current_ip": { "ipv4": "203.0.113.55", "ipv6": null },
"spoof": { "current": "", "options": [ { "value": "", "label": "No spoofing (default)" }, { "value": "windows:1", "label": "Windows 10" }, ... ] },
"ip_mode": { "current": "v4", "ipv6Available": true },
"data_quota_mb": null,
"data_usage_mb": 20377.6 } }
data_usage_mb is total traffic (download + upload) since the proxy was created. data_quota_mb is null on all fixed-term plans, which have no data cap. rotation_url is refreshed here too, so use the value from this call if yours stops working. Any live field is null (or its default) if the UK database is briefly unreachable.
Most of our modems also support WireGuard and Vless (Xray). Each proxy on one of them has a wireguard_link and a vless_link in GET /proxies and GET /proxies/:id; on other proxies both are null.
Check stock with GET /stock?wireguard=1 (or ?vless=1), then claim with "deviceFilter": { "wireguard": true } (or "vless": true). With the filter set, a claim only ever lands on a modem that has it. Without it you may get one that doesn't.
curl -s -H "x-api-key: $KEY" "$BASE/stock?wireguard=1"
curl -s -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"carrier":"EE","planCode":"4g_week","deviceFilter":{"wireguard":true}}' "$BASE/proxies"
| Field | Format | Use it with |
|---|---|---|
wireguard_link | wireguard://PRIVATEKEY@HOST:PORT?publickey=…&address=…&presharedkey=…&remoteDNS=… | Apps that import wireguard:// links (Hiddify, v2rayN, NekoBox), or convert it to a .conf for the official WireGuard apps (below) |
vless_link | vless://… | Xray apps such as v2rayNG, Hiddify, Streisand or Shadowrocket: import from clipboard |
Neither link needs the proxy's username or password: anyone who has one can use the proxy, so treat both like passwords and give each customer only their own. Both change when a proxy is migrated, so read them again after a migration. The keys in wireguard_link are plain base64 and can contain /, + and =, so most URL parsers misread it — split it by hand.
import requests
KEY = "tpk_live_..."
BASE = "https://t-proxy.net/api/v1"
def wireguard_conf(link):
# wireguard://PRIVATEKEY@HOST:PORT?publickey=...&address=...&presharedkey=...&remoteDNS=...
# The keys are plain base64 (they can contain / + =), so split by hand
# rather than with a URL parser.
key, _, rest = link[len("wireguard://"):].partition("@")
endpoint, _, query = rest.partition("?")
params = dict(p.split("=", 1) for p in query.split("&") if "=" in p)
lines = ["[Interface]",
f"PrivateKey = {key}",
"Address = " + params["address"].replace(",", ", ")]
if params.get("remoteDNS"):
lines.append(f"DNS = {params['remoteDNS']}")
lines += ["", "[Peer]", f"PublicKey = {params['publickey']}"]
if params.get("presharedkey"):
lines.append(f"PresharedKey = {params['presharedkey']}")
lines += [f"Endpoint = {endpoint.rstrip('/')}",
"AllowedIPs = 0.0.0.0/0, ::/0",
"PersistentKeepalive = 25"]
return "\n".join(lines) + "\n"
proxy = requests.get(f"{BASE}/proxies/501", headers={"x-api-key": KEY}, timeout=60).json()["proxy"]
if proxy["wireguard_link"]:
with open(f"tproxy-{proxy['id']}.conf", "w") as f:
f.write(wireguard_conf(proxy["wireguard_link"]))
else:
print("No WireGuard on this proxy - claim with deviceFilter.wireguard")
This writes the same file as the WireGuard button on the proxy page:
[Interface]
PrivateKey = …
Address = 10.2.1.15/32, fd78:…::1:15/128
DNS = …
[Peer]
PublicKey = …
PresharedKey = …
Endpoint = server2.t-proxy.net:444
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
WireGuard connects over UDP port 444. Each config is one WireGuard peer, so use it on one device at a time.
Requests a new IP on the modem. Returns { "ok": true, "ext_ip": "203.0.113.99" }.
Reboots the physical device. Returns { "ok": true, "message": "..." }. Expect the proxy to be briefly unreachable afterward.
Extends a proxy's expiry by charging for another term. Body: { "planCode": "4g_week", "couponCode": "BETA25" } (couponCode optional) — planCode must match the proxy's current generation (4G proxies can only extend onto 4G plans, same for 5G).
{ "ok": true, "expires_at": "2026-10-01T12:00:00.000Z", "charged": 15, "discount_amount": 5, "coupon": "BETA25", "plan": "4g_week" }
Downloads the proxy's OpenVPN profile (Content-Type: application/x-openvpn-profile). Same file as ovpn_link, but behind your API key.
Sets the OS / TCP fingerprint the proxy presents. Body: { "os": "windows:1" }, using a value from spoof.options on proxy detail; "" turns spoofing off. os is required.
The profile applies to IPv4 connections to ports 80, 443 and 8443; IPv6 connections and other ports keep the device's own fingerprint. If the spoofed fingerprint matters, set the proxy to prefer IPv4 with POST /proxies/:id/ip-mode and { "mode": "v4" } — otherwise sites that support IPv6 are reached over IPv6 and see the unspoofed fingerprint.
{ "ok": true, "os": "windows:1", "rotation_url": "https://..." }
Chooses whether the proxy prefers IPv4 or IPv6. Body: { "mode": "v4" } or { "mode": "v6" }. Both stay reachable; this only sets the default. "v6" returns 409 if ip_mode.ipv6Available is false, or if the SIM has no IPv6 right now.
{ "ok": true, "mode": "v6", "rotation_url": "https://..." }
Sets your own username or password for HTTP, SOCKS5 and OpenVPN. Body: { "field": "password", "value": "MyPass_123" }, where field is "username" or "password" and value is 3–32 letters, numbers, _ or -. Anything connected with the old login is disconnected.
{ "ok": true, "proxy_username": "...", "proxy_password": "MyPass_123", "rotation_url": "https://..." }
The rotation URL can change when credentials change. Replace your stored copy with the one returned here.
Turns automatic renewal on or off. Body: { "enabled": true }. When on, the proxy is renewed for another term of its current plan from your balance shortly before it expires.
{ "ok": true, "auto_renew": true }