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 /account shows 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.
GET /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
}
GET /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"}}
}
POST /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"
    }
  ]
}
GET /api/reseller/v1/ports

your ports. ?status=live (default), cancelled or all

example response
{"ports": [ ...same shape as above... ]}
GET /api/reseller/v1/ports/{id}

one port

example response
{ ...one port... }
POST /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... }
POST /api/reseller/v1/ports/{id}/cancel

remove the port now. billing stops with it.

example response
{ ...the port, status "cancelled"... }
POST /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"}
POST /api/reseller/v1/ports/{id}/reboot

reboot the modem (5 minute cooldown)

example response
{"message": "Rebooting. The proxy is back in about a minute."}
POST /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... }
GET /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
}
POST /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... }
}
GET /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... } }
POST /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": { ... } }
POST /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": { ... } }
POST /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": { ... } }
POST /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": { ... } }
POST /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": { ... } }
POST /api/reseller/v1/ports/{id}/credentials

change the proxy login

parameters: login and password (both required, letters and digits only)

example response
{ ...the port... }
POST /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... }
POST /api/reseller/v1/ports/{id}/label

set or clear the label

parameters: label (empty to clear)

example response
{ ...the port... }
POST /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"}
POST /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.code says 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's cooldown also carries available_at -- trial_limit and concurrent_limit don't, just wait and retry; attempt_limit on 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.