Reseller API

The Reseller API is a separate, money-moving surface for partners who resell ISP proxies at volume: it holds a prepaid USD balance, a purchasable catalog of ISP plans, an order/fulfillment pipeline, and signed webhooks — all independent of the ordinary dashboard account it is attached to. It is not the same thing as "reselling" via sub-users (see the reselling guide) — that model splits a residential traffic pool across sub-users on the shared gateway; this one buys and holds discrete, static IP:port ISP proxies against a running balance. Every endpoint below (except applying) returns the standard {success, payload, meta, errors, description} envelope described on the ISP Proxies API page.

Every endpoint on this page requires an active, API-enabled reseller account and a reseller-scoped token. The master switch, the token's reseller ability, the resellers row and its api_enabled flag are checked in that order — the first one that fails short-circuits the rest. All four failure modes return the exact same 403 body on purpose (so a caller cannot probe which reason applies):
{
  "success": false,
  "payload": null,
  "errors": { "reseller": "reseller_disabled" },
  "description": "Reseller API is not available for this account"
}
An unauthenticated request (missing/invalid bearer token) is a plain 401 before any of this runs.
Before you integrate, read these — they are not obvious from the endpoint list:
  • An IP can never be returned or cancelled before its term ends. Neither upstream provider supports it. See No Early Returns.
  • max_buy_qty is 1 in production. A single order can only ever cover one proxy — buying N means N separate orders with N separate idempotency keys. See Create an Order.
  • Ordering is asynchronous. POST /orders returns status: "provisioning", never credentials. See Order Lifecycle.
  • Webhooks are a convenience, not a source of truth. A partner that never configures one still works correctly by polling. See Webhooks.
  • The Idempotency-Key header has three outcomes on both POST /orders and POST /proxies/{proxy}/renew: same key + same body replays with 200; same key + different body (or a different target proxy, for renewals) is 409; a missing header is 422. See Idempotency.
  • Clearing notes or external_ref: send "" or null — both clear the field, and it always reads back as null, never "". See Update a Proxy.
  • There is no city-level targeting anywhere in this API. ISP plans are sold by country and ISP name only — the catalog and order endpoints have no city parameter, and this is not a gap you can work around: the residential gateway's own -city- username token is unreliable in production too. Do not advertise city selection to your customers for ISP proxies.

Applying for Partner Access

Reseller access is not self-service. Any authenticated ProxyHat account may apply; a human reviews the application and flips the account to active with api_enabled: true before any other endpoint on this page will admit it. Calling this endpoint twice does not create a duplicate — it returns the existing application.

POST /v1/reseller/apply Requires Auth

Apply for Partner Access

Apply for Partner Access

Submit a reseller application for the authenticated account. Requires only a valid bearer token — no reseller row, no reseller-scoped ability. A second call from an account that has already applied returns the existing application (200) instead of creating another.

Example request:
Request Body
Name Type Required Description
name string Required Your company or brand name, max 191 characters.
website string (url) Optional Your website, max 500 characters.
expected_volume string Optional Free-text description of expected monthly volume. Recorded only in the internal review alert — there is no column for it, so it is never echoed back by any endpoint.
curl -X POST https://api.proxyhat.com/v1/reseller/apply \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"name": "Acme Proxies", "website": "https://acmeproxies.com", "expected_volume": "500 IPs/month"}'
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/reseller/apply",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={
        "name": "Acme Proxies",
        "website": "https://acmeproxies.com",
        "expected_volume": "500 IPs/month",
    },
)

print(response.json())
const response = await fetch("https://api.proxyhat.com/v1/reseller/apply", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({
    name: "Acme Proxies",
    website: "https://acmeproxies.com",
    expected_volume: "500 IPs/month",
  }),
});

console.log(await response.json());
body, _ := json.Marshal(map[string]string{
    "name":            "Acme Proxies",
    "website":         "https://acmeproxies.com",
    "expected_volume": "500 IPs/month",
})

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/apply", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out map[string]interface{}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out)
What happens next. The application starts at status: "pending" with api_enabled: false — every other endpoint on this page still refuses the account at this point. There is no polling endpoint for application status; ProxyHat contacts you once the account is reviewed and activated. Once active, mint a reseller-scoped bearer token with POST /v1/profile/api-keys (see the Profile & API Keys page), passing "abilities": ["reseller"] (or a token with the full ["*"] ability also works) — then use that token as the Authorization: Bearer value on every call below.

Account & Balance

GET /v1/reseller/account Requires Auth

Get Reseller Account

Get Reseller Account

Return your reseller account status, prepaid balance, discount rate, low-balance threshold and webhook configuration state (never the secret itself). meta carries the two numbers you need to size an integration: max_buy_qty and your per-minute rate limit.

No input needed — runs live with your key
curl https://api.proxyhat.com/v1/reseller/account \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/account",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

