MOKLAV API guide (DNS + Hosting)
Base URL: https://dns.moklav.com/api/v1 OpenAPI 3 spec: https://dns.moklav.com/api/v1/openapi.json (interactive docs: https://dns.moklav.com/docs) This guide is written for both humans and AI agents. Everything the web panel can do, the API can do.
1. Authentication
Create a key at https://dns.moklav.com/account/api (shown once). Send it on every request:
Authorization: Bearer mk_xxxxxxxxxxxxxxxxxxxxxxxx
Scopes:
dns: manage the DNS zones selected when the key was created.hosting:read: read hosting accounts, sites, mail, databases, backups, jobs.hosting:write: everything in hosting:read plus create/change/delete.- A "read-only" key never changes anything.
Check your key with GET /me.
2. Response format
Success: hosting endpoints return {"data": ...}. List endpoints return {"data": [...], "pagination": {"limit": 50, "next_cursor": "NTA=", "total": 120}}. (The original DNS endpoints /zones... return plain JSON objects/arrays, unchanged for compatibility.)
Errors always look like this (HTTP status mirrors error.status):
{"error": {"code": "not_found", "message": "Hosting account not found", "status": 404,
"details": null, "request_id": "req_4f..."}, "detail": "Hosting account not found"}
Error codes: bad_request, unauthenticated (401), forbidden, insufficient_scope, domain_not_owned, plan_limit (403), not_found (404), conflict, already_exists, account_not_ready, account_suspended, ssl_not_issued, too_many_pending, request_in_progress, limit_reached (409), validation_error, confirmation_required, idempotency_key_reused (422), rate_limited (429), internal_error (500), upstream_error (502), unavailable, no_capacity (503), upstream_timeout (504). Validation errors list each field in error.details.errors[] as {field, message, type}. Every response carries X-Request-Id; quote it when contacting support. You may send your own X-Request-Id.
3. Rate limits
300 requests per minute per API key. Headers on every authenticated response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (unix time). On 429 wait Retry-After seconds.
4. Idempotency (safe retries)
Send Idempotency-Key: <unique string> on any POST/PUT/PATCH/DELETE. Repeating the same request with the same key within 24 hours returns the original response (header Idempotent-Replayed: true) instead of doing the work twice. Same key with a different body -> 422 idempotency_key_reused; while the first is still running -> 409 request_in_progress. Agents: ALWAYS set an Idempotency-Key on writes, and retry on network errors, 429, 502, 503, 504 with backoff.
5. Pagination
List endpoints accept limit (1-200, default 50) and cursor. Pass pagination.next_cursor as cursor until it is null.
6. Async jobs
Slow operations (create account, add site, SSL, PHP switch, app install, mail domain, backup, restore, plan change, suspend, delete) return 202 Accepted with a job:
{"data": {"id": "job_ab12...", "type": "site.create", "status": "queued", "result": null, "error": null,
"links": {"self": "/api/v1/jobs/job_ab12..."}}}
Poll GET /jobs/{id} every 2-5 s until status is succeeded or failed. Results (e.g. DNS records, WordPress admin password) are in result; failures in error using the same codes as above. GET /jobs?status=running lists jobs.
7. Webhooks
POST /webhooks {"url": "https://example.com/hook", "events": ["job.succeeded","job.failed"]} returns a secret once. Events: job.succeeded, job.failed, order.created, order.approved, order.rejected, account.suspended, account.unsuspended, account.deleted, or *. Each delivery is a POST with JSON {"id","event","created_at","data"} and headers X-Moklav-Event, X-Moklav-Timestamp, X-Moklav-Signature: v1=<hex> where hex = HMAC-SHA256(secret, timestamp + "." + raw_body). Reject if the timestamp is older than 5 minutes. Up to 3 attempts. URL must be https on a public address.
8. Hosting workflow (step by step)
Plans (monthly, tax included, no contract, no setup fee). Plan ids are stable; display names may change:
| id | name | JPY/month | limits |
|---|---|---|---|
| nano | Shinobi 忍 | 110 | 10 GB, 1 website, 10 mailboxes, 1 DB |
| lite | Ronin 浪人 | 220 | 30 GB, 5 websites, unlimited mail, 5 DB, WordPress, daily backup |
| standard | Samurai 侍 | 440 | 100 GB, 20 websites, SSH, 7-day backups |
| pro | Shogun 将軍 | 880 | 200 GB, unlimited websites, SSH, 14-day backups |
GET /hosting/plans(no auth) to see plans and prices.POST /hosting/orders {"plan":"lite"}-> orderpending. Billing is currently off, so staff approve orders; approval creates the account (account.createjob). WatchGET /hosting/ordersor theorder.approvedwebhook. Max 3 pending orders per customer (409too_many_pending,error.details.pending_orderslists them); cancel withPOST /hosting/orders/{id}/cancel.GET /hosting/accounts-> your account id (e.g. 12) oncestatusisactive.POST /hosting/accounts/12/sites {"domain":"example.com","php":"8.3"}-> job. If example.com is a verified zone in your MOKLAV DNS, A/AAAA records for it and www are created automatically and Let's Encrypt SSL is issued. Otherwise the job result listsdns_recordsto create at your DNS provider; thenPOST /hosting/accounts/12/sites/example.com/ssl.POST /hosting/accounts/12/sites/example.com/wordpress {"admin_email":"you@example.com"}-> job; result hasadmin_url,admin_user,admin_password(shown once). Other apps:GET /hosting/apps, thenPOST /hosting/accounts/12/sites/{domain}/apps {"app":"Joomla","options":{...}}.- Mail:
POST /hosting/accounts/12/mail/domains {"domain":"example.com"}(MX, SPF, DKIM, DMARC set automatically for MOKLAV DNS zones; else seeGET .../mail/domains/{domain}/dns), thenPOST .../mail/domains/example.com/mailboxes {"local_part":"info"}-> generated password + IMAP/SMTP/webmail settings. - Databases
POST .../databases {"name":"shop"}; FTP usersPOST .../ftp; cronPOST .../cron; SSH/SFTP keysPOST .../ssh-keys; PHPPUT .../sites/{domain}/php {"php":"8.2"}. - Backups:
GET .../backups,POST .../backups(now),POST .../backups/restore {"backup":"<name>"}. - Usage:
GET /hosting/accounts/12(live usage vs limits) andGET .../stats(monthly history). - Delete: destructive calls require confirmation:
DELETE .../sites/{domain}?confirm={domain},
`DELETE /hosting/accounts/12 {"confirm":"mk000012"}`.
9. DNS workflow
GET /zones, GET /zones/{zone}/records, POST /zones/{zone}/records {"name":"www","type":"A","content":"192.0.2.1","ttl":300}, PUT /zones/{zone}/rrset (replace all values), DELETE /zones/{zone}/records, GET /zones/{zone}/export, GET /zones/{zone}/dnssec, ACME DNS-01 helpers POST /acme/present and /acme/cleanup. Changes are live on ns1/ns2.moklav.com within seconds.
10. Examples
curl:
export K=mk_your_key
curl -s https://dns.moklav.com/api/v1/hosting/plans
curl -s -X POST https://dns.moklav.com/api/v1/hosting/accounts/12/sites \
-H "Authorization: Bearer $K" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" -d '{"domain":"example.com"}'
curl -s https://dns.moklav.com/api/v1/jobs/job_xxx -H "Authorization: Bearer $K"
Python:
import time, uuid, httpx
API = "https://dns.moklav.com/api/v1"
c = httpx.Client(headers={"Authorization": "Bearer mk_your_key"}, timeout=60)
def call(method, path, **kw):
for attempt in range(5):
h = {"Idempotency-Key": kw.pop("idem", None) or str(uuid.uuid4())} if method != "GET" else {}
r = c.request(method, API + path, headers=h, **kw)
if r.status_code in (429, 502, 503, 504):
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt))); continue
if r.status_code >= 400: raise RuntimeError(r.json()["error"])
return r.json()
raise RuntimeError("gave up")
def wait(job):
while job["status"] in ("queued", "running"):
time.sleep(3); job = call("GET", f"/jobs/{job['id']}")["data"]
if job["status"] == "failed": raise RuntimeError(job["error"])
return job["result"]
acct = call("GET", "/hosting/accounts")["data"][0]["id"]
print(wait(call("POST", f"/hosting/accounts/{acct}/sites", json={"domain": "example.com"})["data"]))
11. Rules for AI agents
- Read
GET /mefirst to learn scopes. Never guess ids; list first. - Always send Idempotency-Key on writes; reuse the same key when retrying the same intent.
- For 202 responses, poll the job; do not repeat the original request.
- Secrets (passwords, webhook secrets, WordPress admin password) are returned once; store them securely, never log them.
- Destructive operations need an explicit
confirmvalue; ask your human before deleting.