---
title: "Hosting (DirectAdmin) — API v1 and MCP | Nodea Documentation"
description: "Full reference for the DirectAdmin Hosting module: REST API v1 endpoints (accounts, mailboxes, backups + restore, SSL, termination), account:* scopes, MCP tools and curl / JSON-RPC examples."
url: "https://nodea.io/en/api-docs/hosting"
lang: "en"
updated: "2026-07-16"
---

# Hosting (DirectAdmin) — API v1 and MCP | Nodea Documentation

> Hosting (DirectAdmin) module reference — API v1 endpoints (accounts, mailbox CRUD, backups + restore, issue SSL, terminate account), account:read/write/delete scopes, MCP tools and examples. Mailbox passwords are write-only, DA credentials never returned.

The **Hosting** module covers the customer account's DirectAdmin hosting accounts: listing and account detail, mailbox management, backups with restore, Let's Encrypt certificate issuance and account termination. 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 matching the operation tier:

- account:read — listing and detail (account, mailboxes, backups).
- account:write — mailbox create/update/delete, backup create, SSL issuance.
- account:delete — backup restore (overwrites state) and account termination.

The whole module is additionally gated by the Pennant flag features:hosting-suite — disabling it hides the module from 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 hosting-suite flag.

## Customer account, ownership and visibility

