Moklav GlobalMoklav Global

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:

idnameJPY/monthlimits
nanoShinobi 忍11010 GB, 1 website, 10 mailboxes, 1 DB
liteRonin 浪人22030 GB, 5 websites, unlimited mail, 5 DB, WordPress, daily backup
standardSamurai 侍440100 GB, 20 websites, SSH, 7-day backups
proShogun 将軍880200 GB, unlimited websites, SSH, 14-day backups
  • GET /hosting/plans (no auth) to see plans and prices.
  • 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.
  • GET /hosting/accounts -> your account id (e.g. 12) once status is active.
  • 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.
  • 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":{...}}.
  • 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.
  • 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"}.
  • Backups: GET .../backups, POST .../backups (now), POST .../backups/restore {"backup":"<name>"}.
  • Usage: GET /hosting/accounts/12 (live usage vs limits) and GET .../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.

idnamevCPURAMSSDJPY/month
ashigaruAshigaru 足軽11 GB15 GB490
hatamotoHatamoto 旗本22 GB30 GB880
daimyoDaimyo 大名34 GB50 GB1650
tenkaTenka 天下48 GB80 GB3080
  • GET /vps/plans (no auth): plans, prices, stock, available, allowed oses.
  • 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.
  • 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://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} (scope hosting:read). Invoice numbers look like MK-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=false means bank details are not published yet: "振込先は別途ご案内します".
  • Staff: POST /admin/invoices/{id}/mark-paid -> invoice paid; a hosting order is then approved and provisioned (account.create job);

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 503 payments_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 /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.