How to Resell ProxyHat Proxies

ProxyHat is built to be resold. One ProxyHat account holds a single paid traffic pool, and you carve that pool into sub-users — one per downstream customer, each with its own proxy credentials and its own byte cap that ProxyHat enforces upstream. This guide covers the model, the one mistake that catches every new reseller, and three ways to run your business: by hand in the dashboard, programmatically through the /v1 API embedded in your own site or software, and from the Telegram bot on your phone.

The Model

There is no separate "reseller mode" to switch on — the sub-user system is the reseller system. Three things are worth internalising before you sell anything:

All three surfaces — dashboard, API, and Telegram bot — drive the same core, so a sub-user you create in the dashboard is visible and editable over the API and vice versa.

How the pool splits up. Every capped sub-user reserves its unused headroom from the pool: reserved = max(traffic_limit − used_traffic, 0). What's left to hand out is traffic_available = traffic_total_remaining − Σ reserved. You can read all three numbers from the meta block on GET /v1/sub-users. You cannot allocate more than traffic_available — over-allocation is rejected with a 422, so you can never oversell beyond the pool you've actually bought.

The #1 Trap — Read This First

Topping up your pool does NOT automatically raise an existing customer's traffic_limit. Buying more traffic only grows the shared pool. A sub-user that has run out stays dead at its old cap until you explicitly raise it. There is exactly one exception: if your account has a single active proxy, a credit auto-allocates to it. Every reseller has many sub-users, so this auto-allocation never fires for you. After any top-up you must raise each customer you're renewing with PUT /v1/sub-users (or the dashboard edit form, or a bot PATCH). Paying for more traffic and forgetting this step is the single most common reseller error.

Four more rules that follow from how caps and the pool work:

Resetting usage does not refund the pool. reset-usage zeroes a customer's meter for a new billing cycle, but the bytes they actually spent are already debited from your pool — it is not a way to reclaim traffic. Deletes are also soft: the customer's password is invalidated instantly (they disconnect at once), and the upstream account is torn down about 15 minutes later.

Flow A — Resell Manually via the Dashboard

The fastest way to start, and all you need for a handful of customers. Everything below is at dashboard.proxyhat.com.

  1. Buy a traffic pool. Go to Buy traffic (or start a subscription). This grows your account balance — your sellable inventory. Buy enough to cover the sum of the caps you plan to sell.
  2. (Optional) Create groups. Under Sub-users → Groups, add a group per plan tier or per business customer. Groups are just organisational labels scoped to your account.
  3. Create one sub-user per customer. Sub-users → New. Set a proxy password (9–40 alphanumeric characters), a traffic limit in GB (the customer's cap — leave it unlimited only on purpose), a name you'll recognise them by, and optionally a group.
  4. Generate the connection string. Open the proxy builder for that sub-user, pick the protocol (SOCKS5 on port 1080 is recommended; HTTP on 8080 for browser tooling), choose country / region / city, rotating vs. sticky (30m or 12h), and an AI quality filter. Copy the rendered URL — protocol://username:password@gate.proxyhat.com:port — and hand it to your customer. Everything (geo, session, filter) is baked into the username, so the URL is all they need.
  5. Monitor. The sub-users list shows each customer's used traffic against their limit; Analytics drills into traffic, requests, and per-domain breakdown. The traffic summary shows how much pool you have left to sell.
  6. Renew / top up. Buy more pool if you're low, then edit that specific customer and raise their traffic limit (remember the trap — the top-up alone does nothing). Optionally hit Reset usage to zero their meter for a fresh cycle.
  7. Churn. Lower a customer's limit, or delete the sub-user — their password is invalidated immediately and they disconnect at once.

Flow B — Resell Programmatically via the API

To sell proxies from your own website, panel, or software, drive the /v1 API directly. The base URL is https://api.proxyhat.com/v1 and every call below is authenticated with a Sanctum bearer token: Authorization: Bearer __API_KEY__. The typical lifecycle is: get a key → check inventory → provision a customer → mint their URL → meter → top up / reset. The sub-user endpoints all return the standard {success, payload, meta, errors, description} envelope.

1. Get an API key

You can create a key in the dashboard under Settings → API keys, or mint one programmatically. The plaintext token is returned once — store it immediately.

POST /v1/profile/api-keys Requires Auth

Create an API Key

Create an API Key

Mint a new Sanctum bearer token for your account. The plain_text_token is shown only in this response — you cannot retrieve it again. abilities defaults to ["*"] (full scope); pass a narrower array to scope a token per integration.

Example request:
Request Body
Name Type Required Description
name string Optional A label for the key (defaults to "API key <timestamp>").
abilities array Optional Token scopes. Defaults to ["*"] (full access).
curl -X POST https://api.proxyhat.com/v1/profile/api-keys \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"name": "reseller-integration"}'
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/profile/api-keys",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Accept": "application/json",
    },
    json={"name": "reseller-integration"},
)

