MOKLAV API guide (DNS + Hosting)
Base URL: https://panel.moklav.co.jp/api/v1. Japanese customer panel (main Japan address): https://panel.moklav.co.jp (same app; https://panel.moklav.co.jp keeps working) OpenAPI 3 spec: https://panel.moklav.co.jp/api/v1/openapi.json (interactive docs: https://panel.moklav.co.jp/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://panel.moklav.co.jp/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"}`.
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 |
GET /vps/plans(no auth): plans, prices,stock,available, allowedoses.POST /vps/orders {"plan":"ashigaru","hostname":"web1","os":"ubuntu-24.04"}(scopehosting:write) -> 201, orderpending. Capacity is reserved while pending. Nothing is provisioned automatically and nothing is charged: staff review the order and contact you. 409out_of_stockwhen the plan is sold out; 409too_many_pendingafter 3 pending orders.GET /vps/orders,GET /vps/orders/{id}(scopehosting:read);POST /vps/orders/{id}/cancelfrees 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://panel.moklav.co.jp/enterprise
9c. Company verification
Who operates this service, with public sources you can check (no auth): GET /company (JSON), web page https://panel.moklav.co.jp/about (also https://moklav.co.jp/about and https://panel.moklav.co.jp/about), https://panel.moklav.co.jp/.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://panel.moklav.co.jp).
9d. Orders, invoices (請求書) and payment
Every POST /hosting/orders and POST /vps/orders returns the order plus invoice (unpaid, bank transfer, amounts tax included, 10% consumption tax shown).
GET /invoices,GET /invoices/{id}(scopehosting:read). Invoice numbers look likeMK-202610-000001; use it as the transfer reference.
Printable invoice (HTML, save as PDF from the browser): https://panel.moklav.co.jp/invoices/{id} (logged in; also on panel.moklav.co.jp). links.url_ja has the absolute URL.
bank_transfer.details_available=falsemeans bank details are not published yet: "振込先は別途ご案内します".- Staff:
POST /admin/invoices/{id}/mark-paid-> invoicepaid; a hosting order is then approved and provisioned (account.createjob);
a VPS order becomes approved and is built by staff (no automatic VM creation). POST /admin/invoices/{id}/void, GET|PUT /admin/billing/config (bank branch/type/number/holder, optional registration number T + 13 digits).
- Card payment (Stripe, when enabled; check
GET /billing/methods; VPS only — hosting plans are enquiry-only for now,POST /enquiries {"service":"hosting"}):POST /invoices/{id}/checkout->{"url"}(Stripe Checkout,
monthly subscription in JPY, tax included). On success the invoice becomes paid and the order is approved (hosting provisions automatically, VPS is built by staff). Renewals are charged by Stripe each month. POST /billing/portal -> {"url"} to change the card or cancel; cancellation ends at the end of the paid period and the hosting account is then suspended (not deleted). GET /billing/subscriptions lists card subscriptions. Bank transfer stays available.
- Online payment:
POST /payments/webhook/{stripe|razorpay|payjp}verifies the provider signature; it returns 503payments_disabled
until online payment launches. Paid events carry metadata.invoice_number (Razorpay: notes.invoice_number).
- Hosts: https://panel.moklav.co.jp is the main Japan customer address; https://panel.moklav.co.jp is the same panel (Japanese default on .co.jp). Stripe return URLs and links for Japanese-language customers point to panel.moklav.co.jp. Log in separately on each host;
passkeys work only on the host where they were registered. The API base for both is https://panel.moklav.co.jp/api/v1.
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://panel.moklav.co.jp/api/v1/hosting/plans
curl -s -X POST https://panel.moklav.co.jp/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://panel.moklav.co.jp/api/v1/jobs/job_xxx -H "Authorization: Bearer $K"
Python:
import time, uuid, httpx
API = "https://panel.moklav.co.jp/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.