account = response.json()["payload"]
print(f"{account[\"name\"]}: ${account[\"balance_usd\"]} ({account[\"status\"]})")
const response = await fetch("https://api.proxyhat.com/v1/reseller/account", {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

const { payload, meta } = await response.json();
console.log(`${payload.name}: $${payload.balance_usd} (${payload.status})`);
console.log("max per order:", meta.max_buy_qty, "rate limit/min:", meta.rate_limit_per_minute);
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/account", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
    Meta    map[string]interface{} `json:"meta"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["name"], out.Payload["balance_usd"], out.Payload["status"])

Ledger

GET /v1/reseller/ledger Requires Auth

List Ledger Entries

List Ledger Entries

Every balance movement on your account — deposits (top-ups), purchases, renewals, refunds and manual adjustments — newest first, paginated. Each row carries balance_after_usd as of that entry, which is why two rows never report the same running balance even when read back-to-back.

We filled these in for you: per_page

Tweak any value if you like — or just press Try.

Query Parameters
Name Type Required Description
type string Optional Filter to one entry type: deposit, purchase, renewal, refund, or adjustment.
from string (date) Optional Only entries created on or after this date.
to string (date) Optional Only entries created on or before this date.
per_page integer Optional 1-200, default 50.
curl "https://api.proxyhat.com/v1/reseller/ledger?type=purchase&per_page=20" \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/ledger",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
    params={"type": "purchase", "per_page": 20},
)

for row in response.json()["payload"]["data"]:
    print(row["type"], row["amount_usd"], "->", row["balance_after_usd"])
const response = await fetch(
  "https://api.proxyhat.com/v1/reseller/ledger?type=purchase&per_page=20",
  { headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" } },
);

const { payload } = await response.json();
payload.data.forEach(row => console.log(row.type, row.amount_usd, "->", row.balance_after_usd));
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/ledger?type=purchase&per_page=20", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload struct {
        Data []map[string]interface{} `json:"data"`
    } `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, row := range out.Payload.Data {
    fmt.Println(row["type"], row["amount_usd"], "->", row["balance_after_usd"])
}
Debits are negative. A purchase or renewal reports a negative amount_usd; a deposit or refund reports a positive one. reference is null for a manual admin adjustment and otherwise points back at the order or top-up that caused the entry, so you can join without guessing.

Catalog

GET /v1/reseller/catalog Requires Auth

Browse the Catalog

Browse the Catalog

Every active, purchasable ISP plan, ordered by country then term length, priced at your reseller rate. Internal cost and provider identifiers are never included — this endpoint is audited specifically to never leak them.

No input needed — runs live with your key
curl https://api.proxyhat.com/v1/reseller/catalog \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/catalog",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

data = response.json()
print("max per order:", data["meta"]["max_buy_qty"], "balance:", data["meta"]["balance_usd"])
for plan in data["payload"]:
    print(f"{plan[\"isp_name\"]} ({plan[\"country_code\"]}) ${plan[\"price\"]} / {plan[\"days\"]}d")
const response = await fetch("https://api.proxyhat.com/v1/reseller/catalog", {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

const { payload, meta } = await response.json();
console.log("max per order:", meta.max_buy_qty, "balance:", meta.balance_usd);
payload.forEach(plan => console.log(`${plan.isp_name} (${plan.country_code}) $${plan.price} / ${plan.days}d`));
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/catalog", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload []map[string]interface{} `json:"payload"`
    Meta    struct {
        MaxBuyQty  int     `json:"max_buy_qty"`
        BalanceUsd float64 `json:"balance_usd"`
    } `json:"meta"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, plan := range out.Payload {
    fmt.Printf("%v (%v) $%v / %vd\n", plan["isp_name"], plan["country_code"], plan["price"], plan["days"])
}
GET /v1/reseller/catalog/{id} Requires Auth

Get a Catalog Plan

Get a Catalog Plan

Retrieve a single active plan by its UUID, for an order-confirmation screen. 404 if the plan does not exist, is inactive, or the id is not a valid UUID (a malformed id never reaches the database, so this is always a clean 404, never a 500).

We filled these in for you: id

Tweak any value if you like — or just press Try.

Path Parameters
Name Type Required Description
id string (uuid) Required The plan id from a catalog listing.
curl https://api.proxyhat.com/v1/reseller/catalog/8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84 \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

plan_id = "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84"
response = requests.get(
    f"https://api.proxyhat.com/v1/reseller/catalog/{plan_id}",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

print(response.json()["payload"])
const planId = "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84";

const response = await fetch(`https://api.proxyhat.com/v1/reseller/catalog/${planId}`, {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

console.log((await response.json()).payload);
planID := "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84"
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/catalog/"+planID, nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload)
compare_at is null, not omitted, when there is nothing to strike through. Always check for null rather than treating a missing key as "no discount" — the key is always present. traffic_limit: 0 means unmetered, the same convention as the public ISP store catalog. There is no city, region or ZIP parameter anywhere in this endpoint — plans are scoped to country and ISP name only.

Orders

POST /v1/reseller/orders/quote Requires Auth

Quote an Order

Quote an Order

Price an order without creating anything or touching your balance — no Idempotency-Key needed. quantity is silently clamped to meta.max_buy_qty (1 in production) before pricing, exactly as an actual order would be, so the quoted total always matches what a real order costs.

Example request: plan_id quantity
Request Body
Name Type Required Description
plan_id string (uuid) Required A catalog plan id.
quantity integer Optional Defaults to 1. Clamped to max_buy_qty, never rejected for being too high.
curl -X POST https://api.proxyhat.com/v1/reseller/orders/quote \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"plan_id": "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84", "quantity": 1}'
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/reseller/orders/quote",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={"plan_id": "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84", "quantity": 1},
)

quote = response.json()["payload"]
print(f"total ${quote[\"total_usd\"]}, balance after ${quote[\"balance_after_usd\"]}")
const response = await fetch("https://api.proxyhat.com/v1/reseller/orders/quote", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ plan_id: "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84", quantity: 1 }),
});