data = response.json()
# Store this now — it is never shown again.
print(data["plain_text_token"])
const response = await fetch("https://api.proxyhat.com/v1/profile/api-keys", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ name: "reseller-integration" }),
});

const data = await response.json();
// Store this now — it is never shown again.
console.log(data.plain_text_token);
payload := strings.NewReader(`{"name": "reseller-integration"}`)

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/profile/api-keys", payload)
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 data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data["plain_text_token"])
Changing your account password revokes every API key. If your integration rotates the account password it will invalidate its own token (and all others). Keep password changes and token issuance separate, and re-issue tokens after any password change. If you enable 2FA on the account, every mutating sub-user call additionally requires a twofa_code in the body — run automation on an account without 2FA, or supply a fresh code per call.

2. Check your inventory

Before provisioning, read meta.traffic_available — the bytes free to allocate right now. The same call lists every existing customer with their usage, so it doubles as your monitoring endpoint (step 5).

GET /v1/sub-users Requires Auth

List Sub-Users & Read Available Pool

List Sub-Users & Read Available Pool

Returns all of your sub-users (each enriched with used_traffic and a last-24h stats_used_traffic) plus a meta block with your account traffic accounting. Pass ?uuid= to fetch a single sub-user — there is no dedicated single-resource GET.

No input needed — runs live with your key
Query Parameters
Name Type Required Description
uuid string (uuid) Optional Filter to a single sub-user. Returns a one-element payload array (empty if it is not yours).
curl https://api.proxyhat.com/v1/sub-users \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Accept: application/json"
import requests

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

body = response.json()
available = body["meta"]["traffic_available"]
print(f"{available} bytes free to allocate")
for su in body["payload"]:
    print(su["name"], su["used_traffic"], "/", su["traffic_limit"])
const response = await fetch("https://api.proxyhat.com/v1/sub-users", {
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Accept": "application/json",
  },
});

const body = await response.json();
console.log(`${body.meta.traffic_available} bytes free to allocate`);
body.payload.forEach(su =>
  console.log(su.name, su.used_traffic, "/", su.traffic_limit)
);
req, _ := http.NewRequest("GET", "https://api.proxyhat.com/v1/sub-users", 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 body struct {
    Payload []map[string]interface{} `json:"payload"`
    Meta    map[string]interface{}   `json:"meta"`
}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Printf("%v bytes free to allocate\n", body.Meta["traffic_available"])

3. Provision a customer

Create one sub-user per customer. You supply proxy_password and (almost always) a positive traffic_limit; the proxy_username is generated server-side and is immutable afterward. Creation is confirmed synchronously, but the upstream provider registration is deferred a moment — while provisioning is true the credentials briefly return 401. Poll GET /v1/sub-users?uuid= until provisioning flips to false if you need to be certain before handing out the URL.

POST /v1/sub-users Requires Auth

Create a Sub-User (One per Customer)

Create a Sub-User (One per Customer)

Provision a downstream customer. traffic_limit is in bytes; 0 means uncapped (draws your whole available pool), so set a real positive cap to resell a fixed quota. Rejected with 422 if the requested limit exceeds traffic_available. Blocked if any of your sub-users is currently suspended for overage.

Example request:
Request Body
Name Type Required Description
proxy_password string Required 9–40 characters, alphanumeric only (/^[a-zA-Z0-9]*$/). The customer's proxy password.
traffic_limit integer Optional The customer's cap in BYTES. 0 = uncapped. Must fit within traffic_available.
name string Optional Your label for the customer (max 40 chars).
notes string Optional Private notes (max 5000 chars).
sub_user_group_id string (uuid) Optional Assign to one of your groups. Must be a group you own.
curl -X POST https://api.proxyhat.com/v1/sub-users \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "proxy_password": "CustomerPass123",
    "traffic_limit": 5000000000,
    "name": "customer-acme"
  }'
