Reseller API
Run your clients from your own product: create them, set what they can do, fund them with credits, verify them, and buy them numbers.
A reseller account owns client accounts. Each client is a separate organization with its own balance, its own agents and its own numbers, and you decide what each one may do and what it pays per minute.
This API is that control panel, so your clients never have to see ours. Every call names one of
your clients by user_id or child_organization_id, and only ever one of your own: any other
id is rejected.
The shape of it
Nine calls, in this order. Every operation page in the journey carries this strip with its own step lit, so you can always see where you are.
Authentication
Send your reseller key as a bearer token on every request. See Authentication.
Authorization: Bearer YOUR_API_KEYThe key must belong to the reseller account owner or an admin on it. A teammate with viewer access is refused.
Your clients
List everything under your account, with each organization's balance, rate, concurrency limit and users:
GET /api/v1/reseller/organizationsCreate a client. This makes the user and their organization together, and links it to you:
POST /api/v1/reseller/users/add
{
"name": "Demo User",
"email": "demo@example.com",
"phone": "+15551234567",
"password": "...",
"cost_per_min": 0.20,
"concurrent_call_limit": 2,
"expiry_date": "2026-12-31",
"user_currency": "USD"
}Only the first four are required. The rest are the terms you are selling on, and you can change
them later. cost_per_min sets both the basic and the premium model rate for that client.
What a client can do
Three things are yours to set, and a client cannot change any of them itself.
Dashboard access. Turn menu areas on and off. Only the flags you pass change, and flags outside your own permissions are ignored rather than escalating.
POST /api/v1/reseller/users/access-control
{ "user_id": 1234, "dashboard_menu_access": { "is_bots_menu_access": true } }Expiry. Set a date the account stops working, or clear it.
POST /api/v1/reseller/users/expiry
{ "user_id": 1234, "expiry_date": "2026-12-31" }Pass null to remove the expiry.
Concurrency. How many calls a client can run at once.
POST /api/v1/reseller/concurrency
{ "child_organization_id": 5678, "new_limit": 5 }new_limit is the absolute number you want, not a change to the current one. Assigning is
free: any figure is accepted, nothing is deducted from you, and the call never fails for lack
of capacity.
Credits
You hold a balance. You move minutes from it to a client at a rate you choose, and you can take unused minutes back.
Two different amounts move on every transfer: your balance is debited at your rate, and the client is credited at the rate you set. The gap is your margin. A revert is the exact mirror.
Preview either direction before committing. Nothing moves on this call:
POST /api/v1/reseller/credits/calculate
{ "minutes": 100, "cost_per_min": 0.20 }Then transfer. Your balance is debited immediately, and a cost_per_min below your own rate is
refused as a loss:
POST /api/v1/reseller/credits/transfer
{ "to_organization_id": 5678, "minutes": 100, "cost_per_min": 0.20 }To take unused minutes back, pass only how many. No rate: the client is deducted at their current rate and you are refunded at yours:
POST /api/v1/reseller/credits/revert
{ "from_organization_id": 5678, "minutes": 40 }Every move is recorded:
GET /api/v1/reseller/credits/logsCarriers in a region
A region can hold more than one carrier. They do not stock the same numbers and they do not run
the same verification, so carrier is required on every call that touches numbers or
verification, in every region, however few carriers it holds. Requiring it always is
deliberate: it means we can add a carrier without breaking anything you have already written.
| Region | carrier | Stocks | Verification |
|---|---|---|---|
IN | carrier-1 | Landline numbers, city codes 11, 12 and 80 | OTP pair, then Aadhaar by OTP, ending in a preview you accept |
IN | carrier-2-new | Mobile numbers, 94 and 79 series | No OTP at all. Aadhaar through a DigiLocker link your customer opens |
US | carrier-us | US local numbers, by area code | Business details, address, an authorized representative |
You do not have to hard-code that table, and you should not. Call without a carrier and the
refusal is the directory, listing that region's carriers with what each one stocks, which is
how you pick up a carrier we add later without shipping anything:
{
"error": "carrier_required",
"region": "IN",
"carriers": [
{ "carrier": "carrier-1", "label": "Carrier 1",
"description": "Landline numbers, 80 series.",
"kyc_required": true, "unavailable": false },
{ "carrier": "carrier-2-new", "label": "Carrier 2 (new)",
"description": "Mobile numbers, 94 and 79 series.",
"kyc_required": true, "unavailable": false }
]
}The name is stable across a rename, so it is safe to store. A carrier with unavailable: true
is not taking orders right now: sell on another one meanwhile, and expect a
422 purchase_failed if you try it anyway.
Two consequences worth planning for, because they catch people out:
- Verification does not carry across carriers. A client verified on
carrier-1still has to verify oncarrier-2-newbefore it can buy a mobile number, and the two flows are not the same shape. - Buy from the carrier you searched. The search response names its
carrier; pass that same value to the purchase, or you are buying from inventory you never looked at.
Verify a client
Where a region requires it, a client has to be identity-verified before it can buy a number. For India that is an Aadhaar eKYC, so each check clears in minutes.
Verification is per carrier. A client verified on one carrier of a region still has to verify on the other before it can buy there, and the two do not have the same steps.
You do not hardcode the order of the checks. You ask which step is next, run it, and the response tells you the one after it. That way the sequence can change without breaking your integration.
Read the status once to get the first step, then follow next_step until the status is completed.
completed, verification is done and the client can buy. That is the same state the
purchase endpoint checks, so there is nothing else to poll.GET /api/v1/reseller/kyc/status?user_id=CLIENT_IDThe response has a regions array, one entry per carrier, each with that carrier's status,
its next_step, and whether the client can_purchase on it yet.
carrier = "carrier-1"
status = GET /kyc/status?user_id=CLIENT_ID
next = status.regions.find(r => r.carrier == carrier).next_step
while next:
step = POST /kyc/steps/{next} { user_id: CLIENT_ID, region: "IN", carrier: carrier, ...fields }
next = step.next_step
# status is completed. The client can now buy a number on that carrier.The steps
One endpoint runs every step: the step name is the last part of the path, and the fields it needs go in the body.
Each step's body fields, the next_step it answers with, and the message it returns are listed
in the step reference. Steps differ per
carrier, so read them as data for the carrier you are on and your integration never hardcodes
them:
GET /api/v1/reseller/kyc/requirements?region=IN&carrier=carrier-1Every step comes back with a method, and that is the one thing to branch on. submit posts
the required fields. otp posts a code the client received. redirect hands you a URL to open
in the client's own browser, plus the step to poll for the outcome:
{
"status": "aadhaar_pending",
"method": "redirect",
"redirect_url": "https://provider.example/verify/one-time-link",
"reusable": false,
"poll_step": "aadhaar-status"
}Two things the sequence does not show. verify-gst and skip-gst are a choice, run one or the
other. And if the client says they did not get their code, call resend-otp while they are on
the OTP step: it is never returned as a next_step, so it sits outside the loop. The client
holds two codes at this step, one in their email inbox and one read out to them on a voice
call, and type (email, mobile, or both, default both) picks which to send again. A
client who says "I never got the call" needs { "type": "mobile" }, nothing more.
Your obligation on the data
You are relaying what your client types into your interface, nothing more. Do not store the values you send in these requests, and redact them from your logs. That covers the PAN, the Aadhaar number, and every one-time code.
Buy them a number
Once a client is verified on a carrier, buying is not a separate reseller endpoint: it is the
ordinary number API with your client's user_id added. The number lands on their account and the
rental comes out of their balance, not yours.
GET /api/v1/phone_number/search?region=IN&carrier=carrier-1&user_id=CLIENT_ID
POST /api/v1/phone_number/purchase { "region": "IN", "carrier": "carrier-1", "phone_number": "+91...", "user_id": CLIENT_ID }Buy on the carrier the client verified on: verification does not carry across carriers.
A client who completed verification in the dashboard instead of through this API passes the same gate, so there is nothing to repeat.
Then attach it to one of their agents with attach and the client can take calls on it. The same flow, written out call by call with the money and reservation details, is in Buy a number over the API.
A runnable version of this page
Reseller onboarding
in the examples repo walks the whole thing in Python: create the client, fund
it, verify it on a carrier, buy a number, attach it. It prints the plan and
calls nothing until you pass --live, and there is one answer file per carrier
so you can see what each one asks for.
Reference
Full request and response schemas, with a playground, are in the API reference.
