If you resell proxies, the expensive bug isn't a failed provision. It's a port that keeps billing after your customer has left.
We run Singapore mobile proxies on physical SIMs (one modem per port), and in September we opened a reseller API on top of them. This post walks through wiring it into a shop in Python: create a port when an order is paid, extend it on renewal, cancel it on churn, and run a nightly job that catches the ports your own database forgot about. The same shape works for most proxy APIs, so skip the Singapore bits if they don't apply to you.
All the code is in this repo, with a Node version too.
The client
The API takes one header, x-api-key, and JSON bodies. Errors come back as {"detail": {"message": ...}}, sometimes with a code. Wrap that once so the rest of your shop can except on status codes:
import requests
BASE = "https://singaporemobileproxy.com/api/reseller/v1"
class ResellerAPIError(Exception):
def __init__(self, status, detail):
self.status = status
self.detail = detail if isinstance(detail, dict) else {"message": str(detail)}
super().__init__(f"{status}: {self.detail.get('message')}")
class Reseller:
def __init__(self, api_key, timeout=60):
self.s = requests.Session()
self.s.headers["x-api-key"] = api_key
self.timeout = timeout
def _call(self, method, path, **body):
kw = {"timeout": self.timeout}
if method == "POST":
kw["json"] = {k: v for k, v in body.items() if v is not None}
elif body:
kw["params"] = body
r = self.s.request(method, BASE + path, **kw)
if r.status_code >= 400:
try:
detail = r.json().get("detail")
except ValueError:
detail = r.text
raise ResellerAPIError(r.status_code, detail)
return r.json()
Timeout is 60 seconds, not 10. Creating a port means configuring a real modem, and a short client timeout is how you end up with a port that exists on our side and not on yours.
Order paid: create the port
def fulfil(api, order):
try:
res = api._call("POST", "/ports",
bandwidth_gb=order.tier_gb, # 200, 500 or 1000
days=order.days, # 30 for a monthly plan
telco=order.carrier, # "singtel", "m1" or None
label=f"order-{order.id}")
except ResellerAPIError as e:
if e.status == 402: # past your credit limit; the message says how many fit
return order.hold("reseller credit limit: " + e.detail["message"])
if e.status == 503: # no free modem right now, nothing billed
return order.retry_in(minutes=15)
raise
port = res["ports"][0]
order.attach(port_id=port["id"], http=port["http"], socks5=port["socks5"],
rotation_link=port["rotation_link"])
Three things in there matter more than they look.
days=order.days sets an end date. The port runs through that date (Singapore time) and is removed at 00:05 the next morning. Without it the port runs until you cancel, which is what you want for a customer on auto-renew and exactly what you don't want for a one-off 7-day order.
label is your order id. When something goes wrong, GET /ports hands you back your own ids, so you're never matching ports to orders by hostname.
503 means nothing was created and nothing was billed. Retrying is safe. The one I'd treat carefully is a timeout on your side, because then you don't know. Don't blindly retry that. Run the reconcile below first.
Before a big order, check headroom:
acct = api._call("GET", "/account")
print(acct["available_usd"], "of", acct["credit_limit_usd"], "left this month")
available_usd already subtracts what your live ports will still bill until month end, so if it says a port fits, it fits.
Renewal and churn
def renew(api, order, days=30):
api._call("POST", f"/ports/{order.port_id}/end-date", add_days=days)
def churn(api, order):
api._call("POST", f"/ports/{order.port_id}/cancel")
add_days extends from the current end date, or from today if the port has none. The cancel only marks the port cancelled after the proxy server confirms the removal. If it can't confirm, you get a 502 and the port is still live and still billing, so a failed cancel has to go back on your queue, not into a log file nobody reads.
Rotation
Your customer gets rotation_link and can hit it themselves. If you'd rather keep that server-side:
api._call("POST", f"/ports/{port_id}/rotate") # new IP now, 5 min cooldown per port
api._call("POST", f"/ports/{port_id}/auto-rotate", minutes=30) # 0 turns it off
A rotation takes about 20 seconds on our modems. I check the egress before and after with the proxy itself rather than trusting the 200:
def egress_ip(port):
proxy = f"http://{port['login']}:{port['password']}@{port['host']}:{port['http_port']}"
return requests.get("https://api.ipify.org?format=json",
proxies={"http": proxy, "https": proxy}, timeout=20).json()["ip"]
The nightly reconcile
This is the job that saves money. Once a night, list every live port and compare against your orders:
def reconcile(api, db):
live = api._call("GET", "/ports", status="live")["ports"]
for p in live:
order = db.order_for_label(p["label"])
if order is None or order.status in ("cancelled", "refunded"):
print("orphan, still billing", p["daily_cost_usd"], "/day:", p["label"], p["id"])
# cancel it, or flag it for a human if you're not ready to auto-cancel
elif order.port_id != p["id"]:
print("order points at a different port:", order.id, p["id"])
live_ids = {p["id"] for p in live}
for order in db.active_orders():
if order.port_id not in live_ids:
print("customer paying for a port that isn't live:", order.id)
Both directions matter. The first loop finds ports you pay for and nobody uses. The second finds customers paying you for something that isn't there, which is the one that turns into a chargeback.
Start it read-only. I'd run it for a week printing, not cancelling, before letting it touch anything.
What I haven't covered
The modem-trial endpoints (swap-options and swap-trial) let you move a customer's port to a different modem for 10 minutes and keep or revert it. That's a post of its own, and the rules around cross-server moves (they change host, port and login) are easy to get wrong. The reference spells them out.
StarHub isn't on sale right now, so telco takes singtel or m1. GET /plans returns free stock per carrier, and it's worth showing that in your own checkout rather than selling a carrier that's out.
Wholesale prices and how to apply are on the reseller page.
Top comments (0)