import requests

# 5 GB cap for this customer
response = requests.post(
    "https://api.proxyhat.com/v1/sub-users",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Accept": "application/json",
    },
    json={
        "proxy_password": "CustomerPass123",
        "traffic_limit": 5 * 1000**3,
        "name": "customer-acme",
    },
)

su = response.json()["payload"]
print(su["uuid"], su["proxy_username"])
const response = await fetch("https://api.proxyhat.com/v1/sub-users", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({
    proxy_password: "CustomerPass123",
    traffic_limit: 5 * 1000 ** 3, // 5 GB
    name: "customer-acme",
  }),
});

const { payload } = await response.json();
console.log(payload.uuid, payload.proxy_username);
payload := strings.NewReader(`{
  "proxy_password": "CustomerPass123",
  "traffic_limit": 5000000000,
  "name": "customer-acme"
}`)

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/sub-users", payload)
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 body struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println(body.Payload["uuid"], body.Payload["proxy_username"])

4. Hand the customer a ready connection URL

Rather than teaching customers the username grammar, let the server build it. POST /v1/proxy-descriptors takes the sub-user UUID plus structured location, session, and filter options and returns the fully assembled username, password, host, port, and a ready-to-paste url. Deliver the url to your customer — nothing else is required. The sub-user must be active (a suspended or deleting one returns 404).

POST /v1/proxy-descriptors Requires Auth

Mint a Connection URL for a Customer

Mint a Connection URL for a Customer

Compose a ready-to-use proxy connection string for one of your active sub-users. All targeting (country/region/city, sticky session, AI filter) is encoded into the returned username. host is always gate.proxyhat.com; port is 8080 for http, 1080 for socks5.

We filled these in for you: sub_user auto

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

Request Body
Name Type Required Description
sub_user_uuid string (uuid) Required The customer sub-user. Must be yours and active.
protocol string Required One of "http" or "socks5".
location object Optional Geo targeting: location.country (ISO, max 32), location.region (max 128), location.city (max 128). Omit country to default to any.
session object Optional session.mode = "rotating" (default) or "sticky"; session.ttl = "30m" or "12h"; session.id = 16 hex chars (auto-generated if omitted for sticky).
filter string Optional AI quality filter: filter-high, filter-high-speed-fast, filter-medium (default), filter-medium-speed-fast, or none.
curl -X POST https://api.proxyhat.com/v1/proxy-descriptors \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "sub_user_uuid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "protocol": "socks5",
    "location": { "country": "us", "city": "new york" },
    "session": { "mode": "sticky", "ttl": "30m" },
    "filter": "filter-medium"
  }'
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/proxy-descriptors",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Accept": "application/json",
    },
    json={
        "sub_user_uuid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "protocol": "socks5",
        "location": {"country": "us", "city": "new york"},
        "session": {"mode": "sticky", "ttl": "30m"},
        "filter": "filter-medium",
    },
)

# Hand this url straight to the customer.
print(response.json()["payload"]["url"])
const response = await fetch("https://api.proxyhat.com/v1/proxy-descriptors", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({
    sub_user_uuid: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    protocol: "socks5",
    location: { country: "us", city: "new york" },
    session: { mode: "sticky", ttl: "30m" },
    filter: "filter-medium",
  }),
});

const { payload } = await response.json();
console.log(payload.url); // hand straight to the customer
payload := strings.NewReader(`{
  "sub_user_uuid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "protocol": "socks5",
  "location": { "country": "us", "city": "new york" },
  "session": { "mode": "sticky", "ttl": "30m" },
  "filter": "filter-medium"
}`)

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/proxy-descriptors", payload)
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 body struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println(body.Payload["url"])
Want to understand the username grammar itself? See Connecting to a Proxy for the full token order (-country-, -region-, -city-, sticky -sid-/-ttl-, and the AI filter) and the list of gateway hosts and ports.

5. Meter and bill

Poll GET /v1/sub-users (step 2) for each customer's used_traffic against their traffic_limit, plus a stats_used_traffic figure for the last 24 hours. For deeper reporting the Analytics API gives you per-customer time series and per-domain breakdowns — pass the sub-user UUID as the user parameter.

6. Top up / renew a customer