const { payload } = await response.json();
console.log(`total $${payload.total_usd}, balance after $${payload.balance_after_usd}`);
body, _ := json.Marshal(map[string]interface{}{
    "plan_id":  "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84",
    "quantity": 1,
})

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/orders/quote", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["total_usd"], out.Payload["balance_after_usd"])

Create an Order

POST /v1/reseller/orders Requires Auth

Create an Order

Create an Order

Debit your balance and queue a purchase. Returns immediately with status:"provisioning" — never credentials. Requires an Idempotency-Key header (see below). external_ref is your own opaque identifier for this order (e.g. your customer/order id); it is echoed back on every read and included in the order.fulfilled webhook.

Example request: plan_id quantity external_ref
Request Body
Name Type Required Description
plan_id string (uuid) Required A catalog plan id.
quantity integer Optional Defaults to 1. Silently clamped to max_buy_qty (1 in production) — requesting more never errors, it just buys fewer than you asked for. See "Buying more than one" below.
external_ref string Optional Your own reference for this order, max 191 characters.
auto_renew boolean Optional When true, the resulting proxy is enrolled in the nightly auto-renew sweep once fulfilled. Debits your balance again at each renewal — never creates a card subscription.
curl -X POST https://api.proxyhat.com/v1/reseller/orders \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: order-cust_10293-1" \
  -d '{"plan_id": "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84", "quantity": 1, "external_ref": "cust_10293", "auto_renew": false}'
import requests
import uuid

idempotency_key = str(uuid.uuid4())  # generate ONCE per logical purchase, then reuse on retry

response = requests.post(
    "https://api.proxyhat.com/v1/reseller/orders",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "plan_id": "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84",
        "quantity": 1,
        "external_ref": "cust_10293",
        "auto_renew": False,
    },
)

order = response.json()["payload"]
print(order["id"], order["status"])  # status is "provisioning" here, not a live proxy yet
import { randomUUID } from "crypto";

const idempotencyKey = randomUUID(); // generate ONCE per logical purchase, then reuse on retry

const response = await fetch("https://api.proxyhat.com/v1/reseller/orders", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify({
    plan_id: "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84",
    quantity: 1,
    external_ref: "cust_10293",
    auto_renew: false,
  }),
});

const { payload } = await response.json();
console.log(payload.id, payload.status); // "provisioning" — poll or wait for the webhook
idempotencyKey := uuid.NewString() // generate ONCE per logical purchase, then reuse on retry

body, _ := json.Marshal(map[string]interface{}{
    "plan_id":      "8d2e1f4a-6b3c-4e9d-a12f-7c5b0e3d9a84",
    "quantity":     1,
    "external_ref": "cust_10293",
    "auto_renew":   false,
})

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/orders", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["id"], out.Payload["status"])
Buying more than one proxy. max_buy_qty is 1 in production — both upstream ISP providers report stock as a plain in-stock/out-of-stock boolean, not a count, so an order for N > 1 can never be validated against real availability in advance. Sending "quantity": 5 does not error: the order is silently created for 1 unit and payload.quantity reports the clamped value. To buy N proxies, issue N separate POST /orders calls, each with its own Idempotency-Key.
The Idempotency-Key contract — three outcomes. Generate one key per logical purchase (a UUID is fine) and reuse it only when retrying that exact same purchase:
  • Same key, same body → 200, replayed. The original order is returned unchanged and nothing is debited a second time. Safe to retry blindly on a timeout.
  • Same key, different body → 409. { "success": false, "payload": null, "errors": { "idempotency_key": "idempotency_conflict" }, "description": "This Idempotency-Key was used for a different order" }. Reusing a key for a genuinely different order (different plan, quantity, external_ref, or even just a flipped auto_renew) is refused rather than silently returning the old order or double-charging — pick a new key.
  • Missing header → 422. { "success": false, "payload": null, "errors": { "Idempotency-Key": "The Idempotency-Key header is required." }, "description": "Validation failed" }. This check runs after ordinary field validation, so a request that also has an invalid plan_id gets Laravel's native 422 validation shape instead — see Error Handling for the two 422 shapes.
The identical three-outcome contract applies to POST /proxies/{id}/renew, where "different body" means "a key reused against a different proxy."
GET /v1/reseller/orders Requires Auth

List Orders

List Orders

Your own orders, newest first, paginated. There is no filter by kind (purchase vs renewal) — filter client-side on payload.data[].kind if you need it.

No input needed — runs live with your key
Query Parameters
Name Type Required Description
status string Optional pending, provisioning, fulfilled, failed, or refunded.
external_ref string Optional Exact match on your own reference.
from string (date) Optional Only orders created on or after this date.
to string (date) Optional Only orders created on or before this date.
per_page integer Optional 1-200, default 50.
curl "https://api.proxyhat.com/v1/reseller/orders?external_ref=cust_10293" \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/orders",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
    params={"external_ref": "cust_10293"},
)

for order in response.json()["payload"]["data"]:
    print(order["id"], order["status"])
