Skip to content

List my clients

GET
/reseller/v1/clients
curl --request GET \
--url 'https://api.nsin.cloud/reseller/v1/clients?page=1&per_page=25' \
--header 'Authorization: Bearer <token>'

Every user whose users.reseller_id is the caller, newest first (id DESC) — a direct NSIN customer, or another reseller’s client, is outside the query that builds the page rather than filtered out of it. q is a raw substring match against email, phone_number and name, any one of which may hit; phone numbers are stored normalized (09123456789), so searching for +98912… finds nothing. Disabled accounts come back like any other — there is no active filter — so read active per row.

Each row carries wallet_balance_rials, the number the client himself is never shown. A balance that fails to load is rendered as 0 and logged, so a zero here is not proof of an empty wallet. per_page above 100 is clamped rather than rejected: ask for 1000 and you silently get 100, so drive your loop off meta.total.

page
integer
default: 1 >= 1
per_page
integer
default: 25 >= 1 <= 100
q
string

Free-text match on name, email, or phone.

A page of clients

Media type application/json
object
data
required
Array<object>

An end user owned by the calling reseller.

object
id
required
integer
name
string
nullable
email
required
string format: email
phone_number
required
string
phone_verified
required

Whether the panel will accept this number without challenging it. true for a freshly provisioned client — you vouched for it. See phone_verified_by for the basis.

boolean
phone_verified_by

How the number came to be verified.

  • otp — the client consumed a one-time code sent to this exact number. Proof that somebody holds the handset.
  • provider — you entered it and vouched for it. Trust in you, not proof of the handset.
  • admin — set by NSIN staff.
  • "" — not verified.

Changing a client’s phone via PATCH /clients/{clientId} re-bases this on provider: an OTP consumed against the old number proves nothing about the new one, but your vouching still applies.

string
Allowed values: "" otp provider admin
national_code
string
birth_date
string format: date
nullable
active
required

false = account disabled; cannot log in.

boolean
wallet_balance_rials

Visible to the reseller. The client is never shown this, and gets 403 from every wallet endpoint.

integer format: int64
created_at
required
string format: date-time
meta
required
object
page
required
integer
per_page
required
integer
total
required
integer
Example
{
"data": [
{
"phone_number": "09123456789",
"phone_verified_by": "",
"national_code": "0012345678"
}
]
}

Missing, malformed, expired, or revoked credential.

Media type application/json

Every error body carries error, a human-readable sentence. Some also carry code, a stable machine-readable reason — branch on that, never on the sentence, which is prose and gets reworded.

object
error
required
string
code

Present only on the failures worth branching on, and deliberately not an exhaustive enum: treat a code you do not recognise as if it were absent and fall back to the status code.

The ones that exist today:

  • credit_limit_reached — 402 from POST /services and POST /services/{serviceId}/change-plan. Your wallet, not the client’s: funding the purchase would take you past your overdraft (default 0, i.e. past zero). Top up; do not credit the client.
  • domain_disabled — 409 from a configuration write on a disabled domain (records, rules, cache, SSL, members, settings). The domain is read-only until POST /domains/{domainId}/enable.
  • unknown_rule_type — 404 from the rules paths when ruleType is not one of the documented values. You get this rather than an empty array, so a typo cannot read as a rule set that happens to be empty.
  • rate_limited — 429 from any route. Keyed per credential, so one runaway integration cannot lock its owner out of the dashboard.
  • session_check_failed — 503, and only on calls made with a dashboard session JWT. The session could not be verified, which is not the same as knowing it is revoked, so it is retryable and the session survives. Machine tokens never see this.
string
Example
{
"error": "forbidden",
"code": "credit_limit_reached"
}

The caller’s request budget is spent. 300 requests per minute by default (RESELLER_RATE_LIMIT), and the same limiter covers every one of the 83 operations in this document.

Counted per credential, not per reseller: each nsin_live_ token has its own budget and a dashboard session has another, so one runaway integration cannot lock its owner out of his own panel, and revoking that token is enough to stop it.

Branch on "code": "rate_limited" in the body. The message beside it is written for a human and may be reworded.

Retry-After is set, in seconds until the window resets — wait that long rather than retrying at once. The X-RateLimit-* headers are not on this response; they appear only on the responses the limiter let through, so a client that reads its remaining budget from the 429 alone will never see one.

Media type application/json

Every error body carries error, a human-readable sentence. Some also carry code, a stable machine-readable reason — branch on that, never on the sentence, which is prose and gets reworded.

object
error
required
string
code

Present only on the failures worth branching on, and deliberately not an exhaustive enum: treat a code you do not recognise as if it were absent and fall back to the status code.

The ones that exist today:

  • credit_limit_reached — 402 from POST /services and POST /services/{serviceId}/change-plan. Your wallet, not the client’s: funding the purchase would take you past your overdraft (default 0, i.e. past zero). Top up; do not credit the client.
  • domain_disabled — 409 from a configuration write on a disabled domain (records, rules, cache, SSL, members, settings). The domain is read-only until POST /domains/{domainId}/enable.
  • unknown_rule_type — 404 from the rules paths when ruleType is not one of the documented values. You get this rather than an empty array, so a typo cannot read as a rule set that happens to be empty.
  • rate_limited — 429 from any route. Keyed per credential, so one runaway integration cannot lock its owner out of the dashboard.
  • session_check_failed — 503, and only on calls made with a dashboard session JWT. The session could not be verified, which is not the same as knowing it is revoked, so it is retryable and the session survives. Machine tokens never see this.
string
Example
{
"error": "forbidden",
"code": "credit_limit_reached"
}
Retry-After
integer

Seconds until the current window resets.

Something failed on our side. The body carries a human sentence and never an internal detail — a live sweep of this API once returned dial tcp 127.0.0.1:9000: connect: connection refused, which is our topology rather than an error message. That is now impossible.

Distinguish it from 503. A 503 means a dependency is down and the identical request will succeed later, so retry it. A 500 means the request hit a genuine fault: retrying it unchanged will fail the same way, and it should be reported with the X-Request-Id from the response header.

Declared on every operation because every operation can reach it. It was previously declared on exactly one, which left a generated client with no branch for the answer it is most likely to be surprised by.

Media type application/json

Every error body carries error, a human-readable sentence. Some also carry code, a stable machine-readable reason — branch on that, never on the sentence, which is prose and gets reworded.

object
error
required
string
code

Present only on the failures worth branching on, and deliberately not an exhaustive enum: treat a code you do not recognise as if it were absent and fall back to the status code.

The ones that exist today:

  • credit_limit_reached — 402 from POST /services and POST /services/{serviceId}/change-plan. Your wallet, not the client’s: funding the purchase would take you past your overdraft (default 0, i.e. past zero). Top up; do not credit the client.
  • domain_disabled — 409 from a configuration write on a disabled domain (records, rules, cache, SSL, members, settings). The domain is read-only until POST /domains/{domainId}/enable.
  • unknown_rule_type — 404 from the rules paths when ruleType is not one of the documented values. You get this rather than an empty array, so a typo cannot read as a rule set that happens to be empty.
  • rate_limited — 429 from any route. Keyed per credential, so one runaway integration cannot lock its owner out of the dashboard.
  • session_check_failed — 503, and only on calls made with a dashboard session JWT. The session could not be verified, which is not the same as knowing it is revoked, so it is retryable and the session survives. Machine tokens never see this.
string
Example
{
"error": "forbidden",
"code": "credit_limit_reached"
}