Domains — API v1 and MCP | Nodea Documentation
The Domains module covers the user's registered domains: filtered listing, detail with WHOIS contacts, and maintenance operations — changing nameservers, toggling WHOIS privacy, and requesting the transfer authorization (EPP) code. This document describes the REST API v1 layer (/api/v1) and the module's MCP tools.
Authentication and scopes
API v1 authenticates with a Passport personal access token via the Authorization: Bearer <token> header. This module's routes are guarded by the account scope:
account:read— listing and detail (index, show).account:write— editing nameservers and non-destructive actions (WHOIS privacy, EPP request).
The whole module is additionally gated by the Pennant flag features:hosting-suite — disabling it hides the module from both the web UI and the API. Rate limit: throttle:api-v1 = 240/min per token user. In MCP, each tool additionally requires the transport scope mcp:use and the same hosting-suite flag.
Ownership and visibility
Domains have no policy class and are never shared — every row access is strictly owner-scoped by user_id. A foreign or unknown id consistently yields 404 (not 403), so the API never confirms that a foreign domain exists. In MCP this maps to a single "not found or not accessible" error, preventing cross-tenant id enumeration.
Sensitive-data safety
The EPP / auth-info code is server-side write-only and is never returned by the API or MCP. It lives in an encrypted, hidden column and is populated asynchronously by TransferDomainOutJob; it can only be read from the web portal. Likewise, the WHOIS contact PESEL is encrypted and hidden — contacts return only name, company, address and the NIP/REGON identifiers, never PESEL. Internal registrar data (registrar_data) also stays hidden.
Side effects — asynchronous
Every registrar-touching operation (nameserver change, WHOIS-privacy toggle, EPP request) runs out-of-band through a queued job, exactly like the web flow: the endpoint validates input, persists local state and dispatches, and the registrar HTTP round-trip happens in the worker. That is why these actions return 202 Accepted ("job queued") rather than an immediate result.
API v1 — endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/domains | account:read | Owner's domains (id DESC), paginated. Filters status, q. |
| GET | /api/v1/domains/{domain} | account:read | Domain detail: nameservers, period, EPP code (null until requested) and the four WHOIS contacts. |
| PATCH | /api/v1/domains/{domain} | account:write | Change auto_renew and/or nameservers. The registrar job fires only when the effective NS set changes. |
| POST | /api/v1/domains/{domain}/whois-privacy | account:write | Enable/disable WHOIS privacy (enabled bool). Requires Active status. 202/422. |
| POST | /api/v1/domains/{domain}/epp | account:write | Request the EPP code for an outbound transfer. Requires transfer eligibility (Active + 60 days). 202/422. |
Web-only (out of API and MCP): domain availability search, ordering/registration, and inbound transfer (transfer-in) — multi-step contact-collection wizards unsuited to a headless call.
GET /domains — query parameters
status(enum, optional) — one of:pending,active,expired,redemption,transferred_out,transferring_in,pending_delete.q(string, optional, max 255) — matchessldortld(LIKE).per_page(int 1–100, default 20),page(int ≥1).
List row (DomainResource): id, full_domain, sld, tld, status, registered_at, expires_at, days_until_expiry, auto_renew, whois_privacy_enabled. Detail (show) adds: period_years, nameservers[], epp_code (null until requested), can_transfer_out and registrant/admin/tech/billing (WHOIS contacts).
PATCH /domains/{id} — body parameters
auto_renew(bool, optional).nameservers(array 0–4, optional) — FQDN-shaped hostnames (regex, max 253 chars). An empty array resets NS to the platform defaults.
Both keys are independent — you can change auto_renew alone or only the nameservers. UpdateDomainNameserversJob is dispatched only when the effective NS set actually changes (the same short-circuit as web).
Error codes
401— missing/invalid token.403— missing required scope.404— unknown or foreign domain.422— validation or domain state: WHOIS privacy outside Active (codedomain_not_active), EPP for a non-transferable domain (codedomain_not_transferable).
Examples — API
List active domains
curl -s "https://app.nodea.io/api/v1/domains?status=active&per_page=20" \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Accept: application/json"
Change nameservers
curl -s -X PATCH https://app.nodea.io/api/v1/domains/{id} \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nameservers": ["ns1.example.com", "ns2.example.com"]
}'
Enable WHOIS privacy
curl -s -X POST https://app.nodea.io/api/v1/domains/{id}/whois-privacy \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'
Request the EPP code (outbound transfer)
curl -s -X POST https://app.nodea.io/api/v1/domains/{id}/epp \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Accept: application/json"
# 202 Accepted — the EPP code is not returned; read it in the web portal.
MCP — tools
The MCP tools share the authentication and scope model with API v1 (Bearer + scope checked in ensureScope) and the hosting-suite flag (a tool on an inactive flag disappears from tools/list). The domain id is an integer. A missing and a foreign id return the same error (no cross-tenant enumeration). Registrar-touching actions are asynchronous — the tool returns a queued acknowledgement, not the final result. The EPP code is never returned.
| Name | Scope | Mode | Description |
|---|---|---|---|
| domains-list | account:read | read-only | Domain list (id DESC). Filter status (buckets: all, active, expiring = Active + expiring within 30 days, expired = expired/redemption, pending = pending/transferring_in), search q, page, per_page (max 50). |
| domain-get | account:read | read-only | Domain detail with WHOIS contacts and the can_transfer_out flag. Param: id. The EPP code is not serialized. |
| domain-set-nameservers | account:write | idempotent | Replace nameservers (nameservers, 1–4 hostnames). Saved immediately, pushed to the registrar async. Returns the updated NS list. |
| domain-whois-privacy | account:write | idempotent | Enable/disable WHOIS privacy (enabled bool). Active domains only. Pushed to the registrar async. |
| domain-request-epp | account:write | idempotent | Request the EPP code for an outbound transfer. Only after the ICANN 60-day lock lifts. The code is never returned — read it in the web portal. |
Examples — MCP (JSON-RPC)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "domain-set-nameservers",
"arguments": { "id": 42, "nameservers": ["ns1.example.com", "ns2.example.com"] }
}
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "domain-request-epp",
"arguments": { "id": 42 }
}
}