const response = await fetch(
  "https://api.proxyhat.com/v1/reseller/orders?external_ref=cust_10293",
  { headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" } },
);

const { payload } = await response.json();
payload.data.forEach(order => console.log(order.id, order.status));
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/orders?external_ref=cust_10293", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload struct {
        Data []map[string]interface{} `json:"data"`
    } `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, order := range out.Payload.Data {
    fmt.Println(order["id"], order["status"])
}
GET /v1/reseller/orders/{id} Requires Auth

Get an Order

Get an Order

Poll this endpoint after creating an order until status leaves "provisioning" — this is the authoritative fallback if you do not configure a webhook, or if a webhook delivery is delayed or lost. 404 for another reseller's order, or for a malformed (non-UUID) id.

We filled these in for you: id

Tweak any value if you like — or just press Try.

Path Parameters
Name Type Required Description
id string (uuid) Required The order id returned by Create an Order.
curl https://api.proxyhat.com/v1/reseller/orders/2c5e9a1f-7b34-4d8e-a1c6-9f0b2d3e4a5c \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import time
import requests

order_id = "2c5e9a1f-7b34-4d8e-a1c6-9f0b2d3e4a5c"
H = {"Authorization": "Bearer __API_KEY__", "Accept": "application/json"}

# Authoritative polling fallback: works with zero webhook configuration.
while True:
    order = requests.get(f"https://api.proxyhat.com/v1/reseller/orders/{order_id}", headers=H).json()["payload"]
    if order["status"] != "provisioning":
        break
    time.sleep(15)

if order["status"] == "fulfilled":
    proxy = order["proxies"][0]
    print(proxy["ip"], proxy["port"], proxy["login"], proxy["password"])
else:
    print("order did not fulfill:", order["status"], order["error"])
const orderId = "2c5e9a1f-7b34-4d8e-a1c6-9f0b2d3e4a5c";
const H = { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" };
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// Authoritative polling fallback: works with zero webhook configuration.
let order;
do {
  const res = await fetch(`https://api.proxyhat.com/v1/reseller/orders/${orderId}`, { headers: H });
  order = (await res.json()).payload;
  if (order.status === "provisioning") await sleep(15000);
} while (order.status === "provisioning");

if (order.status === "fulfilled") {
  const [proxy] = order.proxies;
  console.log(proxy.ip, proxy.port, proxy.login, proxy.password);
} else {
  console.log("order did not fulfill:", order.status, order.error);
}
orderID := "2c5e9a1f-7b34-4d8e-a1c6-9f0b2d3e4a5c"

var order map[string]interface{}
for {
    req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/orders/"+orderID, nil)
    req.Header.Set("Authorization", "Bearer __API_KEY__")
    req.Header.Set("Accept", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    var out struct {
        Payload map[string]interface{} `json:"payload"`
    }
    json.NewDecoder(resp.Body).Decode(&out)
    resp.Body.Close()
    order = out.Payload

    if order["status"] != "provisioning" {
        break
    }
    time.Sleep(15 * time.Second)
}
fmt.Println(order["status"])

Order Lifecycle & Refunds

An order moves through pendingprovisioning → (fulfilled | failed) → optionally refunded. Your balance is debited the instant the order is created — before anything is provisioned — so fulfilled_at, not the debit, is the signal that credentials exist.

A renewal order's proxies array is always empty — even once fulfilled. Renewing extends the term of an existing IspProxy row in place; it never creates a new one, so nothing ever attaches to reseller_order_id on a renewal order. To read the renewed proxy's new expires_at, fetch it directly with GET /proxies/{id} using the id you renewed — do not expect to find it on the order.

Proxies

An ISP proxy can never be cancelled or returned before its term ends. There is no DELETE endpoint and none is planned — neither upstream provider (CyberYozh or NodeMaven ISP) exposes an early-termination operation, so ProxyHat cannot offer one either. Setting auto_renew: false only stops the next renewal; the proxy remains yours, billed and usable, until expires_at. There is no partial refund for unused days on a proxy that was actually delivered — the only refund path (see Order Lifecycle) is for an order that was never fulfilled at all.
GET /v1/reseller/proxies Requires Auth

List My Proxies

List My Proxies

Every ISP proxy currently assigned to you, with full live credentials, filterable by external reference, country, active/expired status, or an expiring-soon horizon.

We filled these in for you: expiring_within_days

Tweak any value if you like — or just press Try.

Query Parameters
Name Type Required Description
external_ref string Optional Exact match on the reference you set at order time or via PATCH.
country string Optional 2-letter ISO country code.
status string Optional active or expired.
expiring_within_days integer Optional 1-365. Only proxies still active but expiring inside this window.
per_page integer Optional 1-200, default 50.
curl "https://api.proxyhat.com/v1/reseller/proxies?expiring_within_days=7" \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/proxies",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
    params={"expiring_within_days": 7},
)

for proxy in response.json()["payload"]["data"]:
    print(proxy["ip"], proxy["expires_at"])
