Skip to content

A client's wallet balance and ledger

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

Visible to the reseller only. The client themselves gets 403 on every wallet endpoint — they are never shown a balance, a price, or an invoice.

clientId
required
integer

Must be a client of the caller, or the response is 403.

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

Balance and a page of transactions

Media type application/json
object
balance_rials
required

What has been transferred in, less what his plans cost.

Traffic cannot push it below zero. A reseller’s client has no pay-as-you-go: his overage is recorded with charged_rials = 0, no wallet is debited, and the quota sweep suspends his domain at the plan’s limit instead.

A negative number here is therefore not a traffic bill and not a normal state — nothing in the reseller flow debits a client wallet it has not just funded. Read negative_since for what happens next.

integer format: int64
negative_since
required

When the balance first went below zero. A reseller’s client gets no grace at all — he has no wallet he can reach and no way to settle — so the next negative-balance pass stops every domain he owns, and the reseller gets the SMS, not the client. Credit him back to zero (or let a purchase auto-fund him) and the same ladder resumes what it stopped.

string format: date-time
nullable
transactions
required
Array<object>

One row of the append-only ledger. Historical rows are never updated — a correction is a new, compensating entry.

object
id
required
integer
type
required

E.g. recharge, traffic, subscription, transfer_in, transfer_out.

string
amount_rials
required

Negative for a debit, positive for a credit.

integer format: int64
balance_after
required
integer format: int64
description
string
created_at
required
string format: date-time
meta
required
object
page
required
integer
per_page
required
integer
total
required
integer
Example
{
"balance_rials": 4500000
}

Authenticated, but the target does not belong to you — or you are not a reseller at all.

This is the response the tenant-isolation matrix asserts on. Every route in this document is called with reseller B’s credential against reseller A’s ids, and must answer 403 or 404 with none of A’s data in the body.

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"
}

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"
}