Hosting accounts belong to a **customer account** (see [Customer accounts](https://nodea.io/en/api-docs/accounts)). A request acts in the current account (token pinning → X-Account-Id header → default account), and next to the token scope an account permission is required:

EndpointAccount permissionGET /hosting, GET /hosting/{account}, GET …/emails, GET …/backupsproducts Mailboxes (POST/PATCH/DELETE …/emails), POST …/backups, POST …/backups/{backup}/restore, POST …/sslproducts + manageproducts DELETE /hosting/{account} (termination)the customer account **owner** only (account\_owner\_required)

The list returns only the **current account's** hosting accounts, and HostingAccountResource carries an account\_id field. Mailboxes and backups are checked twice: the hosting account must belong to the current customer account, and the mailbox/backup must belong to that hosting account. Any mismatch (including a resource of another account of yours) yields 404 (not 403), so the API never confirms a foreign resource exists. In MCP this maps to a single "not found or not accessible" error.

**Check order on termination** (DELETE /hosting/{account}): 404 (hosting account outside the current account) → 423 managed\_by\_whmcs (account managed by the WHMCS billing panel — always, regardless of the global lock) → 423 termination\_locked (global lock — the owner too) → 403 account\_owner\_required (a member, even with manageproducts) → 422 already\_terminated.

**Mailbox domain binding.** Mail operations run against DirectAdmin with ADMIN credentials, so the domain field on mailbox creation is restricted to the domains tied to this hosting account: the hosting account's main domain, **the active domains of the same customer account**, and the domains of mailboxes already synced from DA onto this hosting account. The comparison is exact (ignoring case and a trailing dot) — a sub-domain has to be on the list in its own right. A domain outside that set is rejected (422 on the API, a tool error on MCP) — without it a customer could open a mailbox on a co-tenant's domain hosted on the same server.

## Sensitive-data safety

**Mailbox passwords are write-only: accepted in the create/update payload, forwarded straight to the DirectAdmin API, never stored locally and never returned in any response.** The DA panel password (encrypted\_password), SSO login keys, and the DA server's hostname/IP/port and API credentials are deliberately never serialized — the account exposes only its location. Internal backup columns (local\_path, restore\_log) are likewise not exposed.

## Side effects — asynchronous and account state

DirectAdmin-side effects (backup create/restore, SSL issuance, termination) run out-of-band through queued jobs, like web — those actions return 202 Accepted. A backup restore additionally needs the account owner's e-mail confirmation (see below). Mailbox create/update/delete calls the DA API synchronously, and the local mirror is refreshed by SyncEmailAccountsJob. Every DA-backend operation requires an **active** account: a pending account is still provisioning and a terminated one accepts no further actions — both return 409 Conflict.

## Backup restore — e-mail confirmation

A restore overwrites the account's files, mail and databases, so neither API v1 nor MCP **restores at once**. The request only asks for a restore: Nodea e-mails the **customer account owner** a signed, single-use link valid for 30 minutes (the same flow as in the panel). The restore is queued only when the owner opens the link while signed in to Nodea. A token alone — even with the account:delete scope — is not enough.

- Response: 202 Accepted with data.status = "pending\_confirmation", data.backup\_id and data.confirmation\_ttl\_minutes (30).
- A new request for the same account invalidates earlier unused links — only the newest one works.
- A link opened by someone other than the owner, after expiry or a second time restores nothing.
- Track the progress after confirmation in the backup list (GET …/backups, the status field).

{ "message": "Restore requested. The account owner must confirm it with the link sent by e-mail.", "data": { "status": "pending\_confirmation", "backup\_id": 128, "confirmation\_ttl\_minutes": 30 } }

## API v1 — endpoints

MethodPathScopeDescriptionGET/api/v1/hostingaccount:readThe current account's hosting accounts (id DESC), paginated. GET/api/v1/hosting/{account}account:readAccount detail: disk/bandwidth usage, SSL, location, plan, timestamps. GET/api/v1/hosting/{account}/emailsaccount:readAccount mailboxes (by address), paginated — per\_page 1–100 (default 20), page. POST/api/v1/hosting/{account}/emailsaccount:writeCreate a mailbox (write-only password). Returns 202. PATCH/api/v1/hosting/{account}/emails/{email}account:writeChange password (write-only) and/or mailbox quota. Not for the system mailbox \<login\>@\<main domain\> — 409 system\_mailbox\_locked. DELETE/api/v1/hosting/{account}/emails/{email}account:writeDelete a mailbox (DA + local, irreversible). Requires confirm: true in the body (otherwise 422). Not for the system mailbox — 409 system\_mailbox\_locked. Returns 204. GET/api/v1/hosting/{account}/backupsaccount:readAccount backups (id DESC), paginated. POST/api/v1/hosting/{account}/backupsaccount:writeQueue a new backup (name backup-Y-m-d-H-i). Returns 202. POST/api/v1/hosting/{account}/backups/{backup}/restoreaccount:deleteRequest a backup restore — it will overwrite account state (irreversible). Does not restore at once: returns 202 with status pending\_confirmation; the restore starts when the account owner clicks the e-mailed link. POST/api/v1/hosting/{account}/sslaccount:writeQueue a Let's Encrypt certificate. Requires an active account. Returns 202. DELETE/api/v1/hosting/{account}account:deleteTerminate the account (destructive, async) — customer account owner only. Returns 202. Error order: 404 → 423 (managed\_by\_whmcs) → 423 (termination\_locked) → 403 (account\_owner\_required) → 422 (already\_terminated).

**Web-only (out of API and MCP):** DirectAdmin control-panel SSO login (login-key mint) and the account password reset — interactive browser hand-offs, not headless operations.

### Mailboxes — body parameters

- email\_local (string, required on create, 1–64 chars, charset \[a-zA-Z0-9.\_-\]) — local part of the address.
- domain (string, required on create, 4–253, FQDN-shaped) — **must belong to the account**: the hosting account's main domain, an active domain of the same customer account, or the domain of an existing mailbox on that hosting account.
- password (string, 10–128 chars; required on create, optional on update) — **write-only**.
- quota\_mb (int 50–10000; required on create, optional on update).

**Deleting a mailbox** (DELETE …/emails/{email}): body {"confirm": true} (or ?confirm=1). Check order: 404 (mailbox or hosting account outside the current account) → 422 (no confirmation) → 409 (account not active) → 409 system\_mailbox\_locked.

### Hosting account — fields and limits

Account (HostingAccountResource; the same in MCP hosting-list / hosting-get): id, account\_id, domain, username, status, package, disk\_used\_mb, disk\_quota\_mb, bandwidth\_used\_mb, bandwidth\_quota\_mb, inode\_quota, disk\_usage\_percent, bandwidth\_usage\_percent, inodes\_used, limits, limits\_synced\_at, ssl\_enabled, ssl\_expires\_at, lifecycle timestamps, location, plan, next\_due\_at. It never carries the panel password or DA server details.

Limits are synced from DirectAdmin (together with usage, every few hours). disk\_quota\_mb, bandwidth\_quota\_mb and inode\_quota are a number or null — **null does not mean zero**: it is either "no limit" or "no data". The limits object decides, with keys disk\_mb, bandwidth\_mb, inodes, email\_accounts, databases, domains, subdomains, ftp\_accounts; each value is a number, the string "unlimited" (no limit) or null (limit unknown). limits\_synced\_at = null means limits have not been read yet. \*\_usage\_percent is 0 when the limit is unknown or unlimited.

**Contract change (2026-09):** disk\_quota\_mb and bandwidth\_quota\_mb used to be always a number (a missing limit was serialised as 0); they are now nullable. New fields: inode\_quota, limits, limits\_synced\_at.

{ "disk\_used\_mb": 2760, "disk\_quota\_mb": 10240, "bandwidth\_used\_mb": 5120, "bandwidth\_quota\_mb": null, "inode\_quota": null, "limits": { "disk\_mb": 10240, "bandwidth\_mb": "unlimited", "inodes": null, "email\_accounts": 10, "databases": 5, "domains": 1, "subdomains": "unlimited", "ftp\_accounts": 5 }, "limits\_synced\_at": "2026-09-26T08:00:00+00:00" }

Mailbox (HostingEmailResource): id, email, domain, quota\_mb, used\_mb, is\_forwarder, forward\_to, is\_active, last\_synced\_at — never carries a password field. Backup (HostingBackupResource): id, backup\_name, size\_mb, status, is\_remote, created\_at, expires\_at, restore\_url (link to the restore action).

### Error codes

- 401 — missing/invalid token.
- 403 — missing required scope.
- 403 — account-context error: account\_permission\_denied (the permission field names the missing flag) — products for reads, manageproducts for actions, no\_account\_context, account\_forbidden / account\_mismatch (the X-Account-Id header or token pinning) — see [Customer accounts](https://nodea.io/en/api-docs/accounts).
- 403 — account\_owner\_required: only the customer account owner can request termination.
- 404 — unknown hosting account, mailbox or backup, or a resource outside the current account.
- 409 — account not active (pending/terminated) for a DA-backend action.
- 409 — system\_mailbox\_locked: the system mailbox \<login\>@\<main domain\> (its password is the DA account password) cannot be changed (PATCH) or deleted (DELETE).
- 422 — validation (including a mailbox domain outside the account, a mailbox delete without confirm: true and per\_page above 100) or terminating an already-terminated account (code already\_terminated).
- 423 — terminating an account managed by the WHMCS billing panel is always refused, even with the global lock off (code managed\_by\_whmcs); the account is left untouched — the service is closed in the billing panel.
- 423 — termination is currently locked globally in Nodea (code termination\_locked); the account is left untouched. The lock is checked before account permissions. The service lifecycle is managed by the billing system — contact support.

## Examples — API

### List hosting accounts

curl -s "https://app.nodea.io/api/v1/hosting?per\_page=20" \\ -H "Authorization: Bearer $NODEA\_TOKEN" \\ -H "Accept: application/json"

### Create a mailbox (write-only password)

curl -s -X POST https://app.nodea.io/api/v1/hosting/{account}/emails \\ -H "Authorization: Bearer $NODEA\_TOKEN" \\ -H "Content-Type: application/json" \\ -d '{ "email\_local": "office", "domain": "my-domain.com", "password": "S3cretPass!23", "quota\_mb": 2000 }'

### Queue a backup

curl -s -X POST https://app.nodea.io/api/v1/hosting/{account}/backups \\ -H "Authorization: Bearer $NODEA\_TOKEN" \\ -H "Accept: application/json"

### Request a backup restore (irreversible, confirmed by e-mail)

curl -s -X POST https://app.nodea.io/api/v1/hosting/{account}/backups/{backup}/restore \\ -H "Authorization: Bearer $NODEA\_TOKEN" \\ -H "Accept: application/json"

## MCP — tools

The MCP tools share the authentication and scope model with API v1 (Bearer + scope in ensureScope) and the hosting-suite flag (a tool on an inactive flag disappears from tools/list). Ids are integers. A missing and a foreign id return the same error. DirectAdmin-touching actions are asynchronous (returning a queued acknowledgement). Destructive tools — hosting-backup-restore and hosting-terminate — are marked destructive and require an explicit confirm: true; hosting-email-delete requires confirm: true as well. hosting-backup-restore does not restore at once: it returns status: "pending\_confirmation" (queued: false) and the restore starts after the account owner confirms it with the e-mailed link (as in API v1). The system mailbox \<login\>@\<main domain\> (its password is the DA account password) cannot be changed or deleted — an error with the suffix (code: system\_mailbox\_locked). Mailbox passwords remain write-only. The mailbox validation rules (local-part charset, 10–128-char password, 50–10000 MB quota, domain bound to the account) are **shared with API v1** — MCP is not the softer way into the same operations.

NameScopeModeDescriptionhosting-listaccount:readread-onlyDirectAdmin accounts (id DESC); page, per\_page (max 50). hosting-getaccount:readread-onlyAccount detail by id. The DA panel password is never returned. hosting-emailsaccount:readread-onlyAccount mailboxes (by address); account\_id, page, per\_page (max 50). hosting-email-createaccount:writewriteCreate a mailbox; account\_id, email\_local, domain (must belong to the account — the hosting account's main domain, an active domain of the same customer account or the domain of an already synced mailbox), password (write-only, 10–128 chars), quota\_mb (50–10000). Account must be active. hosting-email-updateaccount:writewriteChange password (write-only, 10–128 chars) and/or quota (50–10000); account\_id, email\_id, password?, quota\_mb?. Not for the system mailbox (system\_mailbox\_locked). hosting-email-deleteaccount:writewrite (confirm)Delete a mailbox (DA + local, irreversible). Requires confirm: true; account\_id, email\_id. Not for the system mailbox (system\_mailbox\_locked). Account must be active. hosting-backupsaccount:readread-onlyAccount backups (id DESC); account\_id, page, per\_page (max 50). hosting-backup-createaccount:writewriteQueue a backup; account\_id. Async — poll hosting-backups. Account must be active. hosting-backup-restoreaccount:deletedestructive (confirm)Request a backup restore — it will overwrite account data. Requires confirm: true; account\_id, backup\_id. Returns {queued: false, status: "pending\_confirmation", backup\_id, confirmation\_ttl\_minutes}; the restore starts when the account owner clicks the e-mailed link (30 min, single use, a new request invalidates the previous one). hosting-issue-sslaccount:writewriteQueue a Let's Encrypt certificate; account\_id. Async — poll hosting-get. Account must be active. hosting-terminateaccount:deletedestructive (confirm)Terminate the account — irreversible. Requires confirm: true; account\_id. Async. Customer account owner only. Error order: not found → managed\_by\_whmcs (account managed in WHMCS — always, nothing is queued) → termination\_locked (global lock, nothing is queued) → account\_owner\_required → already\_terminated.

**Customer accounts in MCP.** The tools act in the current account of the MCP connection (see [Customer accounts](https://nodea.io/en/api-docs/accounts)). hosting-list, hosting-get, hosting-emails and hosting-backups require the products permission; hosting-email-create/-update/-delete, hosting-backup-create/-restore and hosting-issue-ssl require products + manageproducts; hosting-terminate requires the owner role. Denials end with the suffix (code: account\_permission\_denied, permission: \<flag\>), (code: account\_owner\_required), (code: no\_account\_context), (code: managed\_by\_whmcs), (code: termination\_locked) or (code: already\_terminated).

### Examples — MCP (JSON-RPC)

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "hosting-email-create", "arguments": { "account\_id": 7, "email\_local": "office", "domain": "my-domain.com", "password": "S3cretPass!23", "quota\_mb": 2000 } } } { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "hosting-backup-restore", "arguments": { "account\_id": 7, "backup\_id": 128, "confirm": true } } }

Response (structuredContent) — the restore waits for the account owner's e-mail confirmation:

{ "queued": false, "status": "pending\_confirmation", "backup\_id": 128, "confirmation\_ttl\_minutes": 30 }

## Related

- [Domains — API v1 and MCP | Nodea Documentation](https://nodea.io/en/api-docs/domains)
- [Migrator — API v1 and MCP | Nodea Documentation](https://nodea.io/en/api-docs/migrations)
- [Customer accounts — API v1 and MCP | Nodea Documentation](https://nodea.io/en/api-docs/accounts)

---

Languages: [pl](https://nodea.pl/pl/dokumentacja-api/hosting.md)  
Site index: [llms.txt](https://nodea.io/en/llms.txt)
