Reseller API
create and manage proxies for your own customers, at your reseller price. make an API key in the reseller dashboard. customer accounts use the customer API instead; each key works on one API only.
base URL
https://singaporemobileproxy.com/api/reseller/v1/
authentication
send your key in the x-api-key header. POST bodies can be JSON or form-encoded.
curl -H "x-api-key: YOUR_API_KEY" https://singaporemobileproxy.com/api/reseller/v1/account
billing
- no deposit. each port bills its daily cost (your reseller price) against your credit limit, and you're invoiced monthly. the default limit is $500 a month.
- a new port has to fit in what's left of the limit, counting what your live ports will still bill until month end.
GET /accountshows the numbers. - a port runs until you cancel it, or until its end date. it runs through the end date (Singapore time) and is removed at 00:05 SGT the next morning. you're billed for the days it runs.
- ports you make here also show in your reseller dashboard, and panel ports show here.
/api/reseller/v1/account
your discount and where you stand against your credit limit this month
example response
{
"shop": {"id": "fee89a06-...", "name": "yourshop"},
"discount_pct": 30,
"bonus_discount_pct": 0,
"credit_limit_usd": 500.0,
"spent_this_month_usd": 12.4,
"committed_to_month_end_usd": 11.2,
"available_usd": 476.4,
"live_ports": 1,
"remaining_days_this_month": 12
}
/api/reseller/v1/plans
the tiers you can buy, at your prices, plus free stock per telco
example response
{
"plans": [
{"plan_id": "626ec4d4-...", "name": "200GB Plan", "bandwidth_gb": 200, "monthly_usd": 28.0, "daily_usd": 0.9333},
{"plan_id": "a8765dc1-...", "name": "500GB Plan", "bandwidth_gb": 500, "monthly_usd": 35.0, "daily_usd": 1.1667}
],
"stock": {"singtel": {"available": 8, "label": "5+"}, "m1": {"available": 3, "label": "3"}}
}
/api/reseller/v1/ports
create ports. nothing is charged up front.
parameters: bandwidth_gb (optional: 200, 500 or 1000; default = the cheapest) or plan_id, quantity (optional, 1-10, default 1), days (optional, 1-365: sets the end date; leave out to run until cancelled), telco (optional: singtel or m1), label (optional, up to 64 characters)
example response
{
"message": "Created",
"requested": 1,
"created": 1,
"ports": [
{
"id": "3f2a...",
"label": "customer 42",
"status": "active",
"plan": {"id": "626ec4d4-...", "name": "200GB Plan", "bandwidth_gb": 200},
"host": "sg.veerlane.com",
"http_port": 8105,
"socks_port": 5105,
"login": "user",
"password": "pass",
"http": "sg.veerlane.com:8105:user:pass",
"socks5": "sg.veerlane.com:5105:user:pass",
"rotation_link": "https://sg.veerlane.com/rotate/...",
"daily_cost_usd": 0.9333,
"end_date": "2026-09-26",
"removed_at": "2026-09-27T00:05:00+08:00"
}
]
}
/api/reseller/v1/ports
your ports. ?status=live (default), cancelled or all
example response
{"ports": [ ...same shape as above... ]}
/api/reseller/v1/ports/{id}
one port
example response
{ ...one port... }
/api/reseller/v1/ports/{id}/end-date
set, clear or extend the end date. returns the port.
parameters: date (YYYY-MM-DD, tomorrow to 365 days out, or null to clear it) or add_days (1-365, added to the current end date, or to today if it has none)
example response
{ ...the port, with the new end_date... }
/api/reseller/v1/ports/{id}/cancel
remove the port now. billing stops with it.
example response
{ ...the port, status "cancelled"... }
/api/reseller/v1/ports/{id}/rotate
new IP (5 minute cooldown per port). your customer can also use the port's rotation_link.
example response
{"message": "Rotated"}
/api/reseller/v1/ports/{id}/reboot
reboot the modem (5 minute cooldown)
example response
{"message": "Rebooting. The proxy is back in about a minute."}
/api/reseller/v1/ports/{id}/swap-modem
move the port to another modem, e.g. when one is dead. host, port and login stay the same.
parameters: telco (optional: singtel or m1)
example response
{ ...the port... }
/api/reseller/v1/ports/{id}/swap-options
every free modem you could move this port to, on every server in its location: carrier, model (M300z, E3372, K5161h), tag (the last 3 characters of the modem's serial, so two modems of the same model can be told apart), 4G/5G, last measured speed and how many of today's speed tests passed. modem_id is what you pass to swap-trial.
example response
{
"current": {"server_name": "Server 3", "telco": "m1", "model": "E3372", "tag": "417", "network": "4G"},
"servers": [
{"server_id": "...", "name": "Server 3", "is_current": true, "modems": [
{"modem_id": "m_1a2b3c4d5e", "telco": "singtel", "model": "K5161h", "tag": "093", "network": "4G", "down_mbps": 38.2, "up_mbps": 11.9, "latency_ms": 42, "tested_at": "...", "tests_ok_24h": 15, "tests_total_24h": 16}
]}
],
"trial": null
}
/api/reseller/v1/ports/{id}/swap-trial
try another modem for 10 minutes. same server: the port moves, host/port/login stay the same, and your old modem is held so nobody else can take it. another server: you get new_proxy connection details to test while the old ones keep working -- until you keep, credentials_change is true and the trial's new_proxy is the pair to test. if you do nothing, on_timeout decides what happens when the 10 minutes run out: keep (default) or revert. we check the new modem actually passes traffic the moment you switch (if it doesn't, you're moved back automatically and the trial never starts) and again when the 10 minutes are up if you haven't kept or reverted -- we don't poll it continuously in between, so a modem that dies partway through is only caught at that final check. the shop's contact email and telegram (if set, in shop settings) get a reminder shortly before the trial ends and a notice of the outcome when it does -- same as the dashboard. for a trial on another server, the reminder warns that keeping it (or doing nothing) changes the port's host, port, username and password, and once it's kept -- by you or by the timeout -- they get the new credentials by email and telegram. the old ones stop working, so update anything that uses the port.
parameters: modem_id (from swap-options), or telco and/or server_id (server_id on its own takes the fastest free modem on that server, regardless of telco); on_timeout: keep (default) or revert
example response
{
"trial": {
"id": "b7e1...", "status": "active", "on_timeout": "keep",
"modem": {"modem_id": "m_1a2b3c4d5e", "carrier": "singtel", "model": "K5161h", "tag": "093", "network": "4G", "server_id": "..."},
"credentials_change": false, "new_proxy": null,
"attempts": 1, "started_at": "...", "expires_at": "...", "seconds_left": 600,
"extended": false, "can_extend": true, "ended_at": null, "outcome": null, "can_go_back": false, "went_back": false
},
"port": { ...the port, with swap_trial set to the same object... }
}
/api/reseller/v1/ports/{id}/swap-trial
this port's current trial, or the one that ended in the last hour. every port object returned anywhere in this API also carries its own latest trial as swap_trial, so you don't usually need to poll this separately.
example response
{ "trial": { ...same shape as above... } }
/api/reseller/v1/ports/{id}/swap-trial/keep
keep the trial modem. for a cross-server trial, this is the moment the new connection details (new_proxy) become the port's real ones and the old modem is released -- the response's port reflects the new host/port/login. requires the port to still be live.
example response
{ "trial": {"status": "kept", ...}, "port": { ... } }
/api/reseller/v1/ports/{id}/swap-trial/revert
go back to the original modem now, before the 10 minutes are up. cancelling or pausing the port ends its trial on its own.
example response
{ "trial": {"status": "reverted", ...}, "port": { ... } }
/api/reseller/v1/ports/{id}/swap-trial/next
try a different modem inside the same trial, without ending it. your original modem stays held the whole time. requires the port to still be live.
parameters: modem_id (from swap-options), or telco and/or server_id
example response
{ "trial": { ...the new attempt, same shape as start... }, "port": { ... } }
/api/reseller/v1/ports/{id}/swap-trial/extend
add another 10 minutes. once per trial. requires the port to still be live.
example response
{ "trial": {"extended": true, ...}, "port": { ... } }
/api/reseller/v1/ports/{id}/swap-trial/go-back
after a keep (including an automatic keep on timeout), move back to the original modem. only for a same-server trial, only within an hour of the keep, only while that original modem is still free, and requires the port to still be live.
example response
{ "trial": {"went_back": true, ...}, "port": { ... } }
/api/reseller/v1/ports/{id}/credentials
change the proxy login
parameters: login and password (both required, letters and digits only)
example response
{ ...the port... }
/api/reseller/v1/ports/{id}/plan
change the tier. an upgrade has to fit your credit limit.
parameters: bandwidth_gb or plan_id
example response
{ ...the port... }
/api/reseller/v1/ports/{id}/label
set or clear the label
parameters: label (empty to clear)
example response
{ ...the port... }
/api/reseller/v1/ports/{id}/os
OS spoofing
parameters: os_type: none, android:1, android:3, android:4, ios:1, ios:2, ios:3, macosx:3, macosx:4, macosx:5, windows:1 or windows:4
example response
{"message": "Updated", "os_type": "windows:4"}
/api/reseller/v1/ports/{id}/auto-rotate
rotate the IP on a timer
parameters: minutes: 0 turns it off, otherwise 5-1440
example response
{ ...the port, with auto_rotate_mins... }
example: a 7-day 500GB port on Singtel
curl -X POST -H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bandwidth_gb": 500, "days": 7, "telco": "singtel", "label": "customer 42"}' \
https://singaporemobileproxy.com/api/reseller/v1/ports
errors
errors are JSON: {"detail": {"message": "what went wrong"}}. every error raised by the trial engine also carries a machine-readable detail.code: disabled, not_found, no_recent_trial, invalid, cancelled, trial_open, no_open_trial, cooldown, trial_limit, concurrent_limit, modem_taken, no_free_modem, cross_server_disabled, start_failed, not_extendable, go_back_unavailable, keep_failed, port_inactive, attempt_limit, carrier_locked or internal_error -- key off it, not the message text, which can change. exception: the general 120-requests-a-minute rate limit below doesn't carry a code, just a message.
- 400 -- a missing or invalid parameter; on a swap-trial endpoint this carries
code: "invalid" - 401 -- missing or invalid API key
- 402 -- past your credit limit (the message says how many ports fit)
- 403 -- not a reseller key, or the reseller account isn't active; on a swap-trial start/next this can also be
cross_server_disabled-- moving the port to another server isn't enabled for your account right now - 404 -- no such port on your account (
not_found), or on a swap-trial endpoint: the trial feature is off (disabled), there's no trial from the last hour to read (no_recent_trial), or the action in the URL isn't one we recognise (not_found) - 409 -- the port is cancelled (
cancelled-- every swap-trial action needs a live port; only the GET above still works on a cancelled port), or (swap-trial actions) a trial is already running on this port, or there's none to act on, or (swap-trial start) the port is paused (port_inactive), or (swap-trial start/next) the port is locked to one network and no free modem on it matches (carrier_locked) --detail.codesays which - 429 -- too fast: 120 requests a minute, 30 new ports an hour, 5 minutes between rotations or reboots of a port (none of these carry a
code); a trial'scooldownalso carriesavailable_at--trial_limitandconcurrent_limitdon't, just wait and retry;attempt_limiton a trial's next means that trial has tried as many modems as it can (5 per trial, and never past 30 minutes from its start) -- keep or revert it instead - 502 -- the proxy server refused the change; nothing was changed, try again
- 503 -- no free modem right now (nothing was created or billed); a trial's keep that couldn't finish on the proxy server (
keep_failed); or a trial's new modem that didn't pass traffic when we switched to it (start_failed) -- rarely, moving you back to the original modem fails too, and the body then also carries"stranded": true. any of these means we're already alerted and support will sort out the port. - 500 --
internal_error: something went wrong on our side; we're already alerted
a trial that moves the port to a modem on another server changes the port's host, port and login -- the trial's credentials_change is true and new_proxy carries the pair to test. keeping it makes those the port's permanent connection details (reflected in the response's port); reverting or timing out with on_timeout: revert leaves the port exactly as it was. a same-server trial never changes host, port or login. timing out with no on_timeout set defaults to keep. whichever account made the API call (your shop), its contact email and Telegram chat id (both set from your shop settings) get a reminder near the end of a trial and a notice of the outcome when it ends, whether it was your API call or a timeout that finished it.