const response = await fetch(
  "https://api.proxyhat.com/v1/reseller/proxies?expiring_within_days=7",
  { headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" } },
);

const { payload } = await response.json();
payload.data.forEach(proxy => console.log(proxy.ip, proxy.expires_at));
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/proxies?expiring_within_days=7", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload struct {
        Data []map[string]interface{} `json:"data"`
    } `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, proxy := range out.Payload.Data {
    fmt.Println(proxy["ip"], proxy["expires_at"])
}
GET /v1/reseller/proxies/{id} Requires Auth

Get a Proxy

Get a Proxy

Fetch one proxy by its id, with full credentials. 404 for a foreign proxy or a malformed id.

We filled these in for you: id

Tweak any value if you like — or just press Try.

Path Parameters
Name Type Required Description
id string (uuid) Required The proxy id.
curl https://api.proxyhat.com/v1/reseller/proxies/7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

proxy_id = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"
response = requests.get(
    f"https://api.proxyhat.com/v1/reseller/proxies/{proxy_id}",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

print(response.json()["payload"])
const proxyId = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c";

const response = await fetch(`https://api.proxyhat.com/v1/reseller/proxies/${proxyId}`, {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

console.log((await response.json()).payload);
proxyID := "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/proxies/"+proxyID, nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload)
PATCH /v1/reseller/proxies/{id} Requires Auth

Update a Proxy

Update a Proxy

Write your own notes, your own external_ref, or flip auto_renew on an already-owned proxy. Never creates a Creem card subscription — a prepaid renewal is always debited from your balance, either via this proxy's own nightly auto-renew pass or an explicit POST /renew.

Example request: id
Path Parameters
Name Type Required Description
id string (uuid) Required The proxy id.
Request Body
Name Type Required Description
notes string Optional Max 2000 characters. Send "" or null to clear it — both do the same thing, and it always reads back null, never "".
external_ref string Optional Max 191 characters. Same clear-with-"" behavior as notes.
auto_renew boolean Optional Enroll or unenroll this proxy in the nightly auto-renew sweep.
curl -X PATCH https://api.proxyhat.com/v1/reseller/proxies/7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"notes": "assigned to customer 10293", "external_ref": "cust_10293", "auto_renew": true}'
import requests

proxy_id = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"
response = requests.patch(
    f"https://api.proxyhat.com/v1/reseller/proxies/{proxy_id}",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={"notes": "assigned to customer 10293", "external_ref": "cust_10293", "auto_renew": True},
)

print(response.json()["payload"])

# Clearing a field — "" and null are equivalent, both read back as null:
requests.patch(
    f"https://api.proxyhat.com/v1/reseller/proxies/{proxy_id}",
    headers={"Authorization": "Bearer __API_KEY__", "Content-Type": "application/json", "Accept": "application/json"},
    json={"notes": ""},
)
const proxyId = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c";

const response = await fetch(`https://api.proxyhat.com/v1/reseller/proxies/${proxyId}`, {
  method: "PATCH",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ notes: "assigned to customer 10293", external_ref: "cust_10293", auto_renew: true }),
});

console.log((await response.json()).payload);

// Clearing a field — "" and null are equivalent, both read back as null:
await fetch(`https://api.proxyhat.com/v1/reseller/proxies/${proxyId}`, {
  method: "PATCH",
  headers: { "Authorization": "Bearer __API_KEY__", "Content-Type": "application/json", "Accept": "application/json" },
  body: JSON.stringify({ notes: "" }),
});
proxyID := "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"

body, _ := json.Marshal(map[string]interface{}{
    "notes":        "assigned to customer 10293",
    "external_ref": "cust_10293",
    "auto_renew":   true,
})

req, _ := http.NewRequest("PATCH", "https://api.proxyhat.com/v1/reseller/proxies/"+proxyID, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload)
Why "" and null behave identically. A global Laravel middleware rewrites every incoming empty string to null before this controller (or any other in the app) ever sees the request body — by the time validation runs, "the client sent an empty string to clear this" and "the client sent an explicit null" are already indistinguishable. Both clear the column. Omitting the key entirely, by contrast, leaves the existing value untouched — the controller only writes a field that was actually present in the request body.
POST /v1/reseller/proxies/{id}/test Requires Auth

Test a Proxy

Test a Proxy

Live connectivity probe of one proxy on every protocol it serves (HTTP and/or SOCKS5), run server-side through the proxy itself. Shares its prober with the dashboard's own ISP proxy test, so the two surfaces can never disagree about the same IP.

Example request: id
Path Parameters
Name Type Required Description
id string (uuid) Required The proxy id.
curl -X POST https://api.proxyhat.com/v1/reseller/proxies/7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c/test \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

