Reseller Integration Kit

Copy-paste, runnable code to resell ProxyHat proxies from inside your own product. Everything here drives the real /v1 API: a reusable API client, a complete Telegram reseller bot, and a website/storefront backend that provisions a proxy the moment a customer pays. If you want the concepts first — the traffic pool, the sub-user model, and the one trap that catches every reseller — read How to Resell ProxyHat Proxies. This page is the code.

Reference for every call below: Sub-Users API (create / update / delete / reset-usage) and Proxy Descriptors API (build a customer's connection URL). The base URL is https://api.proxyhat.com/v1 and every request carries Authorization: Bearer __API_KEY__.

Overview

The model is simple: your ProxyHat account holds one paid traffic pool. For each customer you create a sub-user — its own proxy credentials and its own byte cap that ProxyHat enforces upstream. You then hand the customer a ready connection URL and never expose your own account. Three pieces of code turn that into a product:

  1. A reusable API client — a thin wrapper around the four calls you make constantly.
  2. A Telegram reseller bot — a customer taps /buy, you charge them, and the bot DMs back a working proxy.
  3. A storefront backend — after your checkout confirms payment, a POST /provision endpoint returns the connection string to your site.
Your API key is a server-side secret — it never touches a browser or an end customer. The key controls your whole account and pool. In every example below it lives only on your server (an environment variable). What the customer receives is a single proxy URL (protocol://username:password@gate.proxyhat.com:port) built for their sub-user — never the API key, and never your account credentials. If a customer ever sees __API_KEY__, something is wired wrong.

A Reusable API Client

Almost everything a reseller does is four calls: check how much pool is left to sell, create a customer, mint their connection URL, and later raise their cap. Wrap them once. Note the unit conversion — traffic_limit is always in bytes (1 GB = 1000000000), and 0 means uncapped, so always pass a real positive size.

import secrets
import string
import requests


class ProxyHat:
    """Minimal server-side ProxyHat reseller client. Keep the API key secret."""

    BASE = "https://api.proxyhat.com/v1"
    GB = 1000 ** 3  # 1 GB in bytes

    def __init__(self, api_key):
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Accept": "application/json",
        })

    def available_traffic(self):
        """Bytes still free to allocate across your account (pool left to sell)."""
        r = self.session.get(f"{self.BASE}/sub-users")
        r.raise_for_status()
        return r.json()["meta"]["traffic_available"]

    def create_customer(self, name, gb):
        """Provision one sub-user (a customer) capped at `gb` gigabytes."""
        alphabet = string.ascii_letters + string.digits
        password = "".join(secrets.choice(alphabet) for _ in range(20))
        r = self.session.post(f"{self.BASE}/sub-users", json={
            "proxy_password": password,             # 9-40 alphanumeric chars
            "traffic_limit": int(gb * self.GB),     # bytes; 0 would mean uncapped
            "name": name,
        })
        r.raise_for_status()
        return r.json()["payload"]                   # includes uuid + proxy_username

    def proxy_url(self, sub_user_uuid, country="us", protocol="socks5", mode="rotating"):
        """Return a ready-to-paste connection URL to hand to the customer."""
        r = self.session.post(f"{self.BASE}/proxy-descriptors", json={
            "sub_user_uuid": sub_user_uuid,
            "protocol": protocol,                    # "socks5" (port 1080) or "http" (8080)
            "location": {"country": country},        # ISO code, e.g. "us", "de", "gb"
            "session": {"mode": mode},               # "rotating" or "sticky"
            "filter": "filter-medium",               # AI quality filter (recommended default)
        })
        r.raise_for_status()
        return r.json()["payload"]["url"]

    def top_up(self, uuid, gb):
        """Raise one customer's cap to `gb` GB. A pool top-up alone will NOT do
        this once you have more than one customer, so this call is mandatory."""
        r = self.session.put(f"{self.BASE}/sub-users/_", json={
            "uuid": uuid,                            # target comes from the BODY
            "traffic_limit": int(gb * self.GB),
        })
        r.raise_for_status()
        return r.json()["payload"]


# --- Usage ---------------------------------------------------------------
ph = ProxyHat("__API_KEY__")                         # load from env in real code
print(ph.available_traffic(), "bytes free to sell")