This is where the trap bites. After (optionally) buying more pool, raise the specific customer's cap. The update controller reads the target from the uuid in the JSON body — the {sub_user} path segment is ignored, so any placeholder there works. Only send the fields you're changing; a traffic_limit above traffic_available is rejected with a 422. Set traffic_limit to 0 to uncap the customer entirely.

PUT /v1/sub-users/{sub_user} Requires Auth

Raise a Customer's Traffic Limit

Raise a Customer's Traffic Limit

Update a customer's cap (or password / name / group). The target sub-user is identified by the uuid in the BODY, not the path segment. PATCH is also accepted. A pool top-up does NOT do this for you — this explicit call is mandatory per customer you renew.

Example request: sub_user auto
Path Parameters
Name Type Required Description
sub_user string Required Ignored — a value is only needed to form a valid URL. The body uuid selects the target.
Request Body
Name Type Required Description
uuid string (uuid) Required UUID of the customer to update. This — not the path — determines the target.
traffic_limit integer Optional New cap in BYTES. Must fit within traffic_available; cannot drop below already-used bytes. 0 = uncapped.
proxy_password string Optional 9–40 alphanumeric chars, to rotate the customer's password.
name string Optional Rename the customer (max 40 chars).
sub_user_group_id string (uuid) Optional Move to a group, or null to unassign.
# The URL path segment is ignored; the body uuid selects the target.
curl -X PUT https://api.proxyhat.com/v1/sub-users/_ \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "uuid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "traffic_limit": 10000000000
  }'
import requests

# Raise this customer to 10 GB. The path segment is a throwaway.
response = requests.put(
    "https://api.proxyhat.com/v1/sub-users/_",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Accept": "application/json",
    },
    json={
        "uuid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "traffic_limit": 10 * 1000**3,
    },
)

print(response.json()["payload"]["traffic_limit"])
// The URL path segment is ignored; the body uuid selects the target.
const response = await fetch("https://api.proxyhat.com/v1/sub-users/_", {
  method: "PUT",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({
    uuid: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    traffic_limit: 10 * 1000 ** 3, // 10 GB
  }),
});

const { payload } = await response.json();
console.log(payload.traffic_limit);
// The URL path segment is ignored; the body uuid selects the target.
payload := strings.NewReader(`{
  "uuid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "traffic_limit": 10000000000
}`)

req, _ := http.NewRequest("PUT", "https://api.proxyhat.com/v1/sub-users/_", payload)
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 body struct {
    Payload map[string]interface{} `json:"payload"`
}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println(body.Payload["traffic_limit"])
Lowering a limit is rate-limited. Decreases are guarded by a 120-second lock and return 429 with a Retry-After hint if you call them back-to-back; you also cannot drop a cap below the bytes the customer has already used. Increases have no such limit.

7. Start a new billing cycle

To reset a customer's meter at the start of a period, call reset-usage with their UUID(s). This zeroes used_traffic so they can consume up to their cap again — it does not refund the pool (the bytes were already spent). Registered before the resource routes, so it accepts a plain ids array.

POST /v1/sub-users/reset-usage Requires Auth

Reset a Customer's Usage Meter

Reset a Customer's Usage Meter

Zero the used_traffic counter for one or more sub-users (e.g. at the start of each billing cycle). Returns the affected sub-users plus refreshed account meta. Does not credit the pool.

Example request:
Request Body
Name Type Required Description
ids array Required One or more sub-user UUIDs to reset (min 1).
curl -X POST https://api.proxyhat.com/v1/sub-users/reset-usage \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]}'
import requests