proxy_id = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"
response = requests.post(
    f"https://api.proxyhat.com/v1/reseller/proxies/{proxy_id}/test",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

result = response.json()
print(result["payload"]["protocols_ok"], "/", result["payload"]["protocols_total"], "protocols ok")
const proxyId = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c";

const response = await fetch(`https://api.proxyhat.com/v1/reseller/proxies/${proxyId}/test`, {
  method: "POST",
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

const { payload } = await response.json();
console.log(payload.protocols_ok, "/", payload.protocols_total, "protocols ok");
proxyID := "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"
req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/proxies/"+proxyID+"/test", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["protocols_ok"], "/", out.Payload["protocols_total"])
A failed protocol never fails the whole call. success is true as long as at least one protocol answered (protocols_ok > 0); a protocol that failed reports "ok": false with an error_code of either proxy_auth_failed or proxy_unreachable instead of the connectivity fields above. Calling this on an already-expired proxy skips the network probe entirely and returns 422 with errors.proxy: "proxy_expired" — no HTTP request is ever made to a dead IP.
POST /v1/reseller/proxies/{id}/renew Requires Auth

Renew a Proxy

Renew a Proxy

Extend an existing proxy's term from your balance. Requires an Idempotency-Key header with the same three-outcome contract as Create an Order, EXCEPT that "different body" here means "the same key was already used for a different proxy" — reusing a key against a second proxy is a 409, never a silent renewal of the first one instead.

Example request:
Path Parameters
Name Type Required Description
id string (uuid) Required The proxy id to renew.
curl -X POST https://api.proxyhat.com/v1/reseller/proxies/7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c/renew \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: renew-7c5b0e3d-2026-10-14"
import requests

proxy_id = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"
response = requests.post(
    f"https://api.proxyhat.com/v1/reseller/proxies/{proxy_id}/renew",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Accept": "application/json",
        # One key per (proxy, term) you intend to renew — never reuse across proxies.
        "Idempotency-Key": f"renew-{proxy_id}-2026-10-14",
    },
)

order = response.json()["payload"]
print(order["status"], order["total_usd"])
const proxyId = "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c";

const response = await fetch(`https://api.proxyhat.com/v1/reseller/proxies/${proxyId}/renew`, {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Accept": "application/json",
    // One key per (proxy, term) you intend to renew — never reuse across proxies.
    "Idempotency-Key": `renew-${proxyId}-2026-10-14`,
  },
});

const { payload } = await response.json();
console.log(payload.status, payload.total_usd);
proxyID := "7c5b0e3d-9a84-4e9d-a12f-8d2e1f4a6b3c"

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/proxies/"+proxyID+"/renew", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")
req.Header.Set("Idempotency-Key", "renew-"+proxyID+"-2026-10-14")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["status"], out.Payload["total_usd"])
A renewal can be refused before anything is charged. 409 with errors.proxy: "not_renewable" means the proxy failed the same eligibility gate the dashboard's own Renew button enforces — renewals disabled, no bound provider id, past the provider's cutoff window, a renewal already armed, or the plan no longer sellable — checked before the balance is touched, so a refused renewal never costs you anything. 402 with errors.balance: "insufficient_balance" (payload carries required and balance) means the same for an underfunded account. Poll GET /proxies/{id} or wait for a proxy.renewed / proxy.renewal_failed webhook to learn the outcome — a renewal order's own proxies array never populates (see Order Lifecycle).

Top-Ups

Your balance is prepaid and funded only by crypto deposit via Setype — there is no card rail on this surface. A deposit never creates a Payment row; it is a pure ledger credit once Setype confirms it.

POST /v1/reseller/topups Requires Auth

Create a Top-Up

Create a Top-Up

Open a crypto deposit invoice for amount_usd. Returns pay_address / payment_url / pay_amount_crypto for your customer (or your own treasury) to pay. The minimum deposit is $10 — below it, 422.

Example request: amount_usd cryptocurrency_code
Request Body
Name Type Required Description
amount_usd number Required Minimum 0.01 by field validation, but rejected below the $10 program minimum by a separate 422 (see below).
cryptocurrency_code string Optional One of: btc, eth, usdt, usdt_tron, usdt_bsc, sol, ltc, usdc, usdc_bsc, usdc_sol, usdt_sol, bnb. Omit to let Setype pick.
curl -X POST https://api.proxyhat.com/v1/reseller/topups \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"amount_usd": 250, "cryptocurrency_code": "btc"}'
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/reseller/topups",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={"amount_usd": 250, "cryptocurrency_code": "btc"},
)

topup = response.json()["payload"]
print(topup["pay_address"], topup["pay_amount_crypto"], topup["ticker"])
const response = await fetch("https://api.proxyhat.com/v1/reseller/topups", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ amount_usd: 250, cryptocurrency_code: "btc" }),
});

const { payload } = await response.json();
console.log(payload.pay_address, payload.pay_amount_crypto, payload.ticker);
body, _ := json.Marshal(map[string]interface{}{
    "amount_usd":          250,
    "cryptocurrency_code": "btc",
})

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/topups", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["pay_address"], out.Payload["pay_amount_crypto"])
Below the minimum. { "success": false, "payload": { "minimum_usd": 10 }, "meta": null, "errors": { "amount_usd": "below_minimum" }, "description": "Top-up amount is below the minimum" }, HTTP 422. Crypto discounts configured elsewhere on the platform (steering discounts for the ordinary checkout) deliberately never apply here — a $250 deposit request always invoices exactly $250.
GET /v1/reseller/topups Requires Auth

List Top-Ups

List Top-Ups

Your own top-ups, newest first, paginated.

No input needed — runs live with your key
Query Parameters
Name Type Required Description
status string Optional created, pending, completed, expired, or failed.
per_page integer Optional 1-200, default 50.
curl https://api.proxyhat.com/v1/reseller/topups \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/topups",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

for t in response.json()["payload"]["data"]:
    print(t["id"], t["status"], t["amount_usd"])
