Developers
API reference
Generate proxies and manage your account programmatically over a simple REST API. Base URL https://resifactory.net/api/v1.
On this page
- Getting started
- Overview
- Authentication
- Scopes
- Quickstart
- Endpoints
- GET
/me - POST
/proxies - GET
/balance - GET
/usage - GET
/pools - GET
/pools/{id} - GET
/status - GET
/usage/daily - GET
/usage/domains - GET
/transactions - GET
/generations - POST
/balance/convert - POST
/billing/topups - GET
/billing/topups/{id} - GET
/isp/orders - GET
/isp/orders/{id}/ips - POST
/isp/orders/{id}/reset-password - Reference
- Pools & targeting
- Output formats
- Strict mode
- Idempotency
- Pagination
- Errors
- Rate limits
- Code examples
- Versioning
- FAQ
BreakingTwo breaking changes, 2026-08-06 and 2026-08-31
balances on GET /balance/GET /me changed from a plain number map to { gb, price_per_gb, currency } objects — read balances[poolId].gb, or keep reading balances_gb until its Sunset if you can't migrate yet. Also, the carrier body parameter and the socks5 protocol value were removed from POST /proxies: carrier targeting was withdrawn from the platform on 2026-07-22 with the parameter silently ignored ever since, and no relay had ever run a socks5 listener, so a line generated with either could never actually connect. socks5 returned on 2026-10-23 on port 1080, on every pool except Essential. https was removed the same day for an unrelated reason (two relays couldn't yet serve :7443) and RESTORED 2026-08-06 by RF-16 once every relay served it again, then withdrawn on 2026-08-31 (below). Removing carrier and socks5 corrects the published contract; no working integration loses anything beyond those two. Full detail in Versioning & Changelog.
https protocol value has been withdrawn from POST /proxies. http (port 7777) is served on every pool, and socks5 (port 1080) on every pool except Essential. https is not offered. We restored https on 2026-08-06 after verifying it across the whole fleet, and that verification was accurate at the time — but our relay fleet was rebuilt on new hardware on 2026-08-18, and the rebuilt machines were never given the TLS certificate or the firewall rule the endpoint needs. :7443 has therefore been unreachable since that date, and any proxy line generated with https in that window could not connect. Rather than restore an endpoint we cannot yet renew certificates for automatically, we have withdrawn it. What to do: send protocol: "http" (or "socks5" on any pool except Essential), or omit the field entirely — it already defaults to http. If you omit it, or you use our default lenient validation, nothing changes for you. If you call the API in strict mode and still send protocol: "https", you will now receive a 422 with not_in_enum instead of a 200 — that is the one case that breaks, and it is the same shape as the socks5 removal above. We are sorry for the 13 days during which this page said https worked and it did not.
Overview
Start hereThe ResiFactory REST API lets you generate proxy credentials, inspect account balances, and review recent usage — all from your own code, scripts, or CI pipelines.
Base URL
https://resifactory.net/api/v1
All endpoints are served under this path. /api/v1 is the current stable version, meant to stay non-breaking — see Versioning & Changelog for the one documented exception and how it's mitigated.
HTTPS only
All requests must be made over HTTPS. Plaintext HTTP connections are rejected. Your API key is transmitted in every request header; never send it over an unencrypted channel.
Machine-readable spec
The OpenAPI 3.1 spec is served at:
/api/v1/openapi.jsonImport it into Postman, Insomnia, or any OpenAPI-compatible tool.
All request and response bodies are JSON (Content-Type: application/json). Responses always include a top-level key — either the resource payload or an error object. See Errors for the full error shape.
Authentication
Bearer tokensEvery request is authenticated with a bearer token. Keys are created in the Developer tab of your dashboard. The full secret is displayed exactly once at creation time — copy it immediately.
Sending the key
Authorization: Bearer rf_live_xxxxxxxxxxxxxxxxxxxx
API keys start with rf_live_. Never share or commit your secret key.
Best practices
One key per application
Create a separate key for each integration so you can revoke one without disrupting others. You can hold up to 10 keys at once.
Minimal scopes
Grant only the scopes the application actually needs. A scraper that only generates proxies does not need usage:read or pools:read.
Rotate & revoke promptly
Rotate keys periodically. If a key is leaked, revoke it immediately from the Developer tab of your dashboard. A revoked key returns 401 unauthorized instantly.
process.env.RF_API_KEY). If you accidentally push a key, revoke it immediately — git history is public.
Scopes
Set once, per keyEach API key carries a set of scopes that control which endpoints it can call. Scopes are set at key creation and cannot be changed afterwards — create a new key if you need different permissions. Keys are created and revoked in the dashboard Developer tab; the API itself cannot mint or revoke keys (a leaked key can never create more). Pick the scopes a key needs at creation time — every key gets proxies:generate, usage:read, and pools:read by default.
| Scope | What it unlocks |
|---|---|
proxies:generate |
Generate proxy credentials. The only scope that can spend bandwidth. |
usage:read |
Read balances, usage, transactions, and generation history. |
pools:read |
Read the pools your account can use and their targeting options. |
billing:write |
Move money on the account. The only scope that can spend dollars — must be requested explicitly and carries a monthly spend cap. |
isp:read |
List your ISP proxy orders and their subnets, and download their IPs with logins. Opt-in: every reveal is recorded on your account. |
isp:write |
Reset the proxy password of an ISP order. Opt-in. |
entitlements:read |
Check whether an account holds a live product licence. First-party only — issued to a ResiFactory control plane by an operator, never selectable when you create your own key. |
entitlements:claim |
Register a machine against an account's licence seats when checking it. First-party only — issued to a ResiFactory control plane by an operator, never selectable when you create your own key. |
GET /me and GET /status are accessible with any valid key regardless of scopes — lightweight identity and health checks.
Quickstart
Under two minutesGenerate your first proxy list in under two minutes.
Create an API key
Log in → Dashboard → Developer. Click Create key, give it a name, leave the default scopes, and copy the secret that appears. It will not be shown again.
Generate 10 US proxies
curl -X POST https://resifactory.net/api/v1/proxies \
-H "Authorization: Bearer rf_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"pool":"mobile_dedicated","country":"us","quantity":10}'
import requests
res = requests.post(
"https://resifactory.net/api/v1/proxies",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
json={
"pool": "mobile_dedicated",
"country": "us",
"quantity": 10
},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/proxies", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({"pool":"mobile_dedicated","country":"us","quantity":10})
});
const data = await res.json();
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
body := []byte(`{"pool":"mobile_dedicated","country":"us","quantity":10}`)
req, _ := http.NewRequest("POST", "https://resifactory.net/api/v1/proxies", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
Read the response
The default format (list) returns a JSON object where proxies is a newline-delimited string of host:port:user:pass lines:
{
"proxies": "mobile.resifactory.net:7777:ResiFactory_a3f9c142~us~1a2b3c4d~0~~md:pass\n...",
"count": 10,
"country": "US",
"is_demo": false,
"pool": "mobile_dedicated",
"format": "list"
}
Paste into your tool
Split the string on \n and use each line as a proxy. Each entry is host:port:username:password. Most HTTP clients accept this format directly. Switch to "format":"json" if you prefer a structured array.
Supported destination ports
Proxied CONNECT requests are supported on ports 80 and 443. Requests to other destination ports are refused and return a generic 502.
In practice this affects background traffic rather than the work you configure — most of it is WebRTC/STUN (3478) and mobile push (5228) emitted automatically by browsers and operating systems using the same proxy settings. If you are deliberately proxying a protocol on another port, it will not work over http: use socks5 (port 1080, any TCP destination port, every pool except Essential), or reach out through support before building around it.
Endpoints
Relative to /api/v1All endpoints are relative to https://resifactory.net/api/v1. Every parameter table below is generated directly from the same manifest that builds the OpenAPI spec — the two can never drift apart.
/me
any valid key
Account summary
Returns your account id, username, the scopes on this key, your balances, and this key's metadata. Works with any valid key regardless of scopes — useful as a credential health check.
Errors:
invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/me \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/me",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/me", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/me", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/proxies
proxies:generate
Generate proxies
Returns formatted proxy lines. A zero balance or a coming-soon pool returns non-working demo credentials so an integration can be wired up before purchase. Send an Idempotency-Key header so a retried request cannot mint a second batch.
Accepts an Idempotency-Key header — see Idempotency.
Request body (JSON)
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
pool |
string | No | — | Pool id to generate from (e.g. `mobile_dedicated`). Omit to use the default for your `proxyType`: `mobile_dedicated` for `mobile_rotating`/`mobile_sticky`, otherwise the Premium pool. Call GET /pools to see the pool ids available to your account. |
country |
string | No | — | ISO 3166-1 alpha-2 country code to target (e.g. `us`, `gb`), case-insensitive. Omitted resolves to `us`. An unrecognized value is refused with 422 `invalid_country`, and a supported code this pool cannot serve with 422 `country_not_available` — it is never silently replaced. Call GET /pools to see which countries a given pool supports. |
quantity |
integer | No | 5000 | Number of proxy lines to generate in this batch. |
proxyType |
string | No | rotating | `rotating` assigns a new exit IP per request. `sticky` holds one exit IP for `sessionDuration` minutes. The `mobile_*` variants are the same behavior on mobile exit IPs. |
sessionDuration |
integer | No | — | Minutes to hold the same exit IP for `sticky` or `mobile_sticky`; defaults to 60 when omitted. Ignored (treated as 0) for `rotating` and `mobile_rotating`. On Essential a session holds at most 120 minutes, whatever value is sent. |
state |
string | No | — | US state to target, as the full lowercase state name (e.g. `california`), not a postal abbreviation. Only applies when the resolved country is `us`; an unrecognized value is silently dropped — no state targeting is applied, and no error is returned even in strict mode. |
protocol |
string | No | http | Proxy protocol for generated entries: `http` (port 7777) or `socks5` (port 1080, any TCP destination port). Essential is HTTP only: a `socks5` request on Essential returns `http` lines. In the default lenient mode any other value falls back to `http`; in strict mode it is rejected with `not_in_enum`. |
asn |
string | No | — | Optional ASN targeting. Accepts `AS7922` or `7922`; targets one network. Not applied on geo-forced pools. A valid ASN with no available exit fails the request upstream rather than silently falling back. |
format |
string | No | list | Shape of `proxies` in the response: `list` is newline-delimited `host:port:user:pass`, `json` is an array of proxy objects, `userpass` is newline-delimited `user:pass@host:port`. |
Errors:
country_not_available (422), idempotency_key_reuse (409), insufficient_scope (403), invalid_country (422), invalid_params (422), pool_not_available (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X POST https://resifactory.net/api/v1/proxies \
-H "Authorization: Bearer rf_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"country":"us","quantity":100,"proxyType":"sticky","sessionDuration":30,"format":"json"}'
import requests
res = requests.post(
"https://resifactory.net/api/v1/proxies",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
json={
"country": "us",
"quantity": 100,
"proxyType": "sticky",
"sessionDuration": 30,
"format": "json"
},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/proxies", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({"country":"us","quantity":100,"proxyType":"sticky","sessionDuration":30,"format":"json"})
});
const data = await res.json();
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
body := []byte(`{"country":"us","quantity":100,"proxyType":"sticky","sessionDuration":30,"format":"json"}`)
req, _ := http.NewRequest("POST", "https://resifactory.net/api/v1/proxies", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/balance
usage:read
Balances per accessible pool
Remaining bandwidth for every pool your account can use, with the current price per GB so you can compute spend without hardcoding it.
Errors:
insufficient_scope (403), invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/balance \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/balance",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/balance", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/balance", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/usage
usage:read
Deprecated
Top domains (deprecated)
Deprecated alias of GET /usage/domains, kept for existing integrations. Use /usage/domains, which supports date ranges and pagination.
Query parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
limit |
integer | No | 25 | Maximum number of domains to return, ordered by bytes transferred descending. |
Errors:
insufficient_scope (403), invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/usage?limit=5 \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/usage?limit=5",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/usage?limit=5", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/usage?limit=5", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/pools
pools:read
Accessible pools and their targeting options
Lists the pools your account can use. Each pool carries its own targeting options — country lists differ per pool, so discover them here rather than hardcoding.
Errors:
insufficient_scope (403), invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/pools \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/pools",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/pools", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/pools", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/pools/{id}
pools:read
One pool
Returns a single pool your account can use. A pool id your account cannot use returns the same 404 as an id that does not exist.
Path parameters
| Name | Notes |
|---|---|
id |
Pool id, as returned by GET /pools. |
Errors:
insufficient_scope (403), invalid_params (422), pool_not_available (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/pools/123 \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/pools/123",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/pools/123", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/pools/123", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/status
any valid key
Service status and this key's budgets
Reports service availability, the API behaviour version, the scopes and per-minute rate budgets on this key, and any active deprecations.
Errors:
invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/status \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/status",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/status", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/status", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/usage/daily
usage:read
Daily bandwidth
Bandwidth and request counts per day per pool. Omit the date range for the last 30 days.
Query parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
from |
string (date) | No | — | Start of the date range (YYYY-MM-DD), inclusive. Defaults to 30 days before today when omitted, independent of `to`. |
to |
string (date) | No | — | End of the date range (YYYY-MM-DD), inclusive. Defaults to today when omitted, independent of `from`. |
pool |
string | No | — | Restrict to one pool id, as returned by GET /pools. Omit (or send `all`) to combine every pool your account can access. A pool id you cannot use returns the same 404 as one that does not exist. |
Errors:
insufficient_scope (403), invalid_params (422), pool_not_available (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/usage/daily \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/usage/daily",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/usage/daily", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/usage/daily", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/usage/domains
usage:read
Bandwidth by destination domain
Your top destination domains by bytes, cursor-paginated. Replaces GET /usage, which returned an unbounded top-25 with no date range. Per-domain history covers the last 90 days; a range reaching past that returns 422 `invalid_params`.
Cursor-paginated — see Pagination.
Query parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
from |
string (date) | No | — | Start of the date range (YYYY-MM-DD), inclusive. Defaults to 30 days before today when omitted, independent of `to`. Must be within the last 90 days (today counts as one). |
to |
string (date) | No | — | End of the date range (YYYY-MM-DD), inclusive. Defaults to today when omitted, independent of `from`. The range may span at most 90 days. |
pool |
string | No | — | Restrict to one pool id, as returned by GET /pools. Omit (or send `all`) to combine every pool your account can access. A pool id you cannot use returns the same 404 as one that does not exist. |
limit |
integer | No | 25 | Maximum number of domains to return on this page. |
cursor |
string (cursor) | No | — | Opaque pagination cursor from a previous response's `next_cursor`. Omit to fetch the first page. |
Errors:
insufficient_scope (403), invalid_params (422), pool_not_available (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/usage/domains?limit=5 \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/usage/domains?limit=5",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/usage/domains?limit=5", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/usage/domains?limit=5", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/transactions
usage:read
Account transactions
Purchases, credits, and conversions, newest first, cursor-paginated.
Cursor-paginated — see Pagination.
Query parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
from |
string (date) | No | — | Start of the date range (YYYY-MM-DD), inclusive. Unlike GET /usage/daily and GET /usage/domains, an omitted `from` is not defaulted to 30 days ago — it leaves the range unbounded below. |
to |
string (date) | No | — | End of the date range (YYYY-MM-DD), inclusive. An omitted `to` leaves the range unbounded above, not defaulted to today. |
pool |
string | No | — | Restrict to one pool id, as returned by GET /pools. Omit (or send `all`) to combine every pool your account can access. A pool id you cannot use returns the same 404 as one that does not exist. |
limit |
integer | No | 25 | Maximum number of transactions to return on this page. |
cursor |
string (cursor) | No | — | Opaque pagination cursor from a previous response's `next_cursor`. Omit to fetch the first page. |
Errors:
insufficient_scope (403), invalid_params (422), pool_not_available (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/transactions \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/transactions",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/transactions", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/transactions", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/generations
usage:read
Generation history
Every proxy generation on this account, newest first, cursor-paginated.
Cursor-paginated — see Pagination.
Query parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
limit |
integer | No | 25 | Maximum number of generation events to return on this page. |
cursor |
string (cursor) | No | — | Opaque pagination cursor from a previous response's `next_cursor`. Omit to fetch the first page. |
Errors:
insufficient_scope (403), invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/generations \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/generations",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/generations", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/generations", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/balance/convert
billing:write
Convert balance between pools
Moves leftover bandwidth from one pool to another at current prices. Value-preserving and rounded down, so a conversion never creates value. No card is charged.
Accepts an Idempotency-Key header — see Idempotency.
Request body (JSON)
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
from_pool |
string | Yes | — | Pool id to move balance out of, as returned by GET /pools (e.g. `mobile_dedicated`). A pool id you cannot use returns the same 404 as one that does not exist. |
to_pool |
string | Yes | — | Pool id to move balance into, as returned by GET /pools. Must be a different pool than `from_pool` with a distinct balance — the two cannot share one wallet, refused with `invalid_request` if they do. A pool id you cannot use returns the same 404 as one that does not exist. |
gb |
number | Yes | — | Amount to move out of `from_pool`, in GB (between 0.1 and 100000). The destination amount is computed server-side from live per-GB prices and rounded down — the destination pool is never credited more dollar value than `from_pool` loses. There is no destination-amount parameter; do not compute or send one. |
Errors:
idempotency_key_reuse (409), insufficient_scope (403), internal_error (500), invalid_params (422), invalid_request (400), pool_not_available (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X POST https://resifactory.net/api/v1/balance/convert \
-H "Authorization: Bearer rf_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"from_pool":"mobile_dedicated","to_pool":"retail_select","gb":0.1}'
import requests
res = requests.post(
"https://resifactory.net/api/v1/balance/convert",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
json={
"from_pool": "mobile_dedicated",
"to_pool": "retail_select",
"gb": 0.1
},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/balance/convert", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({"from_pool":"mobile_dedicated","to_pool":"retail_select","gb":0.1})
});
const data = await res.json();
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
body := []byte(`{"from_pool":"mobile_dedicated","to_pool":"retail_select","gb":0.1}`)
req, _ := http.NewRequest("POST", "https://resifactory.net/api/v1/balance/convert", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/billing/topups
billing:write
Start a bandwidth top-up
Starts a purchase. By default returns a hosted checkout URL to open or forward; balance is credited when payment completes, so the top-up is always created as pending. Requires a key with a monthly spend cap. Idempotency-Key is required.
Accepts an Idempotency-Key header — required — see Idempotency.
Request body (JSON)
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
pool |
string | Yes | — | Pool id to top up, as returned by GET /pools. A pool id you cannot use returns the same 404 as one that does not exist. |
gb |
number | Yes | — | Amount to purchase, in GB. The charge is computed server-side from the pool's live price per GB — there is no amount parameter; do not compute or send one. Rejected with invalid_params if the resulting charge is below the $0.50 minimum. |
payment_method |
string | No | checkout | `checkout` (default) returns a hosted Stripe Checkout URL — works for every account. `saved_card` charges a card retained by a prior on-session purchase off-session; today this requires AutoBuy to be enabled on the account, which is not yet generally available, so it returns `card_not_available` for effectively every account. Use `checkout` unless you have confirmed otherwise via GET /me. |
discount_code |
string | No | — | Reserved for a future release — discount codes are not supported on this endpoint yet. The field stays part of the contract so a client already sending one is not broken once support ships, but a non-null value is rejected with invalid_params rather than silently accepted and ignored; omit it. |
Errors:
card_not_available (409), idempotency_key_reuse (409), insufficient_scope (403), internal_error (500), invalid_params (422), invalid_request (400), payment_failed (402), pool_not_available (404), rate_limited (429), spend_cap_exceeded (402), unauthorized (401)
— see Errors.
Example request
curl -X POST https://resifactory.net/api/v1/billing/topups \
-H "Authorization: Bearer rf_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"pool":"mobile_dedicated","gb":0.1}'
import requests
res = requests.post(
"https://resifactory.net/api/v1/billing/topups",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
json={
"pool": "mobile_dedicated",
"gb": 0.1
},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/billing/topups", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({"pool":"mobile_dedicated","gb":0.1})
});
const data = await res.json();
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
body := []byte(`{"pool":"mobile_dedicated","gb":0.1}`)
req, _ := http.NewRequest("POST", "https://resifactory.net/api/v1/billing/topups", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/billing/topups/{id}
billing:write
Top-up status
Poll a top-up until it reaches paid or failed. Uses the read rate budget, not the billing one, so polling never blocks a retry.
Path parameters
| Name | Notes |
|---|---|
id |
Top-up id, as returned by POST /billing/topups. |
Errors:
insufficient_scope (403), invalid_params (422), not_found (404), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/billing/topups/123 \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/billing/topups/123",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/billing/topups/123", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/billing/topups/123", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/isp/orders
isp:read
Your ISP orders and subnets
Lists your ISP proxy orders, newest first, with the /24 subnets each one holds. Carries no logins: use GET /isp/orders/{id}/ips for those.
Errors:
insufficient_scope (403), invalid_params (422), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/isp/orders \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/isp/orders",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/isp/orders", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/isp/orders", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/isp/orders/{id}/ips
isp:read
IPs of an ISP order, with logins
Every IP of one order as an endpoint line `host:port:username:password`. One login serves every IP of an order. `format=csv` and `format=txt` return a file download instead of JSON. Each call is a credential reveal and is recorded on your account, exactly as the dashboard's Show credentials is.
Path parameters
| Name | Notes |
|---|---|
id |
Order id, as returned by GET /isp/orders. |
Query parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
format |
string | No | json | `json` (default); `csv` with columns host, port, username, password, line; or `txt`, one endpoint line per row. |
Errors:
insufficient_scope (403), internal_error (500), invalid_params (422), not_found (404), order_not_active (409), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X GET https://resifactory.net/api/v1/isp/orders/123/ips \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/isp/orders/123/ips",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/isp/orders/123/ips", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/isp/orders/123/ips", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
/isp/orders/{id}/reset-password
isp:write
Reset an ISP order's password
Sets a new password on the order's login; the username stays. The proxies accept the new password within about a minute, and the old one stops working. Fetch GET /isp/orders/{id}/ips to read it; it is also emailed to your account address.
Accepts an Idempotency-Key header — see Idempotency.
Path parameters
| Name | Notes |
|---|---|
id |
Order id, as returned by GET /isp/orders. |
Errors:
idempotency_key_reuse (409), insufficient_scope (403), invalid_params (422), not_found (404), order_not_active (409), rate_limited (429), unauthorized (401)
— see Errors.
Example request
curl -X POST https://resifactory.net/api/v1/isp/orders/123/reset-password \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.post(
"https://resifactory.net/api/v1/isp/orders/123/reset-password",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/isp/orders/123/reset-password", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("POST", "https://resifactory.net/api/v1/isp/orders/123/reset-password", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
Pools & Targeting
Discover, never hardcodeResiFactory operates multiple proxy pools. Each pool has its own host, balance, and set of supported proxy types and countries. Call GET /pools at startup to discover what is currently accessible to your account — never hardcode pool IDs or targeting values, and never assume one pool's country list applies to another.
Proxy types
- rotating — New IP on every request. Ideal for large-scale scraping.
- sticky — Same IP for a configurable session window (1–1,440 min; up to 120 min on Essential).
- mobile_rotating — Mobile exit IPs, rotates per request.
- mobile_sticky — Mobile exit IPs with a session window.
Session duration
Applies only to sticky and mobile_sticky types. Value is in minutes.
- Minimum: 1 minute
- Maximum: 1,440 minutes (24 hours); on Essential a session holds at most 120 minutes
- Default (if omitted): pool default
Rotating types silently ignore this field.
Geographic targeting
- country — ISO 3166-1 alpha-2, lowercase (e.g.
us,gb) - state — US only; the full lowercase state name (e.g.
california), not a postal abbreviation
Country and state lists are set per pool, not globally — read each pool's own targeting.countries / targeting.us_states from GET /pools rather than assuming every pool supports the same list.
state) only applies when country resolves to us. An unrecognized state is silently dropped, not an error — even in strict mode.Output Formats
Match your toolThe format field on POST /proxies controls how credentials are serialized in the response. Choose the format that best matches your tool's import requirements.
list (default)
proxies is a single newline-delimited string. Each line is host:port:username:password. Split on \n and filter empty strings.
"proxies": "premium.resifactory.net:7777:ResiFactory_a3f9c142~us~1a2b3c4d:pass\npremium.resifactory.net:7777:ResiFactory_a3f9c142~us~9f8e7d6c:pass"
json
proxies is a JSON array of objects. Easiest to work with programmatically — each entry has host, port, username, and password fields.
"proxies": [
{
"host": "premium.resifactory.net",
"port": 7777,
"username": "ResiFactory_a3f9c142~us~1a2b3c4d",
"password": "pass"
}
]
userpass
proxies is a newline-delimited string in user:pass@host:port format. Useful for tools that expect credentials before the host.
"proxies": "ResiFactory_a3f9c142~us~1a2b3c4d:[email protected]:7777\nResiFactory_a3f9c142~us~9f8e7d6c:[email protected]:7777"
Strict Mode
RF-Strict: 1By default the API is lenient: an invalid or out-of-range parameter is coerced to its default or nearest valid value rather than failing the request. Send RF-Strict: 1 to opt out of that and have invalid parameters rejected instead.
Lenient (default)
An invalid parameter is coerced and the request still succeeds. The response adds a Warning header naming the coerced field(s), plus Deprecation: true and a Sunset date — silent coercion is being phased out. The exact sunset date is also in GET /status's deprecations array.
Strict
With RF-Strict: 1, the same problem is a 422 invalid_params instead, with a details array naming each rejected field, why, and what was sent:
{
"error": {
"code": "invalid_params",
"message": "One or more parameters are invalid.",
"details": [
{ "field": "quantity", "reason": "above_maximum", "got": 900000 }
]
}
}
RF-API-Version
Every response — success or error — carries an RF-API-Version header: the date-stamped behaviour version it was generated under. Pin your integration's expectations against it.
RF-Strict: 1 on every request — silent coercion exists for callers who predate it, not as the recommended behavior.Idempotency
Idempotency-Key headerSend an Idempotency-Key header — any string you generate, unique per logical operation — on a supporting endpoint so a retried request (timeout, dropped connection, anything) cannot double-apply. Replaying the same key with the same request body returns the original response again, unchanged, with an Idempotency-Replayed: true header. Replaying the same key with a different body is refused with 409 idempotency_key_reuse — that combination means the key was reused for a different operation, not retried. The same applies to the same key sent to a different order's URL.
Scope
A key is scoped to your API key and the specific endpoint — the same Idempotency-Key string is safe to reuse for two different operations on two different endpoints.
What gets cached
Only a successful (2xx) response is cached against the key. A failed attempt leaves the key retryable — fix the request and send it again with the same key.
Where it applies
POST /proxiesPOST /balance/convertPOST /billing/topups— requiredPOST /isp/orders/{id}/reset-password
Pagination
Cursor-basedList endpoints that can return more rows than fit in one response are cursor-paginated: pass limit to size a page, and cursor to fetch the next one. There is no page-number or offset parameter — the cursor is the only supported way to page forward.
Reading a page
Every paginated response includes has_more and next_cursor. When has_more is true, pass next_cursor back as the cursor query parameter to fetch the next page. When it's false, next_cursor is null — you're on the last page.
The cursor is opaque
Treat next_cursor as an opaque token — store and replay it verbatim. Its internal encoding is not part of the contract and may change without notice.
Where it applies
GET /usage/domainsGET /transactionsGET /generations
Errors
One envelopeEvery error response shares one envelope. request_id echoes the response's X-Request-Id header — quote it in support requests. details is present only on 422 invalid_params (see Strict mode).
{
"error": {
"code": "insufficient_scope",
"message": "This key does not have the proxies:generate scope.",
"request_id": "req_a1b2c3d4"
}
}
| Error code | HTTP status | Meaning |
|---|---|---|
unauthorized |
401 | Missing, malformed, or revoked API key. |
account_disabled |
403 | Your account has been suspended. |
insufficient_scope |
403 | The key used for this request does not have the scope this endpoint requires. |
not_found |
404 | No resource exists at this id, or it does not belong to your account. |
pool_not_available |
404 | The pool id is not available to your account, or does not exist. |
invalid_request |
400 | The request could not be performed as sent — see `message` for the specific reason. |
invalid_params |
422 | A parameter failed strict validation. Only returned when the request sends RF-Strict: 1 — otherwise the same problem is silently coerced instead. |
invalid_country |
422 | The `country` value is not a supported ISO 3166-1 alpha-2 code. Call GET /pools for the list a pool accepts. |
country_not_available |
422 | This pool cannot serve the requested `country`. Call GET /pools for the list it accepts. |
rate_limited |
429 | You have exceeded the rate limit for this endpoint's group. Read Retry-After and back off. |
too_many_keys |
409 | You already hold the maximum of 10 API keys. Returned by key creation in the dashboard Developer tab, not by any /api/v1 endpoint. |
idempotency_key_reuse |
409 | The Idempotency-Key header was reused with a request body that does not match the original request — see Idempotency. |
idempotency_in_progress |
409 | |
spend_cap_exceeded |
402 | This key has no monthly spend cap, or this request would exceed it. Create a new key with a higher (or any) cap. |
card_not_available |
409 | No saved card is available for an off-session charge on this account. Use payment_method "checkout" instead. |
payment_failed |
402 | The charge attempt was declined. See `message` for the reason; a card requiring authentication should retry via payment_method "checkout". |
order_not_active |
409 | This order is not active, so it holds no IPs. Only an active or past-due order can be revealed or reset. |
client_not_allowed |
403 | |
internal_error |
500 | The request could not be completed due to a server-side error. Safe to retry. |
server_error |
503 |
On a 429 rate_limited response, read the Retry-After header and back off — see Rate Limits for the per-group budgets and a backoff example.
Rate Limits
Per key · 60 s windowRate limits are enforced per API key over a fixed 60-second window, not a rolling one — the count resets at the top of each window rather than sliding continuously, so a burst of requests that straddles a window boundary can briefly exceed the stated per-minute rate by up to roughly 2x. Endpoints are grouped by cost so heavy proxy generation or a billing write cannot starve read endpoints.
| Endpoint group | Endpoints | Limit |
|---|---|---|
| Generate |
POST /proxies
|
20 / min |
| Billing |
POST /balance/convert, POST /billing/topups
|
6 / min |
| ISP logins (per account) |
GET /isp/orders/{id}/ips, POST /isp/orders/{id}/reset-password
|
10 / min |
| Reads |
GET /me, GET /balance, GET /usage, GET /pools, GET /pools/{id}, GET /status, GET /usage/daily, GET /usage/domains, GET /transactions, GET /generations, GET /billing/topups/{id}, GET /isp/orders
|
120 / min |
Response headers
Every response includes rate-limit headers so you can track consumption without hitting the limit:
| Header | Description |
|---|---|
RateLimit-Limit | Maximum requests allowed in the current window. |
RateLimit-Remaining | Requests remaining before you hit the limit. |
RateLimit-Reset | Seconds remaining until the window resets (not a Unix timestamp). |
Retry-After | Seconds to wait before retrying. Present only on 429 responses. |
Handling 429
When you receive a 429 rate_limited response, read the Retry-After header and wait that many seconds before retrying. Use exponential backoff with jitter for burst scenarios:
// Pseudo-code: exponential backoff with jitter
async function callWithBackoff(fn, maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await fn();
if (result.status !== 429) return result;
const retryAfter = parseInt(result.headers['retry-after'] || '1', 10);
const jitter = Math.random() * 1000;
await sleep((retryAfter * 1000 + jitter) * Math.pow(2, attempt));
}
throw new Error('Max retry attempts exceeded');
}
Code Examples
Pick your stackThe two most common operations — generating proxies and reading balance. Switch the language tab to match your stack.
Generate proxies
curl -X POST https://resifactory.net/api/v1/proxies \
-H "Authorization: Bearer rf_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"country":"us","quantity":50,"proxyType":"rotating","format":"json"}'
import requests
res = requests.post(
"https://resifactory.net/api/v1/proxies",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
json={
"country": "us",
"quantity": 50,
"proxyType": "rotating",
"format": "json"
},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/proxies", {
method: "POST",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({"country":"us","quantity":50,"proxyType":"rotating","format":"json"})
});
const data = await res.json();
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
body := []byte(`{"country":"us","quantity":50,"proxyType":"rotating","format":"json"}`)
req, _ := http.NewRequest("POST", "https://resifactory.net/api/v1/proxies", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
Read balance
curl -X GET https://resifactory.net/api/v1/balance \
-H "Authorization: Bearer rf_live_YOUR_KEY"
import requests
res = requests.get(
"https://resifactory.net/api/v1/balance",
headers={"Authorization": "Bearer rf_live_YOUR_KEY"},
)
print(res.json())
const res = await fetch("https://resifactory.net/api/v1/balance", {
method: "GET",
headers: {
Authorization: "Bearer rf_live_YOUR_KEY"
}
});
const data = await res.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://resifactory.net/api/v1/balance", nil)
req.Header.Set("Authorization", "Bearer rf_live_YOUR_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
Versioning & Changelog
/api/v1 is frozenThe current API version is v1, served at /api/v1/. This path is meant to stay non-breaking — normally, a breaking change (removed field, changed semantics, altered authentication) ships under a new version path (e.g. /api/v2) with advance notice and a deprecation period, rather than under /api/v1 itself. There is one documented exception below: the balances type change dated 2026-08-06, marked Breaking rather than glossed over as routine, and mitigated by keeping the pre-change balances_gb shape live until its stated Sunset.
What counts as breaking
- Removing or renaming a response field
- Changing a field's type
- Changing an error code or HTTP status
- Removing an endpoint
- Requiring a new mandatory parameter
What is not breaking
- Adding new optional response fields
- Adding new optional request parameters
- Adding new endpoints
- Adding new error codes for new conditions
Changelog
Essential sticky sessions · November 2026
Essential holds a sticky session for at most 120 minutes, so a longer sessionDuration on Essential is now set to 120. POST /proxies returns session_duration 120 for such a batch, and GET /pools and GET /pools/{id} report session_minutes.max 120 for Essential. Premium and Mobile still hold a session for 1 to 1,440 minutes. Nothing is refused that was accepted before, and no field changed its name or type. GET /generations keeps the length that was asked for on batches made before this change, so an older Essential batch can report more than 120.
ISP proxies · November 2026
New endpoints: GET /isp/orders and GET /isp/orders/{id}/ips (isp:read), and POST /isp/orders/{id}/reset-password (isp:write). Both scopes are opt-in. GET /isp/orders/{id}/ips returns every IP of an order with its login as JSON, or as a CSV or text download with format=csv or format=txt. Every call to it is recorded on your account, the same as Show credentials in the dashboard. The reveal and the reset share a budget of 10 requests a minute per account, across all of its keys. limits on GET /me and GET /status gains isp_per_min.
Pool detail, status, usage history, billing, idempotency & pagination · August 2026
New endpoints: GET /pools/{id}, GET /status, GET /usage/daily, GET /usage/domains, GET /transactions, and GET /generations (usage:read); POST /balance/convert, POST /billing/topups, and GET /billing/topups/{id} behind a new billing:write scope. GET /usage is deprecated in favor of GET /usage/domains (date ranges, pagination) — it keeps working, adding an unbounded top-25 was never breaking. List endpoints are cursor-paginated (see Pagination). Mutating endpoints accept an Idempotency-Key header (see Idempotency), required on POST /billing/topups. Send RF-Strict: 1 to opt into strict parameter validation (see Strict mode) ahead of it becoming the default.
Breaking: balances on GET /balance/GET /me changed shape — it used to map pool id to a bare number (GB remaining); it now maps pool id to { gb, price_per_gb, currency }. If your integration reads balances[poolId] as a number, this breaks the day this deploys. Migrate to balances[poolId].gb, or keep reading the unchanged numeric shape from the deprecated balances_gb[poolId] until its Sunset date if you can't migrate immediately. Also breaking: the carrier body parameter and the socks5 protocol value are no longer accepted on POST /proxies. carrier-network targeting was withdrawn from the platform on 2026-07-22, and the parameter had been silently ignored since — removing it changes nothing about what a working integration does today. Nothing served socks5 at that time, so a proxy line generated with it could never actually connect; it returned on 2026-10-23 on port 1080, on every pool except Essential. The https protocol value was removed the same day for an unrelated reason — it wasn't served on every relay yet — and RESTORED 2026-08-06 by RF-16 once every relay served :7443, then withdrawn on 2026-08-31 (below).
Breaking, 2026-08-31 — https withdrawn: https is no longer accepted by POST /proxies. http (port 7777) is accepted on every pool, and since 2026-10-23 socks5 (port 1080) on every pool except Essential. The 2026-08-06 restore above was verified across the whole fleet on the day it shipped. On 2026-08-18 we rebuilt that fleet on new hardware, and the rebuild never installed the TLS certificate or opened the firewall rule the :7443 endpoint requires — so it stopped being reachable that day, while this page continued to advertise it for 13 days. Lines generated with https in that window could not connect. We have withdrawn the value rather than restore an endpoint whose certificates we cannot yet renew automatically. Migration: send protocol: "http" (or "socks5" on any pool except Essential) or omit the field — it defaults to http. Callers using the default lenient validation are unaffected: an unrecognised value falls back to http. Strict-mode callers sending protocol: "https" will now get 422 not_in_enum instead of 200. That is the only breaking case.
Initial release · June 17, 2026
Five endpoints — POST /proxies (generation), GET /balance + GET /usage (usage reads), GET /pools (pool discovery), and GET /me (identity) — across three scopes: proxies:generate, usage:read, pools:read. API keys are created and revoked only from the dashboard Developer tab — the API never manages keys, so a leaked key can never mint another. Request examples ship in cURL, Python, JavaScript, and Go, alongside a machine-readable OpenAPI 3.1 spec.
FAQ
Common answersI called POST /proxies but got demo credentials back. What happened?
Your account balance is zero for the requested pool. The API returns non-working demo credentials and sets is_demo: true in the response rather than returning an error. Check the balance with GET /balance and top up from the dashboard. Once you have balance, subsequent calls return live credentials.
I'm getting 401 unauthorized even though I copied the key correctly.
The most common causes are: (1) the key was revoked from the dashboard; (2) there is whitespace or a newline appended to the key string; (3) the Authorization header value is missing the Bearer prefix (note the trailing space). Call GET /me as a quick sanity check — if it returns 401, the key is definitively invalid.
I'm getting 403 insufficient_scope. How do I fix it?
Scopes are baked into a key at creation time and cannot be changed. Create a new key with the required scope in the dashboard Developer tab. Check what scopes a key has using GET /me.
How do I handle 429 rate limit errors in production?
Read the Retry-After header from the 429 response — it tells you the exact number of seconds to wait. Implement exponential backoff with jitter: after the first 429, wait Retry-After seconds; after the second, wait double; and so on. For the proxy-generation endpoint (20/min limit), batching all required proxies into a single large POST /proxies call (up to 250,000 lines) is more efficient than many small calls.
My balance dropped unexpectedly. Can I see where the GB went?
Call GET /usage?limit=100 to see the top domains by byte consumption for your account. This gives a per-domain breakdown of traffic. If usage looks unexpected, check whether your proxy credentials have been shared or leaked — if so, revoke the affected key immediately and generate fresh credentials.
Do sticky sessions actually hold the same IP for the full duration?
Yes, for the duration you specify (1–1,440 minutes), except on Essential, where a session holds at most 120 minutes. Each unique username in the returned credential list corresponds to a pinned session. Requests made with the same username:password pair will exit from the same IP until the session expires. Once a session expires, the next request on that credential rotates to a new IP automatically.
Can I have more than 10 API keys?
No — the limit is 10 keys per account. If you hit the limit, delete unused keys from the dashboard Developer tab to free slots. If you have a legitimate need for more keys (e.g. many separate services), contact support via Discord.
Questions? Reach us on Discord, or grab the machine-readable OpenAPI spec.