Skip to content

Add a domain for a client

POST
/reseller/v1/clients/{clientId}/domains
curl --request POST \
--url https://api.nsin.cloud/reseller/v1/clients/1/domains \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "example.com", "dns_mode": "managed" }'

Only the reseller can do this. A client with a reseller_id gets 403 from POST /domains/ in the panel, and the “add domain” button is hidden.

The reason is money: domains generate billable traffic against a wallet the reseller funded. Letting the client add domains freely would let him spend his reseller’s money on decisions the reseller never approved.

Sets domains.reseller_id = <caller> to match the owner’s users.reseller_id. These two must never disagree.

The domain arrives with NO plan

A domain added through the panel gets a free 10-day Enterprise trial. One added here does not, and you should be glad: the trial is an active subscription, so it would make your very next call — POST /services — fail with 409 already_has_active_plan. Provisioning would break on step two, every time.

So the normal flow is two calls, in this order:

  1. POST /clients/{clientId}/domains
  2. POST /services — buy the plan

Until step 2 the domain has a zero traffic allowance, and because a reseller’s client has no pay-as-you-go, the quota sweep will stop it as soon as it serves a byte. Buy the plan.

clientId
required
integer

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

Media type application/json
object
name
required
string
dns_mode
string
default: managed
Allowed values: managed external

Created. The domain starts in pending until NS delegation is confirmed.

Media type application/json
object
id
required
integer
name
required
string
user_id
required

The owning client.

integer
status
required

NS-delegation state, driven by the checker — not a billing state. moved means the nameservers stopped pointing at NSIN.

string
Allowed values: pending active moved disabled
suspended
required

Billing/administrative stop. This, not status, is what decides whether the site is served. Set by the negative-balance ladder, or manually via POST /domains/{domainId}/disable.

boolean
suspended_at
string format: date-time
nullable
suspended_reason

WHY the domain is suspended. Empty when suspended is false.

Read this before offering a resume button. Only manual can be lifted with POST /domains/{domainId}/enable; the other two are the system’s own and that call returns 409:

  • manual — you switched it off. Nothing auto-resumes it, which is the point: a client suspended for not paying must not come back online because a quota period rolled over.
  • quota_exceeded — the client used up his plan’s included traffic. A reseller’s client has no pay-as-you-go, so this is his hard stop. Resolve it by moving him to a bigger plan; the quota sweep then resumes the domain by itself.
  • negative_balance — a wallet stayed negative past its grace window. On a client’s domain that is usually yours, not his: the tenancy sweep suspends every domain under a reseller who is in debt, and a client’s own balance has nothing that can drive it below zero. Top your balance back up and billing resumes the domains it suspended.
string
Allowed values: "" manual quota_exceeded negative_balance
dns_mode
string
Allowed values: managed external
created_at
string format: date-time
Example
{
"name": "example.com",
"status": "pending",
"suspended_reason": "",
"dns_mode": "managed"
}

Malformed or invalid input.

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

Domain already exists.

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