customer = ph.create_customer("customer-acme", gb=5) # 5 GB cap
url = ph.proxy_url(customer["uuid"], country="us")
print(url)  # hand this to the customer
import crypto from "node:crypto";

class ProxyHat {
  static BASE = "https://api.proxyhat.com/v1";
  static GB = 1000 ** 3; // 1 GB in bytes

  constructor(apiKey) {
    this.headers = {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    };
  }

  async #call(method, path, body) {
    const res = await fetch(`${ProxyHat.BASE}${path}`, {
      method,
      headers: this.headers,
      body: body ? JSON.stringify(body) : undefined,
    });
    if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}`);
    return res.json();
  }

  // Bytes still free to allocate across your account (pool left to sell).
  async availableTraffic() {
    const { meta } = await this.#call("GET", "/sub-users");
    return meta.traffic_available;
  }

  // Provision one sub-user (a customer) capped at `gb` gigabytes.
  async createCustomer(name, gb) {
    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    const password = Array.from({ length: 20 }, () => alphabet[crypto.randomInt(alphabet.length)]).join("");
    const { payload } = await this.#call("POST", "/sub-users", {
      proxy_password: password,                  // 9-40 alphanumeric chars
      traffic_limit: Math.round(gb * ProxyHat.GB), // bytes; 0 would mean uncapped
      name,
    });
    return payload;                              // includes uuid + proxy_username
  }

  // Ready-to-paste connection URL to hand to the customer.
  async proxyUrl(subUserUuid, { country = "us", protocol = "socks5", mode = "rotating" } = {}) {
    const { payload } = await this.#call("POST", "/proxy-descriptors", {
      sub_user_uuid: subUserUuid,
      protocol,                                  // "socks5" (port 1080) or "http" (8080)
      location: { country },                     // ISO code, e.g. "us", "de", "gb"
      session: { mode },                         // "rotating" or "sticky"
      filter: "filter-medium",                   // AI quality filter (recommended default)
    });
    return payload.url;
  }

  // Raise one customer cap to `gb` GB. A pool top-up alone does NOT do this
  // once you have more than one customer, so this call is mandatory.
  async topUp(uuid, gb) {
    const { payload } = await this.#call("PUT", "/sub-users/_", {
      uuid,                                      // target comes from the BODY
      traffic_limit: Math.round(gb * ProxyHat.GB),
    });
    return payload;
  }
}

// --- Usage ---------------------------------------------------------------
const ph = new ProxyHat("__API_KEY__");          // load from env in real code
console.log(await ph.availableTraffic(), "bytes free to sell");

const customer = await ph.createCustomer("customer-acme", 5); // 5 GB cap
const url = await ph.proxyUrl(customer.uuid, { country: "us" });
console.log(url); // hand this to the customer

Build Your Own Telegram Reseller Bot

A complete, runnable bot: a customer sends /start, then /buy; you charge them; and the bot provisions a capped sub-user, mints its connection URL, and DMs it back. The two blocks below are the same bot in python-telegram-bot (v20+, async) and Telegraf for Node.

Payment is modelled as a clearly-marked pluggable hook so the ProxyHat side is 100% real: charge the customer first (Telegram Stars, crypto, or your own gateway), and only call the provisioning function once payment succeeds. Everything after that hook is production code.

# bot.py  --  python-telegram-bot v20+ (async)
import os
import secrets
import string

import httpx
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes

PROXYHAT_API = "https://api.proxyhat.com/v1"
API_KEY = os.environ["PROXYHAT_API_KEY"]        # reseller key -- stays on the server
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]    # from @BotFather

GB = 1000 ** 3
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"}


def random_password(n=20):
    alphabet = string.ascii_letters + string.digits
    return "".join(secrets.choice(alphabet) for _ in range(n))


async def provision_proxy(name, gb, country="us"):
    """Create a capped sub-user and return its ready connection URL."""
    async with httpx.AsyncClient(base_url=PROXYHAT_API, headers=HEADERS, timeout=30) as api:
        # 1. Create one sub-user for this customer, capped in BYTES.
        created = await api.post("/sub-users", json={
            "proxy_password": random_password(),
            "traffic_limit": int(gb * GB),
            "name": name,
        })
        created.raise_for_status()
        sub_user = created.json()["payload"]

        # 2. Mint a ready-to-use connection URL for that sub-user.
        desc = await api.post("/proxy-descriptors", json={
            "sub_user_uuid": sub_user["uuid"],
            "protocol": "socks5",
            "location": {"country": country},
            "session": {"mode": "rotating"},
            "filter": "filter-medium",
        })
        desc.raise_for_status()
        return desc.json()["payload"]["url"]


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "Welcome! Send /buy to get a 5 GB US residential proxy."
    )


async def buy(update: Update, context: ContextTypes.DEFAULT_TYPE):
    # ----- PAYMENT HOOK ---------------------------------------------------
    # Charge the customer FIRST, then provision. Only reach the code below
    # once payment has actually succeeded. With Telegram Stars you would send
    # an invoice here (currency "XTR") and move the provisioning call into your
    # PreCheckoutQuery / successful_payment handler:
    #   await context.bot.send_invoice(chat_id, "5 GB proxy", "...", payload="5gb",
    #       provider_token="", currency="XTR",
    #       prices=[LabeledPrice("5 GB proxy", 250)])
    # For this demo we provision immediately.
    # ----------------------------------------------------------------------
    customer = f"tg-{update.effective_user.id}"
    await update.message.reply_text("Provisioning your proxy...")
    url = await provision_proxy(name=customer, gb=5, country="us")
    await update.message.reply_text(
        f"Your proxy is ready:\n\n`{url}`\n\nPaste it into any SOCKS5 client.",
        parse_mode="Markdown",
    )


def main():
    app = Application.builder().token(BOT_TOKEN).build()
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("buy", buy))
    app.run_polling()


if __name__ == "__main__":
    main()
// bot.js  --  Telegraf (Node.js)
import { Telegraf } from "telegraf";
import crypto from "node:crypto";

const PROXYHAT_API = "https://api.proxyhat.com/v1";
const API_KEY = process.env.PROXYHAT_API_KEY;   // reseller key -- stays on the server
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); // from @BotFather

const GB = 1000 ** 3;
const HEADERS = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
  Accept: "application/json",
};

function randomPassword(n = 20) {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  return Array.from({ length: n }, () => alphabet[crypto.randomInt(alphabet.length)]).join("");
}

// Create a capped sub-user and return its ready connection URL.
async function provisionProxy(name, gb, country = "us") {
  // 1. Create one sub-user for this customer, capped in BYTES.
  const created = await fetch(`${PROXYHAT_API}/sub-users`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      proxy_password: randomPassword(),
      traffic_limit: Math.round(gb * GB),
      name,
    }),
  });
  if (!created.ok) throw new Error(`create failed: ${created.status}`);
  const subUser = (await created.json()).payload;

  // 2. Mint a ready-to-use connection URL for that sub-user.
  const desc = await fetch(`${PROXYHAT_API}/proxy-descriptors`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      sub_user_uuid: subUser.uuid,
      protocol: "socks5",
      location: { country },
      session: { mode: "rotating" },
      filter: "filter-medium",
    }),
  });
  if (!desc.ok) throw new Error(`descriptor failed: ${desc.status}`);
  return (await desc.json()).payload.url;
}

bot.start((ctx) => ctx.reply("Welcome! Send /buy to get a 5 GB US residential proxy."));

bot.command("buy", async (ctx) => {
  // ----- PAYMENT HOOK ---------------------------------------------------
  // Charge the customer FIRST, then provision. Only reach the code below
  // once payment has actually succeeded. With Telegram Stars you would send
  // an invoice here (currency "XTR") and move the provisioning call into your
  // pre_checkout_query / successful_payment handler:
  //   ctx.replyWithInvoice({ title: "5 GB proxy", description: "...",
  //     payload: "5gb", provider_token: "", currency: "XTR",
  //     prices: [{ label: "5 GB proxy", amount: 250 }] });
  // For this demo we provision immediately.
  // ----------------------------------------------------------------------
  const customer = `tg-${ctx.from.id}`;
  await ctx.reply("Provisioning your proxy...");
  const url = await provisionProxy(customer, 5, "us");
  await ctx.reply(
    `Your proxy is ready:\n\n\`${url}\`\n\nPaste it into any SOCKS5 client.`,
    { parse_mode: "Markdown" }
  );
});

