Skip to content

Transfer credit from my wallet to a client's wallet

POST
/reseller/v1/clients/{clientId}/credit
curl --request POST \
--url https://api.nsin.cloud/reseller/v1/clients/1/credit \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: example' \
--data '{ "amount_rials": 5000000, "description": "example" }'

Moves Rials from the calling reseller’s wallet into the client’s wallet.

This call is optional. POST /services performs the same transfer itself, for exactly the shortfall the purchase needs, inside the purchase transaction — a client never has to be pre-funded to be provisioned. Pre-crediting him only makes that automatic top-up smaller, or a no-op. Use it if you want the balance sitting there in advance; skip it and provisioning still works.

Atomic: one debit and one credit, in a single transaction, or neither. Both wallet rows are locked FOR UPDATE in ascending id order to avoid deadlock under concurrent transfers.

This call cannot overdraw you. It debits your wallet only if the full amount is already there, and answers 402 otherwise. The overdraft an admin may have granted you (reseller_max_negative_rials, default 0) is reachable through a PURCHASE, not through this transfer.

So a reseller’s wallet CAN go negative, and there IS a reseller-side suspension policy: stay below zero past the configured grace window (reseller_negative_grace_days) and every domain in your tenancy — your own and all your clients’ — is suspended. Bringing the balance back to zero or above resumes the domains that ladder suspended; a domain you disabled by hand stays disabled.

A rejected transfer writes no ledger row. Not a reversed one. None.

The pair of rows this writes carries ref_type: reseller_transfer; the automatic top-up under POST /services writes reseller_autofund. That is how you tell the two apart when reconciling a ledger.

clientId
required
integer

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

Idempotency-Key
required
string
<= 191 characters

Required on every endpoint that moves money.

A client-generated unique string (a UUID is fine). Replaying the same key returns the original response byte for byte and does not repeat the operation. A replayed response is marked with the response header Idempotent-Replay: true.

This is not optional politeness. Without it, a network timeout on a credit or createService call leaves the caller unable to tell whether the money moved, and a naive retry double-charges.

A key never expires. There is no TTL and nothing prunes the table: the record is written inside the same transaction as the money movement and then kept forever, so a key is burned permanently. The same key sent a year later still replays the original answer instead of transferring again. Generate one key per operation — never one per process, per day, or per client.

Keys are scoped to the calling reseller, so two partners may pick the same UUID without colliding, and neither can probe the other’s key space.

Reuse with a different request is refused rather than replayed: same key, different method, path or body is 422. See the IdempotencyConflict response.

A failed operation records nothing, because the record and the money share one transaction and roll back together. A transfer that bounced for insufficient funds is therefore freely retryable with the same key once the wallet is funded.

The limit is 191 characters, not the 128 this document used to claim — it is the width of the database column, and it is the only validation performed on the value. The check runs on the trimmed string and counts bytes, so a non-ASCII key runs out before 191 characters; over-length is 400.

Media type application/json
object
amount_rials
required

Must be positive. To claw credit back, see debit.

integer format: int64
>= 1
description

Shown on both ledger rows.

string
<= 191 characters

Transferred — or, if this Idempotency-Key was already used for this exact request, the first call’s response replayed unchanged.

Media type application/json
object
amount_rials
required
integer format: int64
reseller_balance_rials
required

My balance after the transfer.

integer format: int64
client_balance_rials
required

The client’s balance after the transfer.

integer format: int64
Example generated
{
"amount_rials": 1,
"reseller_balance_rials": 1,
"client_balance_rials": 1
}
Idempotent-Replay
string
Allowed values: true

Sent as true only on a replay: the body is the stored answer from the first call with this Idempotency-Key, and the operation did NOT run a second time. Absent on the call that actually did the work — which is how a retrying client tells which of its attempts moved the money.

A malformed body, or a value the handler rejects outright.

Also the answer to a missing Idempotency-Key: the generated wrapper rejects that before the handler is entered, so the body carries the wrapper’s message rather than the handler’s. A key longer than 191 bytes is rejected by the handler, with a message saying so.

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

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

Insufficient funds in the reseller’s wallet: this transfer is checked against the balance you actually hold, with no overdraft. No ledger row was written, and no idempotency key was recorded — retry with the same Idempotency-Key once you have topped up.

The body has no code. A 402 from POST /services is a different failure and carries "code": "credit_limit_reached".

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

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

No such resource.

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

This Idempotency-Key has already been used, for a different request.

A replay is only a replay if the method, path and body all match: they are hashed together into a fingerprint stored beside the recorded response, and a key that comes back with a different fingerprint is a caller bug — most often a key generated once per process instead of once per operation.

422 rather than 409 because the request is well-formed and authorized and no resource is in a conflicting state; it is the key that cannot be processed. Replaying the stored answer instead would tell the caller his SECOND transfer succeeded when it never ran, and he would report a client as funded who is not.

Nothing here is retryable until the key changes. The original operation stands, the new one never ran, and the fix is a fresh key.

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