Domain Reseller API Reference
An HTTP interface for registering, renewing and managing domains at your reseller tier price, designed to be driven automatically from your own billing system. The WHMCS and WooCommerce modules are built on this same interface.
https://servernet.cloud/en/developers · 2026/08/25
1Getting started
- Your account must be enabled as a domain reseller. Until then the domain endpoints return an authorization error.
- In your client panel, under Security, issue an API token and narrow its scope to the minimum your integration needs.
- Add the egress IP of your own server to that token allowlist. A token without an IP restriction is usable from anywhere.
- Fund the account. Registrations and renewals settle against your credit at call time; there is no post-paid billing.
- Make a test call to the health endpoint and reconcile the returned tier and balance against the panel.
2Authentication and scope
Authentication uses a bearer token in the Authorization header. There is no session, cookie or CSRF layer; every request is independent and stateless. The token must never appear in a URL path, a query parameter or browser-side code, where it would be recorded in server logs, CDN logs and browser history.
curl -H "Authorization: Bearer sn_xxxxxxxx" \
https://servernet.cloud/api/v1/ping
Scopes
read | read the account profile, services, invoices and credit balance |
domains:read | read the domain portfolio, availability and tier pricing |
domains:write | register and renew domains — settles against account credit |
domains:manage | modify nameservers and the auto-renew flag on existing domains |
tunnel:read | read the WireGuard-over-TCP accounts of your exit server |
tunnel:write | create and remove tunnel accounts — the private key is returned once and never stored |
Write scope implies read scope; the converse never holds. We recommend issuing a separate token per environment — one read-only for reporting, one write and IP-bound for the billing host. A leaked read-only token cannot incur cost.
3Response contract
Every response is a JSON envelope with a fixed shape. Client branching must key on the machine-readable error identifier, never on the message text. Message text is localizable and editable and is not part of the stable contract; identifiers are part of the v1 contract and will not be removed or redefined before a new major version.
{"ok": true, "data": { ... }}
{"ok": false, "error": "insufficient_credit", "message": "..."}
4Endpoints
/api/v1/ping
connection test, tier and credit
read
/api/v1/tlds
per-TLD prices (register/renew/transfer)
domains:read
/api/v1/domains/check
availability and price
domains:read
/api/v1/domains
your domains
domains:read
/api/v1/domains/{domain}
details, expiry, status
domains:read
/api/v1/domains
register — deducted from credit
domains:write
/api/v1/domains/{domain}/renew
renew — deducted from credit
domains:write
/api/v1/domains/{domain}/nameservers
set nameservers
domains:manage
/api/v1/domains/{domain}/lock
turn transfer lock on
domains:manage
/api/v1/domains/{domain}/auto-renew
auto-renew flag
domains:manage
Availability and pricing
POST https://servernet.cloud/api/v1/domains/check
{"domain": "example.com", "tlds": ["com", "net"]}
{"ok": true, "data": [{
"domain": "example.com", "state": "free", "available": true,
"currency": "IRT",
"price": {"register": 1150000, "renew": 1250000, "retail": 1320000},
"discount_pct": 12.88, "price_floored": false
}]}
Client logic must branch on state, not on the available boolean. The value unchecked means the lookup did not resolve and must be treated as a transient condition eligible for retry, not as taken. Collapsing these six states into one boolean once told users in this very system that their preferred name was gone while it was in fact available.
free registrable
premium registrable, at registry premium pricing
taken already registered
unchecked lookup did not resolve — retryable
unsupported the extension is not in our sales catalogue
no_price registrable, but no reliable price is available
A true price_floored flag means your tier discount was not fully applied on that extension because the resulting price reached our margin floor. The condition is never hidden; it is described in full in the pricing section.
Registration
POST https://servernet.cloud/api/v1/domains
Idempotency-Key: your-order-12345
{"domain": "example.com", "years": 1,
"nameservers": ["ns1.you.com", "ns2.you.com"]}
{"ok": true, "data": {
"domain": "example.com", "status": "pending",
"order_state": "registered", "registrant": "reseller",
"charged": 1265000, "currency": "IRT"
}}
order_state | ||
|---|---|---|
registered | terminal — the domain is registered at the registry | no further action |
pending | non-terminal — the order is accepted and queued | poll the domain endpoint with increasing backoff; do not re-order |
manual | non-terminal — held for human review on our side | the amount is held and the outcome will be reported |
failed | terminal — registration did not complete | the amount is returned to your credit in full |
5Idempotency and retries
The server accepts requests without an Idempotency-Key header, but in that case **no duplicate protection applies whatsoever**; sending one on every billable operation is therefore a correctness requirement of any integration. The key is at most 80 characters and is claimed on a unique database index before any work is performed, so two concurrent requests carrying the same key can never produce two financial transactions. A replayed response is identical in substance to the original and carries a replayed flag. If the first attempt ends in an error the key is released — idempotency means one operation is not performed twice, not that one error is repeated forever.
If the key is derived from the domain name alone, the next period renewal of the same domain produces an identical key. The server treats it as a repeat and replays the previous response: your system records success, the customer is charged, and no renewal takes place at the registry. This failure emits no signal until the expiry date. The key must incorporate at least the domain name, the current expiry date and the term in years.
sha256("renew|example.com|2027-01-01|1")
Retry with exponential backoff and jitter, reusing the same key; minting a fresh key on retry defeats the entire guarantee. Our official modules construct and persist this key for you.
6Error identifiers
missing_token | no Authorization header was sent |
invalid_token | the token was not recognised |
token_expired | the token has expired — issue a new one |
token_revoked | the token has been revoked |
ip_not_allowed | the source IP is not on this token allowlist |
insufficient_scope | the token lacks the required scope |
panel_only | this operation is not exposed over the API |
insufficient_credit | insufficient credit — required and available amounts are in data |
daily_cap_reached | the account daily spend cap is exhausted |
already_registered | the domain is present in our active portfolio |
renewal_in_progress | a renewal for this domain is already running |
request_in_progress | a request with this idempotency key is still processing |
tld_blocked | registration in this extension is temporarily suspended; nothing was charged |
tld_not_sold | the extension is not in the sales catalogue |
registrant_incomplete | the registrant details on your account are incomplete |
no_price | no reliable price is available |
lookup_failed | the registrar lookup did not resolve — retryable |
registrar_rejected | the registrar refused the requested change |
validation_failed | the request payload is invalid — per-field detail is in data |
bad_idempotency_key | the idempotency key exceeds 80 characters |
conflict | this idempotency key was already consumed by a different request |
invalid_domain | the domain name is not syntactically valid |
not_found | the domain is not in your account |
not_registered | the domain is not yet registered at the registrar — management operations are unavailable |
not_active | only an active domain can be renewed |
already_yours | the domain is already in your own account |
account_inactive | the reseller account is unavailable |
order_failed | the order did not complete — nothing was charged |
7Pricing and tiers
The price returned by the API is your buying price: retail less the tier discount. The tier is derived from two metrics simultaneously — your trailing twelve month purchase volume and your active portfolio size — and is reviewed daily. A quoted price is not an execution guarantee; settlement is authoritative against the fresh quote taken at order time.
- Upgrades are immediate and apply the moment purchase volume crosses a threshold.
- Downgrades are gradual: a drop in volume first enters a grace period, then moves at most one step. The asymmetry is deliberate.
- Your current tier, the distance to the next threshold and the last review date are available in the reseller panel and in the health endpoint response.
The margin floor
Our margin varies per extension and follows the cost structure of the registry concerned. On thin-margin extensions — in practice also the highest demand ones — your tier discount applies only down to a floor that remains above our landed cost. Wherever that floor engages, the API sets price_floored true and returns the effective applied percentage separately.
Disclosing this constraint is a deliberate choice. An undeclared floor produces a discrepancy the client cannot reconcile, discoverable only by manually auditing invoices — precisely the work a loyalty programme is meant to eliminate.
8Quotas and limits
| read requests | 120 / 1 minute |
| availability and price lookups | 60 / 1 minute |
| write operations | 20 / 1 minute |
| maximum term per order (years) | 10 |
| maximum concurrent active tokens | 20 |
Exceeding a rate limit returns 429 and must be handled with backoff, not with additional concurrency. Each account additionally carries a daily spend cap, adjustable downward in the reseller panel. This cap is a blast-radius control: in a token compromise scenario it bounds the maximum loss achievable before anyone notices. We recommend setting it to a small multiple of your genuine daily turnover, not higher.
9Deliberately out of scope
The following are not unimplemented; they are deliberately excluded from the token surface. The criterion is explicit: any operation that, with a compromised token, transfers control of an end customer domain is performed only under human authentication in the panel. The transfer lock can be enabled over the API but never disabled — an operation that adds protection is safe, one that removes it is not.
auth_code | the transfer authorization (EPP) code is the bearer credential for domain ownership; returned over an API it persists in your application logs, CDN logs and error trackers. |
transfer_unlock | disabling the transfer lock is the necessary first step of moving a domain to another registrar. |
registrant_change | changing the registrant is a transfer of legal ownership, not an update of contact details. |
dns | DNS record management is outside the scope of this interface; delegate to your own nameservers or DNS provider. |
10Official modules
Two reference implementations are maintained against this interface, both downloadable from the reseller panel. If your stack is one of these two, prefer the module over a hand-written integration: the financial safeguards are already implemented in it.
modules/registrars/servernet/
A standard registrar module. It installs into the WHMCS registrars directory and requires only the token. It covers lookup, registration, renewal, nameserver management, transfer lock, status synchronisation and price-table import, and constructs the idempotency key itself, expiry date included.
wp-content/plugins/servernet-domains/
A WordPress plugin with optional WooCommerce integration. A shortcode renders the search interface; with WooCommerce active the domain enters the cart, the customer pays through your own gateway, and registration executes automatically once payment is confirmed. The plugin also loads cleanly on installations without WooCommerce.
- A price is never accepted from the browser. At add-to-cart time the price is re-quoted from this interface and substituted. Without this safeguard a hand-crafted request can place an expensive domain into the cart at an arbitrary amount; settlement still draws the true cost from your credit and the difference is your loss.
- Before registration executes, the buying price is compared against the amount quoted at order time. If the increase exceeds a configured tolerance, automatic registration is skipped and the order is held for a human decision. Days can elapse between add-to-cart and payment, and exchange rates move in that window.
- The idempotency key is derived from the order line identifier, not the order identifier. One order may contain several domains, and WooCommerce marks a single order paid through several independent paths: the gateway webhook, the customer return, and a manual status change by an administrator.
11Roadmap
The following are planned but not yet published in the v1 interface. Until release, calling the corresponding endpoint returns 404. Adding an endpoint is a backward-compatible change and does not increment the interface version; your client must not error on encountering an unknown field in a response.
transfer | inbound domain transfer — submitting the authorization code, tracking request state and settling the added year. In development. Note that the TLD listing endpoint already returns a transfer price so you can build a complete price table; the transfer operation itself is not yet callable. |
ir | supply of Iranian national extensions through a direct connection rather than an international registrar. |
webhook | event delivery to an endpoint of yours, removing the need to poll for state changes on non-terminal orders. |
contact | a per-end-customer registrant data path, with the accompanying consent and erasure mechanics. |