bot.launch();

Set up and run it

Create a bot with @BotFather to get a TELEGRAM_BOT_TOKEN, and mint a ProxyHat API key under Dashboard → Settings → API keys. Keep both in the environment — never in the source.

# 1. Install dependencies
pip install "python-telegram-bot>=20" httpx

# 2. Set your secrets (never commit these)
export TELEGRAM_BOT_TOKEN="123456:ABC-your-botfather-token"
export PROXYHAT_API_KEY="__API_KEY__"

# 3. Run the bot
python bot.py
# 1. Install dependencies  (package.json needs "type": "module")
npm install telegraf

# 2. Set your secrets (never commit these)
export TELEGRAM_BOT_TOKEN="123456:ABC-your-botfather-token"
export PROXYHAT_API_KEY="__API_KEY__"

# 3. Run the bot
node bot.js
This is your own bot, separate from @ProxyHatBot. It authenticates to ProxyHat with your API key and provisions sub-users under your account, so every proxy it sells draws on your pool. Your customers interact only with your bot and only ever receive a proxy URL. Give each customer a stable, unique name (here tg-<telegram_id>) so you can find and renew their sub-user later.

Embed in Your Website / Storefront

To sell from a web store, put a small provisioning endpoint on your backend. After your checkout confirms payment, it creates the sub-user and returns the connection string. The ProxyHat API key stays on the server; the browser only ever sees the resulting proxy URL. Below is the same POST /provision endpoint in Express (Node) and FastAPI (Python).

