Skip to content

List domain subscriptions across all my clients

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

The OPERATING view of the same rows GET /services sells: what is live, what expires when, and whose it is. Kept separate rather than overloading one list with two sets of filters.

Prices are absent by design — what you charge your customer is not something NSIN stores, and your wholesale cost belongs on your wallet rather than on every row of an operations table.

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

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 suspended status 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 in RESELLER_PLAN_V2.md.

string
Allowed values: active grace expired cancelled
expiring_days
integer
>= 1 <= 3650

Only subscriptions expiring within this many days.

A page of subscriptions

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

A domain subscription seen from the operating side: what is live, when it expires, and whose it is. The same underlying row as Service, without the selling fields.

object
id
required
integer
domain_id
required
integer
domain_name
required
string
client_id
required
integer
plan_id
required
integer
plan_name
string
nullable
status
required

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 suspended status 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 in RESELLER_PLAN_V2.md.

string
Allowed values: active grace expired cancelled
auto_renew
required
boolean
started_at
string format: date-time
nullable
expires_at
string format: date-time
nullable
meta
required
object
page
required
integer
per_page
required
integer
total
required
integer
Example
{
"data": [
{
"domain_name": "example.com",
"status": "active"
}
]
}

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

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

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