Get one service
const url = 'https://api.nsin.cloud/reseller/v1/services/1';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url https://api.nsin.cloud/reseller/v1/services/1 \ --header 'Authorization: Bearer <token>'serviceId is the numeric subscriptions.id — not a domain id and not a plan id. It is the one path parameter the tenancy middleware does not resolve, because a service is a billing row, so the handler proves ownership itself by joining users.reseller_id; a service that belongs to another reseller and one that never existed are the same 404, on purpose.
The body is the Service a row of GET /services carries, plan name, term and price included, plus domain_disabled / domain_deleted — the only place this call will tell you that the site behind a paid, active subscription is not actually being served.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Must belong to a client of the caller, or the response is 403.
Responses
Section titled “ Responses ”The service
A subscription. Not a new entity — this is the subscriptions row.
The plan is reported three ways on purpose, so a reseller can always see
which plan is active and what it cost without a second lookup:
plan_id + plan_name (which plan), duration_days (which term), and
price_rials (what the client’s wallet was actually debited).
object
Whether the domain behind this service is currently being served.
A service can be active while its domain is off. The plan is
paid for; the traffic is not flowing. Read this before reporting a
service as healthy.
Present only when domain_disabled is true. manual is yours to
lift with POST /domains/{domainId}/enable; the other two are the
system’s and that call returns 409.
Which plan is active. Free / Starter / Golden / Enterprise.
The term that was bought. The same plan at a different duration is a different price.
NSIN list price of this term — what the client’s wallet was
debited at purchase. 0 for the Free plan, which is a real,
purchasable plan (the debit is simply a no-op).
Mirrors subscriptions.status exactly.
There is deliberately no suspended value. Suspension is not a
subscription state in NSIN — it is domains.suspended, a boolean on the
domain, and that is the thing the edge actually checks before serving.
So: expired and cancelled mean “no plan, still serving”. They are
not an off switch. The off switch is
POST /domains/{domainId}/disable.
A domain in that state has an allowance of zero and — because a reseller’s client has no pay-as-you-go — is suspended by the quota sweep as soon as it serves a byte. It stops shortly; it does not run up a bill. But “shortly” is not “now”, which is why the explicit domain suspend exists.
TODO (deferred): a first-class
suspendedstatus on the subscription itself, so a service can be frozen without touching the domain row. Not needed for v1 — domain suspend already stops serving and stops the meter, which is the entire business requirement. See the Deferred table inRESELLER_PLAN_V2.md.
Example
{ "domain_name": "example.com", "domain_disabled_reason": "manual", "plan_id": 3, "plan_name": "Golden", "plan_term_id": 14, "duration_days": 365, "price_rials": 120000000, "status": "active"}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.
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
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 fromPOST /servicesandPOST /services/{serviceId}/change-plan. Your wallet, not the client’s: funding the purchase would take you past your overdraft (default0, 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 untilPOST /domains/{domainId}/enable.unknown_rule_type— 404 from the rules paths whenruleTypeis 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.
Example
{ "error": "forbidden", "code": "credit_limit_reached"}No such resource.
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
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 fromPOST /servicesandPOST /services/{serviceId}/change-plan. Your wallet, not the client’s: funding the purchase would take you past your overdraft (default0, 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 untilPOST /domains/{domainId}/enable.unknown_rule_type— 404 from the rules paths whenruleTypeis 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.
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.
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
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 fromPOST /servicesandPOST /services/{serviceId}/change-plan. Your wallet, not the client’s: funding the purchase would take you past your overdraft (default0, 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 untilPOST /domains/{domainId}/enable.unknown_rule_type— 404 from the rules paths whenruleTypeis 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.
Example
{ "error": "forbidden", "code": "credit_limit_reached"}