# 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: ` 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=` 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 | 1. `GET /hosting/plans` (no auth) to see plans and prices. 2. `POST /hosting/orders {"plan":"lite"}` -> order `pending`. Billing is currently off, so staff approve orders; approval creates the account (`account.create` job). Watch `GET /hosting/orders` or the `order.approved` webhook. Max 3 pending orders per customer (409 `too_many_pending`, `error.details.pending_orders` lists them); cancel with `POST /hosting/orders/{id}/cancel`. 3. `GET /hosting/accounts` -> your account id (e.g. 12) once `status` is `active`. 4. `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 lists `dns_records` to create at your DNS provider; then `POST /hosting/accounts/12/sites/example.com/ssl`. 5. `POST /hosting/accounts/12/sites/example.com/wordpress {"admin_email":"you@example.com"}` -> job; result has `admin_url`, `admin_user`, `admin_password` (shown once). Other apps: `GET /hosting/apps`, then `POST /hosting/accounts/12/sites/{domain}/apps {"app":"Joomla","options":{...}}`. 6. Mail: `POST /hosting/accounts/12/mail/domains {"domain":"example.com"}` (MX, SPF, DKIM, DMARC set automatically for MOKLAV DNS zones; else see `GET .../mail/domains/{domain}/dns`), then `POST .../mail/domains/example.com/mailboxes {"local_part":"info"}` -> generated password + IMAP/SMTP/webmail settings. 7. Databases `POST .../databases {"name":"shop"}`; FTP users `POST .../ftp`; cron `POST .../cron`; SSH/SFTP keys `POST .../ssh-keys`; PHP `PUT .../sites/{domain}/php {"php":"8.2"}`. 8. Backups: `GET .../backups`, `POST .../backups` (now), `POST .../backups/restore {"backup":""}`. 9. Usage: `GET /hosting/accounts/12` (live usage vs limits) and `GET .../stats` (monthly history). 10. Delete: destructive calls require confirmation: `DELETE .../sites/{domain}?confirm={domain}`, `DELETE /hosting/accounts/12 {"confirm":"mk000012"}`. ## 9a. VPS workflow (manual review) KVM VPS in Japan. Monthly flat, tax included, no contract, no setup fee. Stock is limited to real free capacity (20% headroom) and is shared across plans; `stock` in the response is how many more of that plan can be ordered now. | id | name | vCPU | RAM | SSD | JPY/month | |---|---|---|---|---|---| | ashigaru | Ashigaru 足軽 | 1 | 1 GB | 15 GB | 490 | | hatamoto | Hatamoto 旗本 | 2 | 2 GB | 30 GB | 880 | | daimyo | Daimyo 大名 | 3 | 4 GB | 50 GB | 1650 | | tenka | Tenka 天下 | 4 | 8 GB | 80 GB | 3080 | 1. `GET /vps/plans` (no auth): plans, prices, `stock`, `available`, allowed `oses`. 2. `POST /vps/orders {"plan":"ashigaru","hostname":"web1","os":"ubuntu-24.04"}` (scope `hosting:write`) -> 201, order `pending`. Capacity is reserved while pending. Nothing is provisioned automatically and nothing is charged: staff review the order and contact you. 409 `out_of_stock` when the plan is sold out; 409 `too_many_pending` after 3 pending orders. 3. `GET /vps/orders`, `GET /vps/orders/{id}` (scope `hosting:read`); `POST /vps/orders/{id}/cancel` frees the capacity. Status values: pending, approved, provisioned, rejected, cancelled. ## 9b. Enterprise enquiries (ILL, BGP transit, IPv6 /48, colocation) `POST /enquiries {"service":"bgp_transit","name":"Taro","email":"noc@example.jp","company":"Example KK","message":"..."}` (no auth; 5/hour per IP). `service` is one of ill, bgp_transit, ipv6_48, colocation, other. These are quoted individually; the API publishes no prices or capacity for them. Web form: https://dns.moklav.com/enterprise ## 9c. Company verification Who operates this service, with public sources you can check (no auth): `GET /company` (JSON), web page https://moklav.co.jp/about (also https://dns.moklav.com/about), https://dns.moklav.com/.well-known/security.txt. - Operator: MOKLAV GLOBAL合同会社 (MOKLAV GLOBAL GK), Japan corporate number 8070003006022, head office Ota, Gunma. Verify: https://www.houjin-bangou.nta.go.jp/henkorireki-johoto.html?selHouzinNo=8070003006022 - Network: AS154448 (registered via APNIC/IRINN to MOKLAV GLOBAL (OPC) PRIVATE LIMITED, an Indian company in the same group). Prefixes 144.79.160.0/23 and 2402:4160::/32 (RPKI valid). Upstream AS174 Cogent. https://bgp.he.net/AS154448 https://www.peeringdb.com/net/41436 - Contacts: abuse@moklav.com (abuse, as in APNIC), noc@moklav.com (network, as in PeeringDB), security@moklav.com, general rana@moklav.co.jp. - Company website: https://moklav.co.jp (same public pages; account and API stay on https://dns.moklav.com). ## 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 /me` first 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 `confirm` value; ask your human before deleting.