response = requests.post(
    "https://api.proxyhat.com/v1/sub-users/reset-usage",
    headers={
        "Authorization": "Bearer __API_KEY__",
        "Accept": "application/json",
    },
    json={"ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]},
)

print(response.json()["description"])
const response = await fetch("https://api.proxyhat.com/v1/sub-users/reset-usage", {
  method: "POST",
  headers: {
    "Authorization": "Bearer __API_KEY__",
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ ids: ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"] }),
});

const body = await response.json();
console.log(body.description);
payload := strings.NewReader(`{"ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]}`)

req, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/sub-users/reset-usage", payload)
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 body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println(body["description"])

Groups & bulk operations for fleets

Once you run many customers, organise them with groups and act on them in bulk. Full reference lives on the Sub-User Groups and Sub-Users pages; the essentials:

# Create a group, then move two customers into it
curl -X POST https://api.proxyhat.com/v1/sub-user-groups \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -d '{"name": "Tier 1", "description": "5 GB monthly customers"}'

curl -X POST https://api.proxyhat.com/v1/sub-users/bulk-move-to-group \
  -H "Authorization: Bearer __API_KEY__" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"],
    "sub_user_group_id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
  }'
import requests

H = {"Authorization": "Bearer __API_KEY__", "Accept": "application/json"}

group = requests.post(
    "https://api.proxyhat.com/v1/sub-user-groups",
    headers=H,
    json={"name": "Tier 1", "description": "5 GB monthly customers"},
).json()["payload"]

requests.post(
    "https://api.proxyhat.com/v1/sub-users/bulk-move-to-group",
    headers=H,
    json={
        "ids": [
            "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
            "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
        ],
        "sub_user_group_id": group["id"],
    },
)
const H = {
  "Authorization": "Bearer __API_KEY__",
  "Content-Type": "application/json",
  "Accept": "application/json",
};

const groupRes = await fetch("https://api.proxyhat.com/v1/sub-user-groups", {
  method: "POST",
  headers: H,
  body: JSON.stringify({ name: "Tier 1", description: "5 GB monthly customers" }),
});
const { payload: group } = await groupRes.json();

await fetch("https://api.proxyhat.com/v1/sub-users/bulk-move-to-group", {
  method: "POST",
  headers: H,
  body: JSON.stringify({
    ids: [
      "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
    ],
    sub_user_group_id: group.id,
  }),
});
client := &http.Client{}

groupBody := strings.NewReader(`{"name": "Tier 1", "description": "5 GB monthly customers"}`)
gReq, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/sub-user-groups", groupBody)
gReq.Header.Set("Authorization", "Bearer __API_KEY__")
gReq.Header.Set("Content-Type", "application/json")
gResp, _ := client.Do(gReq)
defer gResp.Body.Close()

var group struct {
    Payload struct{ ID string `json:"id"` } `json:"payload"`
}
json.NewDecoder(gResp.Body).Decode(&group)

moveBody := strings.NewReader(fmt.Sprintf(`{
  "ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"],
  "sub_user_group_id": %q
}`, group.Payload.ID))
mReq, _ := http.NewRequest("POST", "https://api.proxyhat.com/v1/sub-users/bulk-move-to-group", moveBody)
mReq.Header.Set("Authorization", "Bearer __API_KEY__")
mReq.Header.Set("Content-Type", "application/json")
mResp, _ := client.Do(mReq)
defer mResp.Body.Close()

Flow C — Resell from the Telegram Bot

If you'd rather run everything from your phone, @ProxyHatBot exposes the same account. Link your Telegram to your ProxyHat account once (bot /start → link/verify); after that every action operates on that single linked account. The bot is single-tenant — all your sub-users live under your one account — so it's ideal for a solo reseller, not for handing separate logins to end customers.

  1. Fund the pool. Top up traffic from inside the bot (card, crypto, or Telegram Stars). It grows the exact same sellable pool the dashboard and API use.
  2. Create a customer sub-user. The bot's Create screen offers preset caps — 100 MB, 500 MB, 1 GB, 3 GB, 5 GB, or Max — and posts your choice in bytes. The proxy_password is generated for you server-side (you never type it into chat). The credentials are shown for active sub-users.
  3. Hand over a ready URL with quick-proxy. Pick a preset — us_residential, eu_residential, random, or country_specific (with a country ISO code) — and a session — rotating, sticky_30m, or sticky_12h. The bot composes the full username, password, and connection URL (plus a tappable tg://socks?… link) with no modifier syntax to learn. Two taps and the customer has a working proxy.
  4. Top up / rename a customer. Edit the customer and set a new traffic limit or name. As always, this explicit per-customer raise is what actually renews them — a pool top-up alone won't. If you lower a limit too quickly the bot may ask you to retry in a moment (the same 120-second rate-limit as the API).
  5. Offboard. Delete the customer from the bot; their password is invalidated immediately and they disconnect at once.
Under the hood. The bot drives the same core through its own Telegram-authenticated endpoints (GET/POST /v1/bot/sub-users, PATCH/DELETE /v1/bot/sub-users/{uuid}, and POST /v1/bot/quick-proxy). These are authenticated by your linked Telegram identity, not a bearer token, so they're driven from within the bot rather than called directly from your own software. For programmatic reselling from your own site, use the /v1 API in Flow B.

Next Steps