# main.py  --  FastAPI (Python)
import os
import secrets
import string

import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

PROXYHAT_API = "https://api.proxyhat.com/v1"
API_KEY = os.environ["PROXYHAT_API_KEY"]        # server-side only -- never sent to the browser
GB = 1000 ** 3
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"}

app = FastAPI()


class Order(BaseModel):
    order_id: str
    gb: float = 5
    country: str = "us"


def random_password(n=20):
    alphabet = string.ascii_letters + string.digits
    return "".join(secrets.choice(alphabet) for _ in range(n))


@app.post("/provision")
async def provision(order: Order):
    # 1. Confirm THIS order is paid before provisioning. Replace is_order_paid
    #    with your real check (Stripe, Creem, ...). Never provision unpaid.
    if not await is_order_paid(order.order_id):
        raise HTTPException(status_code=402, detail="Order not paid")

    async with httpx.AsyncClient(base_url=PROXYHAT_API, headers=HEADERS, timeout=30) as api:
        # 2. Create the customer sub-user, capped in bytes.
        created = await api.post("/sub-users", json={
            "proxy_password": random_password(),
            "traffic_limit": int(order.gb * GB),
            "name": f"order-{order.order_id}",
        })
        if created.status_code != 200:
            raise HTTPException(status_code=502, detail="Provisioning failed")
        sub_user = created.json()["payload"]

        # 3. Mint the connection URL and return ONLY that to the browser.
        desc = await api.post("/proxy-descriptors", json={
            "sub_user_uuid": sub_user["uuid"],
            "protocol": "socks5",
            "location": {"country": order.country},
            "session": {"mode": "rotating"},
            "filter": "filter-medium",
        })
        if desc.status_code != 200:
            raise HTTPException(status_code=502, detail="Provisioning failed")

    return {"proxy_url": desc.json()["payload"]["url"]}
// server.js  --  Express (Node.js)
import express from "express";
import crypto from "node:crypto";

const PROXYHAT_API = "https://api.proxyhat.com/v1";
const API_KEY = process.env.PROXYHAT_API_KEY;   // server-side only -- never sent to the browser
const GB = 1000 ** 3;
const HEADERS = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
  Accept: "application/json",
};

const app = express();
app.use(express.json());

function randomPassword(n = 20) {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  return Array.from({ length: n }, () => alphabet[crypto.randomInt(alphabet.length)]).join("");
}