const response = await fetch("https://api.proxyhat.com/v1/reseller/topups", {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

const { payload } = await response.json();
payload.data.forEach(t => console.log(t.id, t.status, t.amount_usd));
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/topups", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload struct {
        Data []map[string]interface{} `json:"data"`
    } `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, t := range out.Payload.Data {
    fmt.Println(t["id"], t["status"], t["amount_usd"])
}
GET /v1/reseller/topups/{id} Requires Auth

Get a Top-Up

Get a Top-Up

Poll this to learn when a deposit clears, if you are not using the topup.completed webhook. 404 for another reseller's top-up or a malformed id.

We filled these in for you: id

Tweak any value if you like — or just press Try.

Path Parameters
Name Type Required Description
id string (uuid) Required The top-up id.
curl https://api.proxyhat.com/v1/reseller/topups/4a5c9f0b-2d3e-4d8e-a1c6-7b348d2e1f4a \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

topup_id = "4a5c9f0b-2d3e-4d8e-a1c6-7b348d2e1f4a"
response = requests.get(
    f"https://api.proxyhat.com/v1/reseller/topups/{topup_id}",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

print(response.json()["payload"]["status"])
const topupId = "4a5c9f0b-2d3e-4d8e-a1c6-7b348d2e1f4a";

const response = await fetch(`https://api.proxyhat.com/v1/reseller/topups/${topupId}`, {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

console.log((await response.json()).payload.status);
topupID := "4a5c9f0b-2d3e-4d8e-a1c6-7b348d2e1f4a"
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/topups/"+topupID, nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload["status"])

Webhooks

Webhooks are best-effort. Polling is authoritative. A partner that never configures a webhook URL is fully supported — every state change is equally readable from GET /orders/{id}, GET /proxies/{id} and GET /topups/{id}. Treat a delivered webhook as a hint to check sooner, never as the only place a piece of state is observable — deliveries retry up to 6 times over roughly a day (30s, 2m, 10m, 1h, 6h, then daily) and are then marked failed for good, with no further attempt.
GET /v1/reseller/webhook Requires Auth

Get Webhook Settings

Get Webhook Settings

Your current webhook URL and whether a secret is configured. The secret itself is never returned here — only by Rotate Secret, and only once.

No input needed — runs live with your key
curl https://api.proxyhat.com/v1/reseller/webhook \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/webhook",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

print(response.json()["payload"])
const response = await fetch("https://api.proxyhat.com/v1/reseller/webhook", {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

console.log((await response.json()).payload);
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/webhook", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload)
PUT /v1/reseller/webhook Requires Auth

Set Webhook URL

Set Webhook URL

Set or change where events are delivered. Must be HTTPS — an http:// URL is rejected with 422.

Example request: url
Request Body
Name Type Required Description
url string (url) Required Must start with https://, max 500 characters.
curl -X PUT https://api.proxyhat.com/v1/reseller/webhook \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"url": "https://acmeproxies.com/hooks/proxyhat"}'
import requests

response = requests.put(
    "https://api.proxyhat.com/v1/reseller/webhook",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={"url": "https://acmeproxies.com/hooks/proxyhat"},
)

print(response.json()["payload"])
const response = await fetch("https://api.proxyhat.com/v1/reseller/webhook", {
  method: "PUT",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ url: "https://acmeproxies.com/hooks/proxyhat" }),
});

console.log((await response.json()).payload);
body, _ := json.Marshal(map[string]string{"url": "https://acmeproxies.com/hooks/proxyhat"})

req, _ := http.NewRequest("PUT", "https://api.proxyhat.com/v1/reseller/webhook", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Payload)
configured only flips to true once a secret exists too. Setting the URL alone is not enough to receive signed deliveries — call Rotate Secret next.
POST /v1/reseller/webhook/rotate-secret Requires Auth

Rotate Webhook Secret

Rotate Webhook Secret

Generate a new signing secret. Returned in the response body exactly once — no endpoint on this API ever repeats it. Store it immediately; if you lose it, rotate again.

Example request:
curl -X POST https://api.proxyhat.com/v1/reseller/webhook/rotate-secret \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/reseller/webhook/rotate-secret",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

secret = response.json()["payload"]["secret"]
print("store this now, it will not be shown again:", secret)
const response = await fetch("https://api.proxyhat.com/v1/reseller/webhook/rotate-secret", {
  method: "POST",
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

const { payload } = await response.json();
console.log("store this now, it will not be shown again:", payload.secret);
req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/reseller/webhook/rotate-secret", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println("store this now, it will not be shown again:", out.Payload["secret"])
GET /v1/reseller/webhook/deliveries Requires Auth

List Webhook Deliveries

List Webhook Deliveries

A debugging log of past delivery attempts — id, event, status, attempts, HTTP response status, and timestamps. Never includes the delivery payload itself: a fulfilled-order delivery carries live proxy credentials, and this listing is for diagnosing delivery health, not for reading data you can already get from the order.

No input needed — runs live with your key
Query Parameters
Name Type Required Description
per_page integer Optional 1-200, default 50.
curl https://api.proxyhat.com/v1/reseller/webhook/deliveries \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

response = requests.get(
    "https://api.proxyhat.com/v1/reseller/webhook/deliveries",
    headers={"Authorization": "Bearer __API_KEY__", "Accept": "application/json"},
)

for d in response.json()["payload"]["data"]:
    print(d["event"], d["status"], d["attempts"], d["response_status"])
const response = await fetch("https://api.proxyhat.com/v1/reseller/webhook/deliveries", {
  headers: { "Authorization": "Bearer __API_KEY__", "Accept": "application/json" },
});

const { payload } = await response.json();
payload.data.forEach(d => console.log(d.event, d.status, d.attempts, d.response_status));
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/reseller/webhook/deliveries", nil)
req.Header.Set("Authorization", "Bearer __API_KEY__")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var out struct {
    Payload struct {
        Data []map[string]interface{} `json:"data"`
    } `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, d := range out.Payload.Data {
    fmt.Println(d["event"], d["status"], d["attempts"], d["response_status"])
}

Verifying Webhook Signatures

Every delivery carries three headers alongside the JSON body:

HeaderMeaning
X-ProxyHat-EventThe event name, e.g. order.fulfilled.
X-ProxyHat-DeliveryThis delivery's id (matches GET /webhook/deliveries).
X-ProxyHat-TimestampUnix seconds at send time.
X-ProxyHat-Signaturesha256=<hex hmac> — see below.
The signature is computed over "{timestamp}.{raw body}" — not the body alone. Concatenate the exact string from X-ProxyHat-Timestamp, a literal ., and the exact raw request body bytes (before any JSON parsing), then HMAC-SHA256 that with your webhook secret. Hex-encode the result and compare it to X-ProxyHat-Signature with a constant-time comparison. Binding the timestamp into the signed string is what stops a captured, valid delivery from being replayed later with a fresh timestamp: also reject any delivery whose timestamp is more than 5 minutes old (or from the future), even if the signature itself checks out.
import hashlib
import hmac
import time

def verify_webhook(secret: str, timestamp_header: str, raw_body: bytes, signature_header: str) -> bool:
    # Reject stale or future-dated deliveries before touching the signature.
    if abs(time.time() - int(timestamp_header)) > 300:
        return False

    signed_string = f"{timestamp_header}.".encode() + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), signed_string, hashlib.sha256).hexdigest()

    return hmac.compare_digest(expected, signature_header)

# Flask example:
# ts = request.headers["X-ProxyHat-Timestamp"]
# sig = request.headers["X-ProxyHat-Signature"]
# if not verify_webhook(WEBHOOK_SECRET, ts, request.get_data(), sig):
#     abort(401)
import crypto from "crypto";

function verifyWebhook(secret, timestampHeader, rawBody, signatureHeader) {
  // Reject stale or future-dated deliveries before touching the signature.
  if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > 300) return false;

  const signedString = Buffer.concat([Buffer.from(`${timestampHeader}.`), rawBody]);
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(signedString).digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express example (mount with express.raw({ type: "application/json" }) so req.body is a Buffer):
// const ts = req.header("X-ProxyHat-Timestamp");
// const sig = req.header("X-ProxyHat-Signature");
// if (!verifyWebhook(WEBHOOK_SECRET, ts, req.body, sig)) return res.sendStatus(401);
func verifyWebhook(secret, timestampHeader string, rawBody []byte, signatureHeader string) bool {
    ts, err := strconv.ParseInt(timestampHeader, 10, 64)
    if err != nil || abs(time.Now().Unix()-ts) > 300 {
        return false // reject stale, future-dated, or unparseable timestamps
    }

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(timestampHeader + "."))
    mac.Write(rawBody)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(expected), []byte(signatureHeader))
}

func abs(n int64) int64 {
    if n < 0 {
        return -n
    }
    return n
}

Webhook Events Reference

EventFired whendata shape
order.fulfilledAn order's proxy (or proxies) has been delivered.The order object (same shape as Get an Order).
order.failedAn order could not be delivered (e.g. fulfilled after its own refund raced it, diverting the IP to house stock).The order object plus a reason string.
order.refundedThe unfulfilled-order sweep (or an admin action) refunded an order.The order object, now status: "refunded".
proxy.renewedA renewal actually landed — either the nightly auto-renew pass or your own POST /renew call.The proxy object (same shape as Get a Proxy).
proxy.renewal_failedA renewal did not land. Fired from two different places with two different payload shapes — see the warning below.Either the proxy object + reason (an immediate, synchronous failure, e.g. insufficient balance) or the order object + reason (an armed-mode provider's later, asynchronous refusal correcting an earlier optimistic proxy.renewed).
proxy.expiringOnce daily, for every proxy 7 or 1 calendar days from expires_at (whether or not it has auto-renew).The proxy object plus days_left (7 or 1).
proxy.expiredOnce daily, the first time a proxy is observed past expires_at.The proxy object.
balance.lowAt most once per day, when your balance is under either your low_balance_threshold_usd or the forecast cost of renewing every auto-renew proxy due in the next 7 days.{ "balance_usd": ..., "forecast_usd": ..., "threshold_usd": ... }.
topup.completedA crypto deposit is confirmed and credited.{ "id": ..., "amount_usd": ..., "balance_usd": ... } (the balance after credit).
proxy.renewal_failed does not have one fixed schema. An immediate rejection (insufficient balance, or any exception thrown directly from your POST /renew call or the nightly auto-renew pass) reports the proxy resource. A later correction — an armed-mode provider like CyberYozh confirming failure only after we had already reported proxy.renewed optimistically — reports the order resource instead. Both carry a top-level reason string either way, so branch on whether the payload has an ip field (proxy) or a kind field (order) rather than assuming one shape, or simply treat the payload as a hint and re-fetch current state from GET /proxies/{id}.

Deliveries retry on failure at 30s, 2 minutes, 10 minutes, 1 hour, 6 hours, then daily — 6 attempts total before a delivery is marked failed for good. proxy.expiring / proxy.expired run once daily; proxy.renewed / proxy.renewal_failed from the same daily pass run at 04:40 UTC; the unfulfilled-order sweep behind order.refunded runs hourly.