MCP is live.Set upAsk on Discord
OmniDimension

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.

createthe accountfundcredits, at your ratecomplete eKYCwhere requiredbuy a numberthen it can call
Verification only applies where the region asks for it. Everything else is the same for every client.

Authentication

Send your reseller key as a bearer token on every request. See Authentication.

Authorization: Bearer YOUR_API_KEY

The 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/organizations

Create 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.

your balancethe clienttransfer: you pay your rate, they get your sell raterevert: they are deducted at their rate, you get yours back
A revert needs no rate from you: the client is deducted at their current rate, which is what they were charged, and you are refunded at yours, which is what it cost you.

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/logs

Carriers 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.

RegioncarrierStocksVerification
INcarrier-1Landline numbers, city codes 11, 12 and 80OTP pair, then Aadhaar by OTP, ending in a preview you accept
INcarrier-2-newMobile numbers, 94 and 79 seriesNo OTP at all. Aadhaar through a DigiLocker link your customer opens
UScarrier-usUS local numbers, by area codeBusiness 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-1 still has to verify on carrier-2-new before 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.

GET /kyc/statusthe first next_stepPOST /kyc/steps/{next_step}next_stepno next stepstatus: completedthe client can buy a number
One read to start, then each step response hands you the next one. When a step comes back 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_ID

The 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-1

Every 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.

On this page