app.post("/provision", async (req, res) => {
  const { orderId, gb = 5, country = "us" } = req.body;

  // 1. Confirm THIS order is paid before provisioning. Replace isOrderPaid
  //    with your real check (Stripe, Creem, ...). Never provision unpaid.
  if (!(await isOrderPaid(orderId))) {
    return res.status(402).json({ error: "Order not paid" });
  }

  // 2. Create the customer sub-user, capped in bytes.
  const created = await fetch(`${PROXYHAT_API}/sub-users`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      proxy_password: randomPassword(),
      traffic_limit: Math.round(gb * GB),
      name: `order-${orderId}`,
    }),
  });
  if (!created.ok) return res.status(502).json({ error: "Provisioning failed" });
  const subUser = (await created.json()).payload;

  // 3. Mint the connection URL and return ONLY that to the browser.
  const desc = await fetch(`${PROXYHAT_API}/proxy-descriptors`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      sub_user_uuid: subUser.uuid,
      protocol: "socks5",
      location: { country },
      session: { mode: "rotating" },
      filter: "filter-medium",
    }),
  });
  if (!desc.ok) return res.status(502).json({ error: "Provisioning failed" });

  res.json({ proxy_url: (await desc.json()).payload.url });
});

app.listen(3000, () => console.log("listening on :3000"));

On the frontend, your checkout success page simply calls your own endpoint — for example await fetch("/provision", { method: "POST", body: JSON.stringify({ orderId }) }) — and displays the returned proxy_url for the customer to copy. Because provisioning happens server-to-server, the ProxyHat API key never reaches the browser. Pass an order reference your backend can verify, not the price or byte size the client could tamper with.

Renewals & Lifecycle

After the sale you manage each customer independently: top them up when they renew, reset their meter for a new cycle, and delete them to offboard. The methods below extend the API client from earlier.

The trap: buying more pool does not raise an existing customer's cap. Once you have more than one sub-user, a pool top-up never cascades — you must call top_up per customer, or their proxy stays dead at the old limit even though you paid. The one exception (an account with a single active proxy) never applies to a reseller.
# Add these methods to the ProxyHat client from above.

    def reset_usage(self, *uuids):
        """Zero the meter(s) for a new billing cycle. Does NOT refund the pool
        -- the bytes a customer already spent stay debited."""
        r = self.session.post(f"{self.BASE}/sub-users/reset-usage", json={"ids": list(uuids)})
        r.raise_for_status()
        return r.json()["payload"]

    def offboard(self, uuid):
        """Delete a customer. The password is invalidated immediately (they
        disconnect at once); the upstream account is torn down a few minutes later."""
        r = self.session.delete(f"{self.BASE}/sub-users/_", json={"uuid": uuid})
        r.raise_for_status()
        return r.json()["payload"]                    # {"deleted": true, "status": "deleting"}


# --- A monthly renewal for one customer ----------------------------------
# (Buy more pool first if you are low on traffic_available.)
ph.top_up(uuid, gb=10)        # 1) raise the cap  -- MANDATORY (the trap)
ph.reset_usage(uuid)          # 2) clear the meter for the new cycle

# Over-allocating beyond your pool is rejected:
try:
    ph.top_up(uuid, gb=99999) # more than traffic_available
except requests.HTTPError as e:
    print(e.response.status_code)  # 422 -- buy more pool before allocating
// Add these methods to the ProxyHat client from above.

  // Zero the meter(s) for a new billing cycle. Does NOT refund the pool.
  async resetUsage(...uuids) {
    const { payload } = await this.#call("POST", "/sub-users/reset-usage", { ids: uuids });
    return payload;
  }

  // Delete a customer. Password invalidated immediately; upstream torn down soon after.
  async offboard(uuid) {
    const { payload } = await this.#call("DELETE", "/sub-users/_", { uuid });
    return payload;                                // { deleted: true, status: "deleting" }
  }

// --- A monthly renewal for one customer ----------------------------------
// (Buy more pool first if you are low on traffic_available.)
await ph.topUp(uuid, 10);     // 1) raise the cap  -- MANDATORY (the trap)
await ph.resetUsage(uuid);    // 2) clear the meter for the new cycle

// Over-allocating beyond your pool throws (the #call helper turns non-2xx
// into an error): PUT /sub-users/_ -> 422 when the limit exceeds
// traffic_available. Buy more pool before allocating.
Delete is soft, and reset does not refund. Deleting a sub-user invalidates its password instantly (the customer disconnects immediately) and removes the upstream account about 15 minutes later. reset-usage only clears the meter so the customer can consume up to their cap again — the bytes they already used are gone from your pool.

Go to Production — Checklist

Next Steps