Reseller API
The Reseller API is how a partner runs his own customers on NSIN from his own systems — his website, his billing panel, or a WHMCS install — without anybody logging into a dashboard.
- Base URL:
https://api.nsin.cloud - Prefix: every path in this API begins
/reseller/v1 - Authentication: a machine token you mint yourself
- Full endpoint reference: Reseller API Reference
- Swagger UI: try the endpoints — sends real requests
- Standalone explorer: single-page reference
- Machine-readable:
/docs/reseller-v1.yaml
This is a different product from the customer REST API.
Different audience, different credential, different prefix. A panel API key
(nsin_…) cannot call it, and a reseller token cannot call the customer API.
Who NSIN is
Section titled “Who NSIN is”NSIN is the provider, not a reseller. There is no “reseller #1”. A direct
NSIN customer has reseller_id = null; your clients carry yours.
Authenticate
Section titled “Authenticate”Mint a token, then send it as a bearer credential:
curl -H "Authorization: Bearer nsin_live_xxxxxxxxxxxx_yyyy…" \ https://api.nsin.cloud/reseller/v1/pingGET /reseller/v1/ping is the cheap liveness-and-credentials check. WHMCS’s
TestConnection maps onto it.
A machine token looks like this:
nsin_live_<key_id>_<secret> └ public ┘ └ secret ┘The key_id is stored in plaintext and is safe to display and to log — it is how
NSIN looks the token up. The secret is stored only as a bcrypt hash and is shown
exactly once, when you mint it. Nothing in the system can recover it later; if
you lose it, revoke the token and mint another.
Two kinds of credential arrive in that same Authorization: Bearer header, and
the middleware tells them apart by prefix:
| Credential | Used by | How it is checked |
|---|---|---|
nsin_live_… | machine clients — WHMCS, your own site | key_id looked up, secret verified against its bcrypt hash |
| anything else | the reseller dashboard | parsed as a session JWT |
Either way the handler ends up with the same caller identity, and every handler checks that the client or domain you named is yours before touching a row.
You top up your own wallet at face value through the normal Zarinpal flow — that is not part of this API. Everything your clients buy is ultimately paid for out of that balance, at NSIN list price. There is no reseller discount at purchase time, ever.
Your client’s wallet is real, but he can never see or reach it: every wallet,
invoice, price and ticket endpoint returns 403 for a user who has a
reseller_id.
Every monetary field is an integer count of Rials (*_rials). Divide by 10 to
display Toman. Never send a float.
Buying a plan funds the client automatically
Section titled “Buying a plan funds the client automatically”You do not have to pre-fund a client before buying his plan. Creating or
changing a service works out what the client’s wallet is short, moves exactly
that much from your wallet to his, and then charges him — all inside one database
transaction. The transfer appears in both ledgers as reseller_autofund.
POST /reseller/v1/clients/{clientId}/credit is still there and still useful —
for putting a float on an account ahead of time, or for topping one up outside a
purchase. It simply is not a prerequisite. Pre-funding just makes the automatic
transfer a no-op.
Unlike a purchase, credit never overdraws you: you cannot transfer more than
your current balance.
The 402 you will actually see
Section titled “The 402 you will actually see”When a purchase fails for money, the wallet that came up short is yours, not your client’s:
{ "error": "This purchase would take your wallet past your credit limit. Top up, or ask NSIN to raise the limit.", "code": "credit_limit_reached" }Top up your own wallet through Zarinpal, or ask NSIN to raise your limit.
Crediting the client will not help — and will 402 as well.
NSIN can grant you an overdraft (a negative balance you are allowed to run). It is zero by default, which means that out of the box any purchase you cannot afford returns exactly the error above. If you do have one and you stay negative past your grace window, every domain in your tenancy is suspended — yours and your clients’.
Your clients are capped, not metered
Section titled “Your clients are capped, not metered”A reseller’s client has no pay-as-you-go. Traffic beyond his plan’s included GB is not billed to anyone: the usage row is recorded at zero and no wallet is touched. When he reaches the plan’s limit he is suspended instead.
So a client’s balance is never driven negative by traffic, and you should not budget for metered overage revenue on top of a plan — there is none. Sell the next plan up.
Idempotency
Section titled “Idempotency”Every endpoint that moves money requires an Idempotency-Key header.
curl -X POST https://api.nsin.cloud/reseller/v1/clients/7/credit \ -H "Authorization: Bearer $NSIN_RESELLER_TOKEN" \ -H "Idempotency-Key: 5f9c1b7e-3a2d-4c11-9d40-6f2f0a1b8e33" \ -H "Content-Type: application/json" \ -d '{"amount_rials": 5000000}'The key is any client-generated unique string; a UUID is fine. Keys longer than 191 characters are rejected. Replaying a key returns the original response and does not repeat the operation.
This is not optional politeness. A network timeout on a credit call leaves your
server unable to tell whether the money moved, and a naive retry double-credits —
which is precisely what a timeout is.
Four properties worth building against:
- Replays are permanent, not windowed. There is no expiry: a key you used a year ago still replays its original response today. Generate a fresh key per logical operation and never recycle them on a timer.
- A replay is labelled. The repeated response carries the header
Idempotent-Replay: true. That is the only way to tell a replay from a fresh execution — the status and body are identical by design. - A failed operation records nothing, so it is freely retryable with the same key. If a purchase bounced because your wallet was short, top up and retry with the same key and it will go through.
- Reusing one key for a different request is
422, not a replay. The key is checked against the endpoint and a hash of the request body. Answering a different request with a stored response would tell you a second transfer succeeded when it never ran.
Keys are scoped to you, so two partners choosing the same UUID never collide.
Rate limits
Section titled “Rate limits”300 requests per minute per credential, by default.
The budget is keyed on the credential, not on the reseller — one runaway integration will not lock you out of your own dashboard, and revoking that token is enough to stop it.
Over the budget, the API answers 429:
{ "error": "Rate limit exceeded. Slow down and retry shortly.", "code": "rate_limited" }Back off and retry; honour Retry-After when it is present. If you are driving a
bulk import, prefer the batch endpoints (records/import,
records/scan-import) over a per-record loop — a fan-out over a zone with a real
number of records will trip this.
Pagination
Section titled “Pagination”List endpoints take page (from 1) and per_page (1–100, default 25), and return
the rows under data with a meta object:
{ "data": [ … ], "meta": { "page": 1, "per_page": 25, "total": 137 }}Errors
Section titled “Errors”Every error body is {"error": "human readable message"}, sometimes with a
machine-readable "code".
| Status | Means |
|---|---|
400 | Malformed or invalid input, including a missing Idempotency-Key on a money endpoint. |
401 | Missing, malformed, expired or revoked credential. |
402 | Your wallet cannot cover it — "code": "credit_limit_reached". Top up your own balance; crediting the client does not help. |
403 | Authenticated, but the target is not yours — or you are not a reseller at all. |
404 | No such resource — and also what a resource belonging to another reseller looks like, deliberately indistinguishable. |
409 | The resource is in a state that cannot accept this. "code": "domain_disabled" is the common one. |
422 | An Idempotency-Key was reused with a different request body. |
429 | Rate limited — "code": "rate_limited". |
503 | The analytics store is unreachable — deliberately an error rather than a page of zeros, which is indistinguishable from “your customers’ traffic stopped”. |
Disabled, not suspended
Section titled “Disabled, not suspended”In reseller-facing language an account or domain is disabled and enabled —
never “suspended”. A disabled domain is read-only: it keeps its configuration
and you can still read every part of it, but writes answer 409 until it is
enabled again.
That is 409 rather than 403 on purpose. You have every right to make the
change; the domain is simply in a state that cannot accept it, and telling a
partner “you don’t have permission” would send him to entirely the wrong
conclusion.
You cannot delete a client
Section titled “You cannot delete a client”There is no route for it — not a guarded one, no route at all. Disable the account instead.
Isolation
Section titled “Isolation”Every route in this API is exercised by a tenant-isolation matrix: called with
one reseller’s credential against another reseller’s ids, it must answer 403 or
404 with none of the other party’s data in the body.
What your clients never receive
Section titled “What your clients never receive”All business SMS and email about your client — domain down, plan expiring, balance negative — is redirected to you, not to him.
Two things are deliberately exempt and still go to the client: login OTP and password reset. Both are account recovery, and suppressing either would lock your client out of the panel permanently.
Versioning
Section titled “Versioning”The prefix is /reseller/v1. Additive changes — a new endpoint, a new optional
field, a new enum member on a response — ship inside v1 and will not be
announced as breaking. Removing a field, tightening a type, or changing a status
code would mean /reseller/v2.
Build your client to ignore response fields it does not recognise.