{"openapi":"3.1.0","info":{"title":"NSIN API","version":"1.0.0","summary":"Public REST API for NSIN CDN, DNS and edge-security management.","description":"The NSIN API lets you manage everything you can manage from the panel:\ndomains, DNS records, edge rules (cache, WAF, redirects, rate limiting, …),\nTLS certificates, analytics, uptime and domain sharing.\n\n## Authentication\n\nEvery endpoint in this reference is authenticated with an **API key**.\nCreate one in the panel under *Settings → API keys*. Keys are shown once at\ncreation time and are prefixed `nsin_`.\n\nSend the key either way — both are equivalent:\n\n```\nAuthorization: Bearer nsin_xxxxxxxxxxxxxxxxxxxx\n```\n```\nX-Api-Key: nsin_xxxxxxxxxxxxxxxxxxxx\n```\n\n### Read-only keys\n\nA key marked read-only may only issue `GET`, `HEAD` and `OPTIONS` requests.\nAny other method returns `403` with `{\"error\": \"read-only API key\"}`,\nregardless of the endpoint.\n\n### Inactive accounts\n\nIf the account that owns the key is deactivated, every request made with\nthat key is refused with `403` and\n`{\"error\": \"account is inactive\", \"code\": \"account_suspended\"}` — on any\nendpoint, reads included. It is deliberately `403` and not `401`: the\ncredential is still valid, the account behind it is not, so rotating the key\nfixes nothing. Individual operations do not list this code.\n\n### What API keys cannot do\n\nSome parts of the product are deliberately unreachable with a key, so that a\nleaked key can never take over the account or spend money. These return\n`403` for **every** key, including full-access ones:\n\n| Surface | Reason |\n|---|---|\n| `/users/**` | Profile, password, sessions and API-key management. A key cannot mint or revoke keys. |\n| `/auth/**` | Login, registration, OTP. |\n| `/billing/**` | Plan catalogue and billing settings. |\n| `/admin/**` | Administrative surface. |\n| `POST /wallet/topup` | Moves money. |\n| `POST /subscriptions/purchase`, `/switch`, `/auto-renew` | Removed for everyone. Subscriptions are per domain, so these three now answer `410` whatever the credential — a key just never gets that far, because the denylist answers `403` first. Use the `/domains/{domain}/subscriptions/...` routes from the panel. |\n| `POST /domains/{domain}/subscriptions/purchase`, `/switch`, `/auto-renew` | Moves money. |\n\nReading subscription, feature, traffic-usage, invoice and wallet state *is*\nallowed — only the money-moving writes are blocked.\n\n### If your account is managed by a reseller\n\nA second gate, entirely separate from the denylist above, sits in front of the\nroutes in this section. It keys on the **account**, not on the credential: it\nruns after both authentication paths have resolved the user, so a managed\nclient gets the identical `403` from a panel session and from an API key —\nthere is no way around it by switching credential. The single predicate is\nwhether the account has a provider at all. If you signed up with NSIN directly,\nnothing here applies to you; it also never fires for a reseller himself.\n\nA reseller's client owns his sites; his provider owns the money, the support\nrelationship and the sharing graph. So for a managed client these return `403`\nwith `code: \"managed_by_reseller\"`:\n\n| Surface | Why |\n|---|---|\n| `/wallet` and `/wallet/**` | The wallet is funded and owned by your provider. |\n| `/invoices`, `/invoices/{id}`, `/domains/{domain}/invoices/**` | Invoices are raised against your provider, who bills you out of band. |\n| `POST /domains/{domain}/subscriptions/purchase`, `/switch`, `/auto-renew` | Your provider buys and switches plans on your behalf. |\n| `/tickets` and `/tickets/**` | Support goes to your provider, not to NSIN. |\n| `POST /domains/` | Your provider provisions domains, because their traffic burns his wallet. Managing a domain you already have is untouched. |\n| `/domains/{domain}/invites/**`, `/invites/{token}`, `/invites/{token}/accept` | You cannot sub-grant access to a domain your provider funds, or accept an invitation into one. |\n| `/domains/{domain}/members/{userId}` — every method except `GET` | Reading the member list is allowed on purpose; changing it is not. |\n\nTwo traps in that table. `GET /domains/{domain}/members` stays open — your\nprovider appears in it as an `admin` and you are entitled to see who holds\nrights on your domain — but the member `PATCH` is blocked wholesale, so a\nmanaged client cannot change even his *own* notification preferences through\nit. And key management is blocked for him too, so a managed client cannot mint\nan API key in the first place: the keys that hit these `403`s are ones minted\nbefore the account moved under a provider.\n\nEverything else is untouched — domains, DNS, rules, SSL, cache, analytics,\nuptime. Plan and subscription **reads** stay open so you can see which plan a\ndomain is on and what it includes, but every price field is stripped from those\nresponses on the way out. NSIN does not know what your provider charges you,\nand NSIN's own retail price is not the number you pay. Read a missing `price`\nas *not disclosed*, never as free.\n\n## Rate limiting\n\nRequests are limited **per key**, by default to 300 requests per minute.\nExceeding it returns `429` with `{\"error\": \"rate limit exceeded\"}`.\nPanel (browser) traffic is limited separately and does not consume your key's\nbudget.\n\n## Conventions\n\n* **`{domain}` path parameter** — every path segment written as `{domain}` is\n  the domain **name** (`example.com`), not a numeric id. Percent-encode it if\n  it contains characters that are unsafe in a path segment.\n* **Errors** — every error body carries `error`, a human-readable message.\n  Some of them also carry `code`, a stable machine-readable reason, when the\n  distinction matters more than the sentence: `account_suspended`,\n  `domain_disabled`, `managed_by_reseller` and `invite_email_mismatch` (which\n  adds `invited_email` as well). Branch on `code` where it exists and never on\n  the wording of `error`. See the `Error` schema for what each one means. One\n  endpoint breaks even that shape: the removed `GET /subscriptions/current`\n  puts a machine string in `error` and the prose in `message`.\n* **Timestamps** — RFC 3339 / ISO 8601 strings in UTC unless stated otherwise.\n* **Byte counts** — always bytes; **traffic and quota** values are documented\n  per field.\n* **Access control** — a key inherits the permissions of the user who owns it.\n  For a shared domain that is the role granted to that user (`viewer`,\n  `editor`, `admin`); for your own domains it is `owner`. Endpoints document\n  the permission they require, and return `403` when the role lacks it and\n  `404` when the domain is not visible to you at all.\n\n## Plan features\n\nSeveral endpoints are gated on the domain's active plan (analytics, logs,\nWAF, custom certificates, …). When the plan does not include the feature the\nresponse is `403` with an `error` explaining which feature is missing.\n","contact":{"name":"NSIN Support","url":"https://nsin.cloud"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://api.nsin.cloud","description":"Production"}],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"tags":[{"name":"Domains","description":"Add, configure, verify and remove domains."},{"name":"DNS Records","description":"DNS record CRUD, bulk operations, zone scan and import."},{"name":"SSL","description":"Certificate status, manual issuance and custom certificate upload."},{"name":"Rules","description":"Edge rules — cache, WAF, redirect, rewrite, rate limit, captcha, bot\nrouting, origin selection, fingerprinting and error pages.\n\nEvery rule type below answers the same way on access. Reading needs\n`domain.view`, which every role has. Creating, updating, deleting,\ntoggling and reordering need `rules.edit`, which a `viewer` does not\nhave — those calls answer `403`, not `404`. Any of those writes against a\ndisabled domain answers `409` with `code: domain_disabled`, while the\nrules stay readable. Only a domain that does not exist, or that you hold\nno role on, is a `404`.\n"},{"name":"Cache","description":"Cache statistics, key browsing and purging."},{"name":"Analytics","description":"Traffic analytics, request logs and ad-hoc queries over your own traffic."},{"name":"Uptime","description":"Origin outage incidents and detection settings."},{"name":"Recommendations","description":"Per-domain advisory checklist."},{"name":"Sharing","description":"Domain members and invitations."},{"name":"Billing","description":"Read-only access to subscriptions, features, traffic usage, invoices and wallet."},{"name":"Support","description":"Support tickets."},{"name":"Account","description":"Account-wide reads."}],"paths":{"/domains/":{"get":{"tags":["Domains"],"operationId":"listDomains","summary":"List domains","description":"Every domain you can access — owned and shared with you — each with a\nshort SSL summary, its active subscription and your role on it.\n","responses":{"200":{"description":"Domain list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DomainWithSsl"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Domains"],"operationId":"createDomain","summary":"Add a domain","description":"Registers a domain on your account.\n\n* `dns_mode: managed` (default) — NSIN hosts the zone. The domain starts\n  in `pending` until its nameservers point at the NSIN set returned by\n  `GET /domains/ns-sets`, then flips to `active` automatically.\n* `dns_mode: external` — you keep DNS elsewhere. The domain starts in\n  `unverified` and you prove ownership with the TXT record from the\n  `verification` block, then call `POST /domains/{domain}/verify`.\n\nExisting records are scanned and imported in the background for managed\ndomains.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainCreate"}}}},"responses":{"200":{"description":"Domain created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDetail"}}}},"400":{"description":"Invalid or unsupported domain name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"409":{"description":"The domain already exists on this or another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/ns-sets":{"get":{"tags":["Domains"],"operationId":"listNameserverSets","summary":"List accepted nameserver sets","description":"The nameserver sets a managed domain's delegation may match. The\ndelegation must match **exactly one set in full** — nameservers from\ndifferent sets cannot be mixed. The first set is the one shown in the\npanel and is the recommended choice.\n","responses":{"200":{"description":"Accepted nameserver sets.","content":{"application/json":{"schema":{"type":"object","properties":{"sets":{"type":"array","description":"Each entry is one complete, acceptable nameserver set.","items":{"type":"array","items":{"type":"string"}},"examples":[[["th.ns.nsin.ir","ny.ns.nsin.ir","eu.ns.nsin.ir"]]]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Domains"],"operationId":"getDomain","summary":"Get a domain","description":"Full domain state, including nameserver/verification progress, your role\nand permissions on it, and every edge setting.\n","responses":{"200":{"description":"Domain detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Domains"],"operationId":"updateDomain","summary":"Update domain settings","description":"Partial update — omitted fields are left unchanged. Requires the\n`domain.settings` permission.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainUpdate"}}}},"responses":{"200":{"description":"Updated domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDetail"}}}},"400":{"description":"Invalid value — e.g. `dns_mode` not `managed`/`external`,\n`cache_l2_ttl_days` outside 1–7, or `cache_cap_mb` not one of the\nallowed tiers.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Read-only key, insufficient role, or the requested `cache_cap_mb`\nexceeds what the domain's plan allows.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Domains"],"operationId":"deleteDomain","summary":"Delete a domain","description":"Removes the domain, its records, rules and DNS zone. Owner only\n(`domain.delete`). This cannot be undone.\n","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/developer-mode":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["Domains"],"operationId":"enableDeveloperMode","summary":"Enable developer mode","description":"Bypasses all cache reads and writes for this domain at the edge, so you\nalways see the origin's current response. Auto-expires — the response\ncarries the expiry — so a forgotten toggle can never permanently disable\ncaching. Requires `domain.settings`.\n","responses":{"200":{"description":"Developer mode enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeveloperMode"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Domains"],"operationId":"disableDeveloperMode","summary":"Disable developer mode","description":"Turns developer mode off immediately. Requires `domain.settings`.","responses":{"200":{"description":"Developer mode disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeveloperMode"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/enable":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["Domains"],"operationId":"enableDomain","summary":"Re-enable a disabled domain","description":"Brings a `disabled` domain back into service. A managed domain returns to\n`pending` and is re-checked against the NSIN nameservers. Requires\n`domain.settings`.\n","responses":{"200":{"description":"Domain re-enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDetail"}}}},"400":{"description":"The domain is not in the `disabled` state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/check-ns":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["Domains"],"operationId":"checkNameservers","summary":"Check nameserver delegation now","description":"Runs an immediate delegation check for a `pending` or `moved` **managed**\ndomain instead of waiting for the background checker. On success the\ndomain is activated right away.\n\nRate-limited to once per hour per domain, independently of the API key\nrate limit. Requires `domain.settings`.\n","responses":{"200":{"description":"Check result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NsCheckResult"}}}},"400":{"description":"Not a managed domain, or the domain is not awaiting nameserver\nchanges.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"description":"Either the once-per-hour manual check limit or the API key rate\nlimit was hit.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/domains/{domain}/verify":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["Domains"],"operationId":"verifyDomain","summary":"Verify an external-DNS domain","description":"Checks for the TXT record described by the domain's `verification` block\nand activates the domain when it is found. Only valid for\n`dns_mode: external`. Requires `domain.settings`.\n","responses":{"200":{"description":"Verified — the domain is now active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResult"}}}},"400":{"description":"Not an external-DNS domain, or not awaiting verification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"The verification token has expired; call `verify/retry` for a fresh one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The TXT record was not found or did not match. `verified` is `false`\nand `error` explains what was seen.\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"verified":{"type":"boolean","const":false}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/verify/retry":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["Domains"],"operationId":"retryDomainVerification","summary":"Issue a fresh verification token","description":"Resets a failed external-DNS verification and mints a new TXT token. Use\nthe `verification` block of the response as the new record to publish.\nRequires `domain.settings`.\n","responses":{"200":{"description":"Verification reset with a new token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDetail"}}}},"400":{"description":"Not an external-DNS domain, or not in the failed-verification state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/ssl/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["SSL"],"operationId":"getSslInfo","summary":"Get certificate status","description":"The domain's current certificate — issuer, validity, SANs, key size — plus\nwhether a manual re-issue is currently allowed, and `coverage`: proxied\nhostnames that are **not** on the certificate yet, with their retry state.\n","responses":{"200":{"description":"Certificate status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SslInfo"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["SSL"],"operationId":"uploadCustomCertificate","summary":"Upload a custom certificate","description":"Installs your own certificate and private key for the domain. Include the\nfull chain (leaf **and** intermediates) in `certificate` — a leaf-only\nupload makes clients fail chain verification.\n\n`hostnames` selects which of the certificate's SANs this upload should\ncover; use the `eligible` list from `POST /domains/{domain}/ssl/parse` to\npick them. Requires `ssl.manage` and a plan that includes custom\ncertificates.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomCertificateUpload"}}}},"responses":{"200":{"description":"Certificate installed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomCertificateResult"}}}},"400":{"description":"Missing fields, unparseable PEM, key/certificate mismatch, an expired\ncertificate, or a hostname that the certificate does not cover.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Read-only key, insufficient role, or the plan does not include custom certificates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/ssl/parse":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["SSL"],"operationId":"parseCustomCertificate","summary":"Inspect a certificate before uploading","description":"Parses a certificate PEM and reports its subject, issuer, validity and\nSANs — without installing anything. `eligible` lists the SANs that belong\nto this domain and may therefore be passed as `hostnames` to the upload\ncall; `default_selection` is the subset the panel pre-selects.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["certificate"],"properties":{"certificate":{"type":"string","description":"PEM-encoded certificate."}}}}}},"responses":{"200":{"description":"Parsed certificate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParsedCertificate"}}}},"400":{"description":"Missing or unparseable certificate PEM.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/ssl/issue":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["SSL"],"operationId":"issueCertificate","summary":"Request certificate issuance","description":"Starts an ACME order for the domain. Allowed only when SSL is currently\n`missing` or `failed` — certificates are otherwise issued and renewed\nautomatically. Not available for `dns_mode: external` domains.\n\nIssuance is asynchronous: this returns immediately with\n`status: \"pending\"`; poll `GET /domains/{domain}/ssl/` for the outcome.\nManual attempts are rate-limited per domain — `GET /ssl/` reports\n`can_manual_issue` and `next_manual_issue_at`. Requires `ssl.manage`.\n","responses":{"200":{"description":"Issuance started.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","examples":["SSL issuance started"]},"status":{"type":"string","const":"pending"},"last_issue_attempt_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"The domain uses external DNS, or SSL is not in a state where a manual\nissue is allowed.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"An issuance is already in progress.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-domain manual issuance cooldown has not elapsed, or the API\nkey rate limit was hit.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The certificate issuer is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/domains/{domain}/records/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["DNS Records"],"operationId":"listRecords","summary":"List DNS records","description":"All records of the domain, newest first.\n\nProxied records may carry `origin_rules` — origin route or origin pool\nrules that override where that record's traffic actually goes, so the\neffective origin is **not** the record's `destination`. Routes are listed\nbefore pools, mirroring edge precedence.\n","responses":{"200":{"description":"Record list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RecordWithOriginRules"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["DNS Records"],"operationId":"createRecord","summary":"Create a DNS record","description":"Creates one record and publishes it to the DNS zone.\n\nSetting `proxied: true` routes the hostname through the NSIN edge: the\npublished DNS answer becomes the NSIN proxy IP and `destination` becomes\nthe origin the edge connects to. Only `A`, `AAAA`, `CNAME` and `ANAME`\ncan be proxied. Requires `records.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordCreate"}}}},"responses":{"200":{"description":"Record created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"400":{"description":"Invalid record — bad type, malformed destination, or a value the zone rejects.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"The domain is disabled, or a conflicting record already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/{recordId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RecordId"}],"put":{"tags":["DNS Records"],"operationId":"updateRecord","summary":"Update a DNS record","description":"Partial update — omitted fields keep their current value. Requires\n`records.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordUpdate"}}}},"responses":{"200":{"description":"Updated record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Record"}}}},"400":{"description":"Invalid value, or the record is not editable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or record not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["DNS Records"],"operationId":"deleteRecord","summary":"Delete a DNS record","description":"Removes the record from the zone. Requires `records.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or record not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/batch-update":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["DNS Records"],"operationId":"batchUpdateRecords","summary":"Update many records at once","description":"Applies an update to many records in one request. Each item accepts\nexactly the same optional fields as a single `PUT`.\n\n**Best-effort:** every record is processed independently, so one bad\nrecord does not abort the rest. The response always returns `200` with a\nper-record `results` array — check it rather than the status code.\nRequires `records.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["updates"],"properties":{"updates":{"type":"array","items":{"allOf":[{"type":"object","required":["id"],"properties":{"id":{"type":"integer","description":"Id of the record to update."}}},{"$ref":"#/components/schemas/RecordUpdate"}]}}}}}}},"responses":{"200":{"description":"Per-record outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/batch-delete":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["DNS Records"],"operationId":"batchDeleteRecords","summary":"Delete many records at once","description":"Deletes many records in one request. Best-effort per record — see\n`batch-update`. Requires `records.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Per-record outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/scan":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["DNS Records"],"operationId":"scanRecords","summary":"Scan the domain's existing DNS from public resolvers","description":"Queries public resolvers for records that already exist for this domain\nand returns them as an import preview — nothing is written. Each entry is\nmarked `new`, `overwrite` (an NSIN record with the same name and type\nalready exists) or `unsupported`.\n\nUse this to review before calling `scan-import`. Requires `records.edit`.\n","responses":{"200":{"description":"Scan preview.","content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","items":{"$ref":"#/components/schemas/ImportPreviewRecord"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/scan-import":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["DNS Records"],"operationId":"scanImportRecords","summary":"Scan and import in one step","description":"Scans the domain's existing DNS from public resolvers and imports\neverything it finds, without a review step. Convenient right after adding\na domain. Requires `records.edit`.\n","responses":{"200":{"description":"Import outcome, plus the domain's full record list afterwards.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"The domain is disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/import/parse":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["DNS Records"],"operationId":"parseZoneFile","summary":"Parse a zone file into an import preview","description":"Accepts a BIND-style zone file and returns what would be imported, with\neach entry marked `new`, `overwrite` or `unsupported`. Nothing is\nwritten — pass the entries you want to `POST .../records/import`.\nRequires `records.edit`.\n","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"The zone file to parse."}}}},"text/plain":{"schema":{"type":"string","description":"Raw zone file contents."}}}},"responses":{"200":{"description":"Import preview.","content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","items":{"$ref":"#/components/schemas/ImportPreviewRecord"}}}}}}},"400":{"description":"The zone file could not be parsed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/records/import":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["DNS Records"],"operationId":"importRecords","summary":"Import records","description":"Creates the supplied records, overwriting any existing record with the\nsame name and type. Best-effort per record — the response counts what\nsucceeded and lists what failed. Requires `records.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["records"],"properties":{"records":{"type":"array","items":{"$ref":"#/components/schemas/ImportRecordItem"}}}}}}},"responses":{"200":{"description":"Import outcome.","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"integer"},"overwritten":{"type":"integer"},"failed":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"error":{"type":"string"}}}}}}}}},"400":{"description":"Malformed request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/cache/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listCacheRules","summary":"List cache rules","description":"Decides what the edge caches, for how long, and which safety bypasses apply. A domain may hold several cache rules with different tradeoffs; each is self-contained.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Cache rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CacheRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createCacheRule","summary":"Create a cache rule","description":"Decides what the edge caches, for how long, and which safety bypasses apply. A domain may hold several cache rules with different tradeoffs; each is self-contained.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/cache/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderCacheRules","summary":"Reorder cache rules","description":"Sets the `priority` of several cache rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/cache/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getCacheRule","summary":"Get a cache rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateCacheRule","summary":"Update a cache rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteCacheRule","summary":"Delete a cache rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/cache/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleCacheRule","summary":"Enable or disable a cache rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/drop/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listDropRules","summary":"List drop rules","description":"Blocks matching requests at the edge, optionally restricted by visitor country.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Drop rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DropRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createDropRule","summary":"Create a drop rule","description":"Blocks matching requests at the edge, optionally restricted by visitor country.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/drop/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderDropRules","summary":"Reorder drop rules","description":"Sets the `priority` of several drop rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/drop/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getDropRule","summary":"Get a drop rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateDropRule","summary":"Update a drop rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteDropRule","summary":"Delete a drop rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/drop/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleDropRule","summary":"Enable or disable a drop rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/redirect/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listRedirectRules","summary":"List redirect rules","description":"Returns an HTTP redirect for matching requests instead of proxying them to the origin.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Redirect rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RedirectRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createRedirectRule","summary":"Create a redirect rule","description":"Returns an HTTP redirect for matching requests instead of proxying them to the origin.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/redirect/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderRedirectRules","summary":"Reorder redirect rules","description":"Sets the `priority` of several redirect rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/redirect/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getRedirectRule","summary":"Get a redirect rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateRedirectRule","summary":"Update a redirect rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteRedirectRule","summary":"Delete a redirect rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/redirect/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleRedirectRule","summary":"Enable or disable a redirect rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rewrite/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listRewriteRules","summary":"List rewrite rules","description":"Rewrites the path and/or query string before the request is sent to the origin. The visitor's URL is unchanged.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Rewrite rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RewriteRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createRewriteRule","summary":"Create a rewrite rule","description":"Rewrites the path and/or query string before the request is sent to the origin. The visitor's URL is unchanged.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewriteRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewriteRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rewrite/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderRewriteRules","summary":"Reorder rewrite rules","description":"Sets the `priority` of several rewrite rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rewrite/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getRewriteRule","summary":"Get a rewrite rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewriteRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateRewriteRule","summary":"Update a rewrite rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewriteRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewriteRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteRewriteRule","summary":"Delete a rewrite rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rewrite/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleRewriteRule","summary":"Enable or disable a rewrite rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewriteRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/waf/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listWafRules","summary":"List WAF rules","description":"Runs the OWASP Core Rule Set against matching requests at the chosen paranoia level and blocks once the anomaly score passes the threshold.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"WAF rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WafRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createWafRule","summary":"Create a WAF rule","description":"Runs the OWASP Core Rule Set against matching requests at the chosen paranoia level and blocks once the anomaly score passes the threshold.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WafRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WafRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/waf/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderWafRules","summary":"Reorder WAF rules","description":"Sets the `priority` of several WAF rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/waf/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getWafRule","summary":"Get a WAF rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WafRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateWafRule","summary":"Update a WAF rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WafRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WafRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteWafRule","summary":"Delete a WAF rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/waf/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleWafRule","summary":"Enable or disable a WAF rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WafRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/captcha/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listCaptchaRules","summary":"List captcha rules","description":"Challenges visitors on matching paths before letting them through. A solved challenge is remembered for `ttl_sec`.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Captcha rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CaptchaRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createCaptchaRule","summary":"Create a captcha rule","description":"Challenges visitors on matching paths before letting them through. A solved challenge is remembered for `ttl_sec`.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptchaRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptchaRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/captcha/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderCaptchaRules","summary":"Reorder captcha rules","description":"Sets the `priority` of several captcha rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/captcha/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getCaptchaRule","summary":"Get a captcha rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptchaRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateCaptchaRule","summary":"Update a captcha rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptchaRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptchaRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteCaptchaRule","summary":"Delete a captcha rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/captcha/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleCaptchaRule","summary":"Enable or disable a captcha rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptchaRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rate-limit/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listRateLimitRules","summary":"List rate limit rules","description":"Counts requests per key over a sliding window and drops or challenges the ones above the limit.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Rate limit rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RateLimitRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createRateLimitRule","summary":"Create a rate limit rule","description":"Counts requests per key over a sliding window and drops or challenges the ones above the limit.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rate-limit/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderRateLimitRules","summary":"Reorder rate limit rules","description":"Sets the `priority` of several rate limit rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rate-limit/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getRateLimitRule","summary":"Get a rate limit rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateRateLimitRule","summary":"Update a rate limit rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteRateLimitRule","summary":"Delete a rate limit rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/rate-limit/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleRateLimitRule","summary":"Enable or disable a rate limit rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/bot-route/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listBotRouteRules","summary":"List bot route rules","description":"Acts on classified bot traffic — block it, serve alternative content, send it to a different origin, or just tag it in telemetry.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Bot route rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BotRouteRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createBotRouteRule","summary":"Create a bot route rule","description":"Acts on classified bot traffic — block it, serve alternative content, send it to a different origin, or just tag it in telemetry.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotRouteRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotRouteRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/bot-route/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderBotRouteRules","summary":"Reorder bot route rules","description":"Sets the `priority` of several bot route rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/bot-route/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getBotRouteRule","summary":"Get a bot route rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotRouteRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateBotRouteRule","summary":"Update a bot route rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotRouteRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotRouteRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteBotRouteRule","summary":"Delete a bot route rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/bot-route/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleBotRouteRule","summary":"Enable or disable a bot route rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotRouteRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_pool/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listOriginPoolRules","summary":"List origin pool rules","description":"Load-balances matching traffic across several origins with optional health checking. Overrides the DNS record's own destination for every path it matches.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Origin pool rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OriginPoolRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createOriginPoolRule","summary":"Create a origin pool rule","description":"Load-balances matching traffic across several origins with optional health checking. Overrides the DNS record's own destination for every path it matches.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginPoolRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginPoolRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_pool/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderOriginPoolRules","summary":"Reorder origin pool rules","description":"Sets the `priority` of several origin pool rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_pool/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getOriginPoolRule","summary":"Get a origin pool rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginPoolRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateOriginPoolRule","summary":"Update a origin pool rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginPoolRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginPoolRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteOriginPoolRule","summary":"Delete a origin pool rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_pool/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleOriginPoolRule","summary":"Enable or disable a origin pool rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginPoolRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_route/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listOriginRouteRules","summary":"List origin route rules","description":"Sends matching paths to a different origin than the DNS record's destination. Takes precedence over origin pools.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Origin route rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OriginRouteRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createOriginRouteRule","summary":"Create a origin route rule","description":"Sends matching paths to a different origin than the DNS record's destination. Takes precedence over origin pools.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginRouteRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginRouteRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_route/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderOriginRouteRules","summary":"Reorder origin route rules","description":"Sets the `priority` of several origin route rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_route/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getOriginRouteRule","summary":"Get a origin route rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginRouteRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateOriginRouteRule","summary":"Update a origin route rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginRouteRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginRouteRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteOriginRouteRule","summary":"Delete a origin route rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/origin_route/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleOriginRouteRule","summary":"Enable or disable a origin route rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginRouteRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/fingerprint/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listFingerprintRules","summary":"List fingerprint rules","description":"Matches requests on their TLS/HTTP fingerprint (JA4, JA4H) and drops, challenges or tags them.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Fingerprint rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FingerprintRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createFingerprintRule","summary":"Create a fingerprint rule","description":"Matches requests on their TLS/HTTP fingerprint (JA4, JA4H) and drops, challenges or tags them.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FingerprintRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FingerprintRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/fingerprint/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderFingerprintRules","summary":"Reorder fingerprint rules","description":"Sets the `priority` of several fingerprint rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/fingerprint/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getFingerprintRule","summary":"Get a fingerprint rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FingerprintRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateFingerprintRule","summary":"Update a fingerprint rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FingerprintRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FingerprintRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteFingerprintRule","summary":"Delete a fingerprint rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/fingerprint/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleFingerprintRule","summary":"Enable or disable a fingerprint rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FingerprintRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/error-page/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Rules"],"operationId":"listErrorPageRules","summary":"List error page rules","description":"Controls what visitors see for selected status codes — the NSIN branded page, your own HTML, or the origin's own response passed through untouched.\n\nReturned in evaluation order. Requires `domain.view`.\n","responses":{"200":{"description":"Error page rules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ErrorPageRule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Rules"],"operationId":"createErrorPageRule","summary":"Create a error page rule","description":"Controls what visitors see for selected status codes — the NSIN branded page, your own HTML, or the origin's own response passed through untouched.\n\nRequires `rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorPageRuleBody"}}}},"responses":{"200":{"description":"Rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorPageRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RulePlanLimited"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/error-page/reorder":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"patch":{"tags":["Rules"],"operationId":"reorderErrorPageRules","summary":"Reorder error page rules","description":"Sets the `priority` of several error page rules at once. Lower priority\nvalues are evaluated first. The body is a bare array. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderRequest"}}}},"responses":{"200":{"description":"Number of rules whose priority changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleReorderResult"}}}},"400":{"description":"Malformed body, or a negative priority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleDomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/error-page/{ruleId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"get":{"tags":["Rules"],"operationId":"getErrorPageRule","summary":"Get a error page rule","description":"Requires `domain.view`.","responses":{"200":{"description":"The rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorPageRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Rules"],"operationId":"updateErrorPageRule","summary":"Update a error page rule","description":"Partial update — omitted fields keep their current value. Requires\n`rules.edit`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorPageRuleBody"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorPageRule"}}}},"400":{"$ref":"#/components/responses/RuleInvalid"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Rules"],"operationId":"deleteErrorPageRule","summary":"Delete a error page rule","description":"Requires `rules.edit`.","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/rules/error-page/{ruleId}/toggle":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/RuleId"}],"patch":{"tags":["Rules"],"operationId":"toggleErrorPageRule","summary":"Enable or disable a error page rule","description":"Flips the rule's `enabled` flag. Takes no request body — the new state\nis always the opposite of the current one, and is returned. Requires\n`rules.edit`.\n","responses":{"200":{"description":"The rule in its new state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorPageRule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/RuleNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/overview":{"get":{"tags":["Analytics"],"operationId":"analyticsOverview","summary":"Per-domain totals across your account","description":"One row per domain you can access, with request, bandwidth and visitor\ntotals for the period. Account-wide — takes no `domain` parameter.\n","parameters":[{"$ref":"#/components/parameters/Period"}],"responses":{"200":{"description":"One entry per domain.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OverviewItem"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/domains-overview":{"get":{"tags":["Analytics"],"operationId":"analyticsDomainsOverview","summary":"Per-domain totals with sparkline","description":"Like `/analytics/overview`, plus an error rate, a small\nrequests-over-time series for sparklines, and the most recent log\ntimestamp seen for each domain.\n","parameters":[{"$ref":"#/components/parameters/Period"}],"responses":{"200":{"description":"One entry per domain.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DomainsOverviewItem"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/global-summary":{"get":{"tags":["Analytics"],"operationId":"analyticsGlobalSummary","summary":"Account-wide summary","description":"Headline figures aggregated across every domain you can access.","parameters":[{"$ref":"#/components/parameters/Period"}],"responses":{"200":{"description":"Account-wide totals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/bandwidth-overview":{"get":{"tags":["Analytics"],"operationId":"analyticsBandwidthOverview","summary":"Origin-direction bandwidth across your domains","description":"Bytes sent to and received from origins, as a time series plus per-domain\ntotals.\n\n`ratio` is `min(up,down) / max(up,down)`. A value near `1.0` means the\ndomain pushes about as much to the origin as it pulls back, which is\nunusual for web traffic (downloads normally dominate) and sets\n`flagged`.\n","parameters":[{"$ref":"#/components/parameters/Period"}],"responses":{"200":{"description":"Bandwidth series and per-domain totals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginBandwidthResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/tunnel-suspects":{"get":{"tags":["Analytics"],"operationId":"analyticsTunnelSuspects","summary":"Clients whose traffic resembles a proxy tunnel","description":"Clients whose WebSocket/gRPC traffic looks like a VPN or proxy tunnel run\nbehind the CDN: sustained volume over a single fixed path, with opaque\npayloads and no sign of ordinary browsing (no real assets fetched, no\nreferer).\n\nThis is a heuristic for investigation, not proof of abuse. `balance` is\ninformational — tunnels used for browsing are download-heavy, so\nsymmetry is **not** a criterion.\n","parameters":[{"$ref":"#/components/parameters/Period"}],"responses":{"200":{"description":"Suspected tunnel clients.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TunnelSuspectsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/nodes":{"get":{"tags":["Analytics"],"operationId":"analyticsListNodes","summary":"List edge nodes","description":"Active edge nodes (points of presence). Use `name` as the `node` filter\non `/analytics/traffic-by-node` and `/analytics/origins`.\n","responses":{"200":{"description":"Edge nodes.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Node identifier used in filters."},"label":{"type":"string","description":"Human-readable name."},"country":{"type":"string","description":"ISO country code."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/node-countries":{"get":{"tags":["Analytics"],"operationId":"analyticsNodeCountries","summary":"List edge node countries","description":"The distinct countries edge nodes are located in.","responses":{"200":{"description":"Country codes.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/summary":{"get":{"tags":["Analytics"],"operationId":"analyticsSummary","summary":"Traffic summary for one domain","description":"Headline figures for the domain over the period: requests, bandwidth,\nunique visitors, error rate and latency percentiles.\n\nUnique visitors are counted as distinct (client IP, JA4 TLS\nfingerprint) pairs, which separates people sharing one NAT address by\ndevice. On plain HTTP there is no JA4, so it degrades to counting IPs.\nLatency figures exclude WebSocket requests, whose duration spans the\nwhole connection.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Summary figures.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSummary"}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/requests-compare":{"get":{"tags":["Analytics"],"operationId":"analyticsRequestsCompare","summary":"Request counts across days, for overlaying","description":"Request counts bucketed by hour or by day over a window of whole days,\nso the dashboard can draw one line per day and compare them.\n\nScope, in order:\n  * `domain_id` given - that domain alone, and 404 if the caller may not see it.\n  * `scope=all` - every domain. Admins only; anyone else falls through.\n  * otherwise - every ACTIVE domain the caller owns or is a member of.\n\nThe scoping parameter is `domain_id`, in snake_case. It is the odd one\nout on this surface, and getting it wrong is silent: a camelCase\n`domainId` is never read, so the handler sees no id, falls through to\nthe third branch, and answers for every domain you can see instead of\nthe one you asked for. Nothing in the response says the scope was\nwidened.\n\n`domain_id` selects that domain whatever its status; the fall-through\ncounts ACTIVE domains only. Naming a `pending` or `disabled` domain by\nid therefore returns traffic the account-wide query leaves out.\n\nBuckets are anchored to local midnight, so today's partial day is still a\nwhole-day bucket and lines stay aligned.\n","parameters":[{"name":"granularity","in":"query","schema":{"type":"string","enum":["hour","day"],"default":"hour"},"description":"`hour` overlays one line per day; `day` gives a single daily series.\nOnly `day` is recognised as itself: every other value, misspelling\nincluded, is silently treated as `hour` rather than rejected.\n"},{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":35},"description":"Window length in whole days. Both the default and the cap depend on\n`granularity`: `hour` defaults to 3 and is capped at 14, `day`\ndefaults to 14 and is capped at 35 - the ClickHouse rows expire at\n35 days, so nothing older exists to chart. The schema's `maximum` is\nthe higher of the two caps because one schema cannot express the\nsplit; over the cap for the granularity you asked for, the value is\nclamped, not rejected, and 0, a negative number or a non-numeric\nvalue takes the default. Either way the window can come back shorter\nthan you asked for with no error.\n"},{"name":"scope","in":"query","schema":{"type":"string","enum":["domains","all"],"default":"domains"},"description":"`all` is honoured for admins only; for anyone else it is ignored and\nthe query stays scoped to their own domains. It is also ignored\nwhenever `domain_id` is set - that branch wins outright.\n"},{"name":"domain_id","in":"query","description":"Numeric domain id. snake_case, not `domainId` - see the scoping note\nabove. 404 if the caller may not see the domain.\n","schema":{"type":"integer"}}],"responses":{"200":{"description":"Time series. This operation formats `timestamp` as\n`2026-08-16 13:00:00` - a space separator and no zone suffix - not\nthe RFC 3339 `2026-08-16T13:00:00Z` the shared schema's\n`format: date-time` implies and the other series endpoints return.\nParse it as a local-time stamp, not as RFC 3339.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestsDataPoint"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/requests":{"get":{"tags":["Analytics"],"operationId":"analyticsRequests","summary":"Requests over time","description":"Request counts bucketed by hour (periods up to 24h) or by day (`7d`,\n`30d`).\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Time series.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestsDataPoint"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/visitors":{"get":{"tags":["Analytics"],"operationId":"analyticsVisitors","summary":"Unique visitors over time","description":"Distinct visitors per bucket, counted as (client IP, JA4) pairs. Note\nthat visitors do not sum across buckets — the same person appears in\nevery bucket they were active in.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Time series.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestsDataPoint"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/bandwidth":{"get":{"tags":["Analytics"],"operationId":"analyticsBandwidth","summary":"Bandwidth over time","description":"Bytes in and out per bucket, from the visitor's perspective.","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Time series.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BandwidthDataPoint"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/top-uris":{"get":{"tags":["Analytics"],"operationId":"analyticsTopUris","summary":"Most requested URIs","description":"The ten busiest page URLs for the domain, by request count. The limit is hardcoded — there is no `limit` and no paging — and `domain` names exactly one domain: unlike `/analytics/requests-compare`, the per-domain sections have no account-wide or admin scope.\n\nRows are grouped by the whole `uri`, query string included, so `/search?q=a` and `/search?q=b` compete for separate slots. URIs whose path ends in a `.<ext>` are excluded in SQL so that assets (`.css`, `.js`, `.png`, fonts) cannot crowd out pages — which also excludes `.html` and `.php`, so a site that serves its pages with an extension sees very little here; `/analytics/top-requests?metric=uris` is the unfiltered ranking (100 rows, query string stripped), but it needs a plan with the `logs` feature where this one needs only `monitoring`. The `403` is always that plan gate and never your role — every role that can see a domain, down to `viewer`, carries `analytics.view` — and a domain with no active plan at all answers `402` rather than `403`.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Top URIs by request count.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TopUri"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/top-requests":{"get":{"tags":["Analytics"],"operationId":"analyticsTopRequests","summary":"Top-N breakdown by a chosen metric","description":"A ranked breakdown of the domain's traffic. `metric` selects what is\nranked, and which fields of each row are populated — rows omit the\nfields that do not apply.\n\nRequires a plan including the `logs` feature.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"name":"metric","in":"query","required":true,"description":"* `slow_requests` — slowest paths, with average and maximum duration. Excludes WebSockets.\n* `uris` — most requested paths.\n* `errors_5xx` — paths returning server errors.\n* `hosts` — busiest subdomains.\n* `countries` — busiest visitor countries.\n* `user_agents` — busiest user agents.\n* `networks` — busiest visitor networks, keyed `AS<number>` with the operator in `label`.\n","schema":{"type":"string","enum":["slow_requests","uris","errors_5xx","hosts","countries","user_agents","networks"]}},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Ranked rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TopRequestRow"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/countries":{"get":{"tags":["Analytics"],"operationId":"analyticsCountries","summary":"Traffic by visitor country","description":"Per-country totals for the domain — requests, bytes served to visitors, and unique visitors — ranked by requests and truncated to the top 20 countries.\n\nThe country is a GeoLite2 lookup of the client IP performed once at ingest, so a visitor falls in exactly one country and these `unique_visitors` do sum to the `unique_visitors` of `/analytics/summary`, within `uniq()`'s HLL error and only when the domain saw traffic from 20 countries or fewer. IPs the database cannot place — and every request logged while no GeoIP database was loaded at all — are grouped under the code `ZZ`, which is a bucket rather than a country.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-country totals.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CountryStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/asns":{"get":{"tags":["Analytics"],"operationId":"analyticsAsns","summary":"Traffic by visitor network (ASN)","description":"The ten busiest visitor networks for the domain, by request count, each with its autonomous system number, an operator name and bytes served.\n\nRequests whose ASN could not be resolved are dropped from the query (`asn != 0`), so these counts do not add up to the domain's total requests, and if `GeoLite2-ASN.mmdb` was never shipped to the backend the endpoint returns an empty list while traffic is plainly flowing. `asn_org` is `any()` of the operator strings recorded for that ASN in the window, so a network that changed its registered name mid-period shows one of them arbitrarily.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-network totals.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AsnStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/protocols":{"get":{"tags":["Analytics"],"operationId":"analyticsProtocols","summary":"Traffic by HTTP protocol version","description":"Request counts by the version the client spoke to the edge, bucketed into `h1`, `h2`, `h3` and `other` and ordered by count — at most four rows, and only for buckets that saw traffic.\n\n`other` is a catch-all for anything not logged as `HTTP/1.x`, `HTTP/2.x` or `HTTP/3.x`, including requests logged with no protocol string at all, so it is not a fifth protocol. The version the edge spoke to your origin is a separate field and appears only on raw log rows (`origin_protocol` on `/analytics/logs`, which needs the `logs` feature).\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-protocol request counts.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProtocolStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/status-codes":{"get":{"tags":["Analytics"],"operationId":"analyticsStatusCodes","summary":"Traffic by HTTP status code","description":"One row per distinct status the domain returned, ordered by count descending rather than by code, and deliberately uncapped — a domain being scanned can come back with dozens of rows, so do not assume a short list.\n\nFailed requests are counted here too, under whatever status the edge showed the visitor rather than anything the origin returned; `/analytics/unreachable-reasons` breaks those same requests down by cause and fault. Note that the `503` fires only when the ClickHouse connection was never opened at startup: an analytics store that is merely down, or a query that outruns its timeout (10s, rising to 20s for `7d` and 45s for `30d`), answers `500` instead, so retry logic should treat the two alike.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-status-code counts.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StatusCodeStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/unreachable-reasons":{"get":{"tags":["Analytics"],"operationId":"analyticsUnreachableReasons","summary":"Why requests could not be served","description":"A breakdown of failed requests by cause, with plain-language\nexplanations and who is responsible (`client`, `origin`, `network` or\n`config`) — so you can tell a visitor hanging up from your server\ncrashing without reading raw proxy errors.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Failure reasons.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UnreachableReason"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/cache":{"get":{"tags":["Analytics"],"operationId":"analyticsCache","summary":"Cache hit, miss and bypass counts","description":"`hit_rate` is `hits / (hits + misses)` — bypasses are excluded from the\ndenominator, since a bypassed request was never a caching candidate.\n`bypass_reasons` breaks down why requests bypassed the cache.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Cache counters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheAnalytics"}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/traffic-by-cache":{"get":{"tags":["Analytics"],"operationId":"analyticsTrafficByCache","summary":"Egress bytes by cache status over time","description":"Bytes served to visitors per time bucket, split three ways by cache status: `hit`, `miss` and `bypass`.\n\nRequests logged with no cache status never entered the caching pipeline and count towards none of the three, so the columns do not add up to the `bytes_out` of `/analytics/bandwidth`; `/analytics/traffic-by-reqstatus` is the orthogonal split of the same bytes by serving path. Buckets are hourly up to `24h` and daily for `7d` and `30d`, but the window starts exactly one period ago while buckets are anchored to the hour or day, so the oldest bucket is partial — and buckets with no traffic are omitted rather than zero-filled, so a chart has to fill its own gaps. It also honours a `node` filter (a `name` from `GET /analytics/nodes`) that is not listed above. As with every section endpoint, the response is cached in-process per domain, period and filter — one minute, three at `7d`, ten at `30d` — so polling faster than that returns the same numbers.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Time series.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TrafficByCacheDataPoint"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/traffic-by-reqstatus":{"get":{"tags":["Analytics"],"operationId":"analyticsTrafficByReqStatus","summary":"Egress bytes by serving path over time","description":"Serving path is orthogonal to cache status: `cache` went through the\ncaching pipeline, `proxied` reached the origin through the edge proxy,\nand `direct` reached the origin without it.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Time series.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TrafficByReqStatusDataPoint"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/traffic-by-node":{"get":{"tags":["Analytics"],"operationId":"analyticsTrafficByNode","summary":"Traffic by edge node","description":"Requests and egress bytes per edge node, split by cache status.","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/NodeFilter"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-node totals.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TrafficByNodeStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/origins":{"get":{"tags":["Analytics"],"operationId":"analyticsOrigins","summary":"Edge-to-origin request statistics","description":"How each of your origin addresses is performing as seen from the edge —\nrequest counts, failures, server errors and upstream latency — with the\nper-node split that produced them.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/NodeFilter"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-origin statistics.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OriginStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/user-agents":{"get":{"tags":["Analytics"],"operationId":"analyticsUserAgents","summary":"Traffic by user-agent category","description":"Request counts by broad client category, and only for categories that saw traffic: `Search Engine Bots`, `AI/LLM Crawlers`, `Mobile Devices`, `Desktop Browsers` and `Scripted Agents` — five fixed buckets, so at most five rows, uncapped only because there is nothing more to return.\n\nClassification is substring matching on the raw user-agent string in that order, first match wins, so Googlebot on a phone is a search-engine bot and Chrome on Android is `Mobile Devices` and never `Desktop Browsers`: this is not a device split. Nothing is verified — a scraper that claims to be Googlebot is counted as Googlebot; the checked classification lives on raw log rows (`bot_kind` and `bot_verified` on `/analytics/logs`, which needs the `logs` feature). `Scripted Agents` is the leftover bucket, holding curl, unrecognised apps and requests that sent no user-agent header at all.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/HostnameFilter"},{"$ref":"#/components/parameters/PathFilter"}],"responses":{"200":{"description":"Per-category request counts.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UserAgentCategoryStats"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/logs":{"get":{"tags":["Analytics"],"operationId":"analyticsLogs","summary":"Raw request logs","description":"Individual request records, newest first, with every filter applied as\nan AND. Requires a plan including the `logs` feature.\n\nHeader and body fields are retained for a shorter window than the rest\nof the row, so older entries return them empty.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"name":"limit","in":"query","description":"Rows per page, 1–500. Values outside the range fall back to 100.","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0,"minimum":0}},{"name":"status","in":"query","description":"One exact status code (`404`), or a whole class written `4xx` /\n`5xx`, which matches 400-499 / 500-599. Only the first character of\nan `xx` value is read, and it goes into the query as a digit: a\nnon-digit there is not rejected, it breaks the query and the\nendpoint answers 500.\n","schema":{"type":"string"}},{"name":"method","in":"query","description":"HTTP method — case-insensitive.","schema":{"type":"string"}},{"name":"uri","in":"query","description":"Substring of the request URI. The stored URI is percent-encoded\nexactly as the client sent it, and the filter tries both the raw\ncolumn and its decoded form, so a readable non-ASCII string and its\nalready-encoded equivalent find the same rows.\n","schema":{"type":"string"}},{"name":"cache","in":"query","description":"Cache status: `hit`, `miss` or `bypass`.","schema":{"type":"string","enum":["hit","miss","bypass"]}},{"name":"reqStatus","in":"query","description":"Serving path: `cache`, `proxied` or `direct`.","schema":{"type":"string","enum":["cache","proxied","direct"]}},{"name":"rayId","in":"query","description":"Exact ray id of a single request.","schema":{"type":"string"}},{"name":"hostname","in":"query","description":"Exact host, a subdomain of it, or a bare subdomain label.","schema":{"type":"string"}},{"name":"originHost","in":"query","description":"Case-insensitive substring of the `Host` header the edge sent\nupstream. Matched inside the JSON of the edge-to-origin request\nheaders, under that key specifically, so another header carrying the\nsame text cannot produce a false hit.\n","schema":{"type":"string"}},{"name":"originSni","in":"query","description":"Case-insensitive substring of the SNI presented to the origin, read\nthe same way as `originHost` - from the `Nsn-Origin-Sni` key of the\norigin request headers.\n","schema":{"type":"string"}},{"name":"originAddr","in":"query","description":"Substring of the resolved origin address the edge connected to. It\nis stored as `ip:port`, so a bare `1.2.3.4` also matches\n`1.2.3.4:443`.\n","schema":{"type":"string"}},{"name":"originAddrs","in":"query","description":"Comma-separated list, OR'd together - but despite the name this is\nnot the plural of `originAddr`. Each value is matched as a\ncase-insensitive substring anywhere in the origin request-header\nJSON, not against the origin address column, so a value can hit on a\nheader that is not an address at all.\n","schema":{"type":"string"}},{"name":"remoteAddr","in":"query","description":"Substring of the visitor's IP, so a prefix like `10.0.` or a partial\nIPv6 address works.\n","schema":{"type":"string"}},{"name":"country","in":"query","description":"Visitor country, derived from the client IP, as an exact ISO 3166-1\nalpha-2 code. Upper-cased for you, so `ir` and `IR` are the same\nfilter.\n","schema":{"type":"string"}},{"name":"nodeCountry","in":"query","description":"ISO country of the edge node that served the request. Resolved\nagainst the node inventory first and then matched on those node\nnames, so a country with no nodes returns an empty page rather than\nan error.\n","schema":{"type":"string"}},{"name":"node","in":"query","description":"Edge node name, from `name` in `GET /analytics/nodes`. Matched\nexactly, case-insensitively, and never as a substring: `ir` does not\nalso match `ir-2`.\n","schema":{"type":"string"}},{"name":"threat","in":"query","description":"`waf` is any request that scored above zero, `bot` any request\nclassified as a bot, `action` any request a detection rule acted on,\nand `any` the union of the three. An unrecognised value applies no\nfilter at all rather than erroring, so a typo returns everything.\n","schema":{"type":"string","enum":["waf","bot","action","any"]}},{"name":"detectAction","in":"query","description":"The action a detection rule took, matched exactly against the\nlower-cased action name - unless the value contains a `%`, which\nturns it into a SQL LIKE pattern.\n","schema":{"type":"string"}},{"name":"botKind","in":"query","description":"Substring of the classified bot kind, case-sensitive.","schema":{"type":"string"}},{"name":"wafRuleId","in":"query","description":"Substring of the row's fired-rule-id list, not an exact id match, so\n`94` also matches a row that fired `942100`. Pass the whole id.\n","schema":{"type":"string"}},{"name":"headerSearch","in":"query","description":"Case-insensitive substring across the captured client request\nheaders only. The origin request, origin response and client\nresponse headers are not searched.\n","schema":{"type":"string"}},{"name":"uriPatterns","in":"query","description":"Comma-separated URI globs, OR'd against each other while every other\nfilter is AND'd. `*` is the wildcard; a pattern without one matches\nas a prefix, so `/api` means \"starts with /api\" and never\n\"contains /api\".\n","schema":{"type":"string"}},{"name":"recordId","in":"query","description":"Narrow to one proxied DNS record. A value that is not a number is\nignored rather than rejected, so a typo silently widens the result\nback to the whole domain.\n","schema":{"type":"integer"}},{"name":"md","in":"query","description":"Markdown-for-Agents view. `converted` keeps requests whose response\nwas converted, `failed` those where a conversion was attempted and\nfailed, `any` either. Unrecognised values apply no filter.\n","schema":{"type":"string","enum":["converted","failed","any"]}}],"responses":{"200":{"description":"A page of request logs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogsResponse"}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/waf-logs":{"get":{"tags":["Analytics"],"operationId":"analyticsWafLogs","summary":"WAF event logs","description":"Requests the WAF evaluated, with the rules that fired and the score they\nproduced. Entries where `dryRun` is true were logged only — the request\nwas not actually blocked.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"$ref":"#/components/parameters/Period"},{"name":"limit","in":"query","schema":{"type":"integer","default":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"hostname","in":"query","description":"Substring match on hostname.","schema":{"type":"string"}},{"name":"action","in":"query","description":"The action taken.","schema":{"type":"string"}},{"name":"ruleId","in":"query","description":"A CRS rule id that fired.","schema":{"type":"string"}},{"name":"clientIp","in":"query","schema":{"type":"string"}},{"name":"country","in":"query","schema":{"type":"string"}},{"name":"rayId","in":"query","schema":{"type":"string"}},{"name":"blocked","in":"query","description":"Restrict to blocked or non-blocked requests.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"A page of WAF events.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WafLogEntry"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsPlanLimited"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/analytics/markdown-tester":{"get":{"tags":["Analytics"],"operationId":"analyticsMarkdownTester","summary":"Preview Markdown-for-Agents conversion","description":"Fetches one page twice — once normally and once with\n`Accept: text/markdown` — and returns both responses so you can compare\nthem. Ownership is checked but there is no plan gate: you may preview\nthe conversion before enabling `markdown_for_agents` on the domain.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"},{"name":"hostname","in":"query","description":"Which hostname to fetch. Defaults to the domain apex.","schema":{"type":"string"}},{"name":"path","in":"query","description":"Path to fetch.","schema":{"type":"string","default":"/"}}],"responses":{"200":{"description":"Both fetches, side by side.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkdownTesterResult"}}}},"400":{"description":"Missing `domain`, or an invalid hostname or path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/analytics/query":{"post":{"tags":["Analytics"],"operationId":"analyticsQuery","summary":"Run a custom query over your request logs","description":"Runs a read-only SQL `SELECT` against the `requests` table — your raw\nrequest log — for analyses the dedicated endpoints do not cover.\n\n**Scoping is enforced by the database engine**, not by your query: a\nfilter restricting rows to the domains this key can access is appended\nto every read of `requests`. You cannot read another account's traffic,\nhowever the query is written.\n\nRestrictions:\n\n* A single statement only, starting with `SELECT` or `WITH`.\n* Only the `requests` table may be read. Common table expressions you\n  define yourself are fine; other tables and any `db.table` reference\n  are rejected.\n* Writes, DDL and settings changes are rejected.\n* Execution is capped at 30 seconds and 10 000 returned rows —\n  `truncated` tells you when the cap was hit.\n\nUseful `requests` columns: `event_time`, `domain_id`, `hostname`,\n`method`, `uri`, `status`, `bytesIn`, `bytesOut`, `duration` (ms),\n`remoteAddr`, `country`, `asn`, `asnOrg`, `userAgent`, `cacheStatus`,\n`reqStatus`, `isWS`, `protocol`, `referer`, `originStatus`,\n`originAddr`, `error`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sql"],"properties":{"sql":{"type":"string","description":"The query to run.","examples":["SELECT toStartOfHour(event_time) AS h, count() AS c FROM requests WHERE status >= 500 GROUP BY h ORDER BY h"]}}}}}},"responses":{"200":{"description":"Query result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsQueryResult"}}}},"400":{"description":"The query was rejected by validation, or the database refused it.\n`detail` carries the underlying message when the engine rejected it.\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"}}},"examples":{"disallowedTable":{"value":{"error":"querying \"system.parts\" is not allowed; only the 'requests' table may be read"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Read-only key, or the account has no domains whose logs could be\nqueried.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/AnalyticsUnavailable"}}}},"/uptime":{"get":{"tags":["Uptime"],"operationId":"listOutageIncidents","summary":"List outage incidents","description":"The domain's sustained origin-outage incidents, most recent first. An\nincident opens when a subdomain's origin-error rate stays above the\ndomain's threshold for the whole detection window, and resolves after\n`recover_min` clear minutes.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"responses":{"200":{"description":"Incident history.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutageIncident"}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/uptime/live":{"get":{"tags":["Uptime"],"operationId":"getUptimeLive","summary":"Current origin-error status per subdomain","description":"What is happening right now, per subdomain, over the domain's detection\nwindow — including hosts that are erroring but have not (yet) crossed\nthe alert thresholds. Distinct from `/uptime`, which lists only\nsustained outages.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"responses":{"200":{"description":"Live status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UptimeLive"}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/uptime/settings":{"get":{"tags":["Uptime"],"operationId":"getUptimeSettings","summary":"Get outage-detection settings","description":"The domain's detection thresholds, with the valid range for each in\n`bounds`.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"responses":{"200":{"description":"Current settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UptimeSettings"}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Uptime"],"operationId":"updateUptimeSettings","summary":"Update outage-detection settings","description":"Partial update — omitted fields keep their current value.\n\nOut-of-range values are rejected, not clamped. A `threshold_pct` outside\n50–100, a `window_min` outside `bounds.window_min_min`–`window_min_max`,\na `min_requests` below 1, a `min_active_min` outside 1–`window_min`, or\na `recover_min` outside 1–30 fails the whole request with `400`, naming\nthe offending field, and nothing is written. The one value the server\nadjusts silently is `min_active_min`: if the update lowers `window_min`\nbelow it, it is pulled down to match, because it can never exceed the\nwindow.\n\nRequires `analytics.view`, not `domain.settings`. The handler resolves\nthe domain with the same permission the uptime reads use, and every role\nhas `analytics.view` — so a `viewer`, who can change nothing else on the\ndomain, can switch outage alerts off or move the thresholds far enough\nthat no incident ever opens. Treat `viewer` on a domain as \"can silence\noutage alerting\" and grant it accordingly.\n\nA disabled domain does not block this write either. The disabled-domain\nguard only covers configuration permissions and `analytics.view` is not\none of them, so there is no `409` here.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UptimeSettingsUpdate"}}}},"responses":{"200":{"description":"The settings as stored, with the valid range for each in `bounds`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UptimeSettings"}}}},"400":{"description":"The `domain` query parameter is missing, the body is malformed, or a\nvalue is outside its bound — for example\n`threshold_pct must be between 50 and 100`. Nothing is saved.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/recommendations":{"get":{"tags":["Recommendations"],"operationId":"listRecommendations","summary":"Get the domain's advisory checklist","description":"Per-domain advice derived from analytics, configuration and live probes.\nItems with `status: ok` are passing checks; `warn` items suggest an\naction. Dismissed items are still returned, flagged `dismissed: true`.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"responses":{"200":{"description":"Checklist items.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Recommendation"}}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/recommendations/count":{"get":{"tags":["Recommendations"],"operationId":"countRecommendations","summary":"Count outstanding recommendations","description":"How many items need action — excluding dismissed and passing ones.","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"responses":{"200":{"description":"Outstanding count.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/recommendations/dismiss":{"post":{"tags":["Recommendations"],"operationId":"dismissRecommendation","summary":"Dismiss a recommendation","description":"Hides one checklist item for the calling user on this domain. Dismissals\nare per user, not per domain — they do not affect other members.\nRepeating the call is a no-op.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"The recommendation's `key`."}}}}}},"responses":{"200":{"description":"Dismissed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"Missing `domain`, or missing `key` in the body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Recommendations"],"operationId":"undismissRecommendation","summary":"Restore a dismissed recommendation","description":"Deletes your dismissal row for one `key` on this domain, so the item goes\nback to `dismissed: false` in `GET /recommendations` and, when its status is\n`warn`, is counted again by `/recommendations/count`. The `key` travels in a\nJSON body even though this is a `DELETE`, and the domain is still the\n`?domain=` query parameter. Only your own view changes — dismissals are per\nuser and per domain, so dismissing never hid the item from other members.\nThe key is not checked against the live checklist: an unknown key, or one\nyou never dismissed, deletes nothing and still returns `200`.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key"],"properties":{"key":{"type":"string"}}}}}},"responses":{"200":{"description":"Restored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"Missing `domain`, or missing `key` in the body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/cache/stats/":{"get":{"tags":["Cache"],"operationId":"getCacheStats","summary":"Cached entry count and size for a domain","description":"The domain's live cache footprint, summed across every storage node.\nResults are cached briefly, so a purge can take a few seconds to show\nup here. Returns zeroes when the cache layer is not enabled.\n","parameters":[{"$ref":"#/components/parameters/DomainQuery"}],"responses":{"200":{"description":"Cache footprint.","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"integer"},"size_bytes":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/DomainQueryRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/cache/":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"delete":{"tags":["Cache"],"operationId":"purgeDomainCache","summary":"Purge the entire cache for a domain","description":"Removes every cached entry for the domain.\n\nThe sweep runs in the background: a `202` means it was queued and\n`deleted` is not yet known. Requires `cache.edit` and a plan including\ncache purge.\n","responses":{"200":{"description":"Purge completed synchronously — nothing was cached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurgeResult"}}}},"202":{"description":"Purge queued; it runs in the background.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurgeResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Read-only key, insufficient role, or cache purge is not on the plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"The domain is disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/cache/keys":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Cache"],"operationId":"listCacheKeys","summary":"Browse cached entries","description":"A page of the domain's individual cached objects.\n\nNote the two host/path pairs on each row: `host` and `path` are the\nhuman-readable request URL, while `hostname` (the storage namespace) and\n`store_path` are the stored identity you must echo back when purging a\nspecific row. Requires `domain.view`.\n","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"sort","in":"query","description":"Sort column. Anything else falls back to `cached_at`.","schema":{"type":"string","enum":["size","host","hostname","path","expires_at","cached_at"]}},{"name":"dir","in":"query","schema":{"type":"string","enum":["asc","desc"]}},{"name":"hostname","in":"query","description":"Exact match on the storage namespace host.","schema":{"type":"string"}},{"name":"host","in":"query","description":"Match on the request host — substring, or a `*` wildcard.","schema":{"type":"string"}},{"name":"node","in":"query","description":"Edge node that cached the entry. Case-sensitive as stored.","schema":{"type":"string"}},{"name":"path","in":"query","description":"Match on the request path — substring, or a `*` wildcard.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of cached entries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheKeysPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/CacheRegistryUnavailable"}}}},"/domains/{domain}/cache/keys/summary":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Cache"],"operationId":"getCacheKeysSummary","summary":"Cache totals with per-node breakdown","description":"Entry count and byte size for the domain, broken down by the edge node\nthat cached each entry. Accepts the same filters as\n`/domains/{domain}/cache/keys`. Requires `domain.view`.\n","parameters":[{"name":"hostname","in":"query","schema":{"type":"string"}},{"name":"host","in":"query","schema":{"type":"string"}},{"name":"node","in":"query","schema":{"type":"string"}},{"name":"path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Totals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheTotals"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/CacheRegistryUnavailable"}}}},"/domains/{domain}/cache/keys/purge":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"post":{"tags":["Cache"],"operationId":"purgeCacheKeys","summary":"Purge or refresh selected cached entries","description":"Targets specific entries — either by listing them in `entries`, or by\nmatching a `filter`. Supply one or the other.\n\n* `mode: \"delete\"` (default) removes the entry and drops it from the\n  listing.\n* `mode: \"refresh\"` only evicts the stored copy, so the next visitor\n  re-fills it. The row stays and updates itself.\n\n`entries` must carry each row's **stored** identity — copy `hostname`,\n`store_path`, `key_hash` and `node` straight from the listing (note the\nrequest body uses camelCase for these). Entries belonging to another\ndomain are rejected.\n\n`truncated` is `true` when a filter matched more entries than one call\nmay touch — repeat the call until it is `false`. Requires `cache.edit`\nand a plan including cache purge.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CachePurgeKeysRequest"}}}},"responses":{"200":{"description":"Purge result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CachePurgeKeysResult"}}}},"400":{"description":"Malformed body, or neither `entries` nor `filter` supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Read-only key, insufficient role, or cache purge is not on the plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"The domain is disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/CacheRegistryUnavailable"}}}},"/domains/{domain}/members":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Sharing"],"operationId":"listDomainMembers","summary":"List domain members","description":"Everyone with access to the domain, including the owner, plus your own\nrole and whether you may manage membership.\n","responses":{"200":{"description":"Members.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/members/{userId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"name":"userId","in":"path","required":true,"description":"The member's user id, from the member list.","schema":{"type":"integer"}}],"patch":{"tags":["Sharing"],"operationId":"updateDomainMember","summary":"Change a member's role or notifications","description":"Partial update. Changing `role` requires `members.manage`; a member may\nchange their own notification preferences without it.\n\nThe owner's role cannot be changed, and the owner always receives every\nnotification category.\n\nOne member is not editable here at all: the domain's service provider,\nwhich appears in the member list with role `provider`. That row is a\nprojection of the account the provider manages, not a grant someone\nmade, so a role change on it is refused with `409` and a reconciliation\npass would undo it anyway; its notification preferences may be changed\nonly by the provider himself, and anyone else gets `403`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberUpdate"}}}},"responses":{"200":{"description":"The stored membership row — not the enriched shape that\n`GET /domains/{domain}/members` returns. It carries `id`,\n`domain_id`, `user_id`, `role`, the three `notify_*` flags,\n`invited_by`, `created_at` and `updated_at`, and it does NOT carry\n`email`, `name`, `is_owner`, `is_self` or `joined_at`. Re-list the\nmembers if you need those.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Membership"}}}},"400":{"description":"Invalid user id or body, nothing to update, an invalid role, or an\nattempt to change the owner's role or notifications.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The target is the domain's service provider and the body changes\n`role`. Provider access follows the managed account, not a\nmembership, so it cannot be re-roled from here.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Sharing"],"operationId":"removeDomainMember","summary":"Remove a member","description":"Revokes the member's access. The owner cannot be removed. Requires\n`members.manage`.\n","responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"Invalid user id, or an attempt to remove the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/invites":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Sharing"],"operationId":"listDomainInvites","summary":"List pending invitations","description":"Invitations that have not yet been accepted. Invites addressed to\nsomeone who already has access are filtered out. Requires\n`members.manage`.\n","responses":{"200":{"description":"Pending invitations.","content":{"application/json":{"schema":{"type":"object","properties":{"invites":{"type":"array","items":{"$ref":"#/components/schemas/Invite"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Sharing"],"operationId":"createDomainInvite","summary":"Invite someone to the domain","description":"Creates an invitation and emails it.\n\nAn invite is bound to the address it was sent to: forwarding the email\ndoes not let someone else accept it. Requires `members.manage`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteCreate"}}}},"responses":{"200":{"description":"Invitation created, with its accept link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invite"}}}},"400":{"description":"Invalid body, an invalid role, a missing or malformed email, an\nattempt to invite yourself, or an attempt to invite the domain's\nowner.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"409":{"description":"That user is already a member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/invites/{inviteId}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/InviteId"}],"delete":{"tags":["Sharing"],"operationId":"revokeDomainInvite","summary":"Revoke an invitation","description":"Requires `members.manage`.","responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or invitation not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/invites/{inviteId}/resend":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"$ref":"#/components/parameters/InviteId"}],"post":{"tags":["Sharing"],"operationId":"resendDomainInvite","summary":"Resend an invitation","description":"Refreshes the invitation's expiry and emails it again. Share links\ncannot be resent. Requires `members.manage`.\n","responses":{"200":{"description":"Resent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invite"}}}},"400":{"description":"The invite is a share link, or is no longer active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or invitation not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/invites/{token}":{"parameters":[{"$ref":"#/components/parameters/InviteToken"}],"get":{"tags":["Sharing"],"operationId":"getInvite","summary":"Inspect an invitation","description":"What an invitation grants, for the authenticated caller. Use it before\naccepting to show who invited them and to which domain.\n\n`email_match` reports whether the invitation was addressed to the\ncalling account — `POST /invites/{token}/accept` will refuse when it is\nfalse. When it is false, `invited_email` carries the masked target\naddress.\n\nIf the caller already has access, `already_member` is true and the\nremaining fields describe their existing role.\n","responses":{"200":{"description":"Invitation details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitePreview"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The invitation does not exist, has expired, was revoked, or is used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/invites/{token}/accept":{"parameters":[{"$ref":"#/components/parameters/InviteToken"}],"post":{"tags":["Sharing"],"operationId":"acceptInvite","summary":"Accept an invitation","description":"Joins the domain with the role the invitation carries.\n\nThe invitation binds to the address it was sent to, so accepting from a\ndifferent account fails with `403` and `code: \"invite_email_mismatch\"`.\nAccepting when you already have access is a no-op that returns\n`already_member: true`.\n","responses":{"200":{"description":"Accepted, or you already had access.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteAcceptResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Read-only key, or the invitation was sent to a different email\naddress.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteMismatch"}}}},"404":{"description":"The invitation does not exist, has expired, was revoked, or is used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/wallet":{"get":{"tags":["Billing"],"operationId":"getWallet","summary":"Get wallet balance","description":"`negative_since` is set while the balance is below zero. If it stays\nnegative past the grace window, paid domains are suspended; it clears as\nsoon as the balance is non-negative again.\n","responses":{"200":{"description":"Wallet.","content":{"application/json":{"schema":{"type":"object","properties":{"wallet":{"$ref":"#/components/schemas/Wallet"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/wallet/transactions":{"get":{"tags":["Billing"],"operationId":"listWalletTransactions","summary":"List wallet transactions","description":"The wallet ledger, newest first. `amount_rials` is signed: positive is a\ncredit, negative a debit. Traffic charges carry the domain they are\nattributed to.\n","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Ledger rows.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WalletTransaction"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/wallet/transactions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Billing"],"operationId":"getWalletTransaction","summary":"Get one wallet transaction","description":"The ledger row, plus — for a traffic charge — the per-domain byte and\ncost breakdown of that billing window. A traffic charge is billed once\nper account per window, summed across all your domains, so `by_domain`\nis how you attribute it.\n","responses":{"200":{"description":"Transaction detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletTransactionDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such transaction on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/wallet/topup-result":{"get":{"tags":["Billing"],"operationId":"getTopupResult","summary":"Look up a top-up payment result","description":"The outcome of a wallet top-up, by payment gateway authority. Reading is\nallowed; starting a top-up is not available to API keys.\n","parameters":[{"name":"authority","in":"query","required":true,"description":"The payment gateway authority returned when the top-up started.","schema":{"type":"string"}}],"responses":{"200":{"description":"Payment outcome.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"ref_code":{"type":"string"},"amount_rials":{"type":"integer"}}}}}},"400":{"description":"Missing `authority`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such payment on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/wallet/period-statement":{"get":{"tags":["Billing"],"operationId":"getWalletPeriodStatement","summary":"Get the current billing-period statement","description":"A live estimate for the billing period in progress: plan price plus\ntraffic accrued so far, per domain.\n","parameters":[{"name":"subscription_id","in":"query","description":"Which subscription's period to report. Defaults to the current one.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Period statement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeriodStatement"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/wallet/period-statements":{"get":{"tags":["Billing"],"operationId":"listWalletPeriodStatements","summary":"List completed billing-period statements","description":"A period statement is the frozen record of one *closed* billing period of one\nsubscription — the plan price, the period's traffic bytes per tier, the rials\nactually charged for that traffic, and the per-GB rates that were in force —\nwritten once when the period closes at renewal, at a manual extension, or on\nthe drop into grace. It is deliberately not a tax document: no number, no VAT,\n`is_estimate` false and `not_a_tax_invoice` true. The numbered counterpart of\nthe same close is the `period`-kind row in `GET /invoices`, which itemizes only\nthe overage actually debited from the wallet. Rows span every subscription on\nthe account, newest `period_end` first; `limit` defaults to 50 and is clamped\nto 1–200, and there is no offset, so the newest 200 statements are all you can\nreach. `subscription_id` is accepted but never applied — filter on each row's\nown `subscription_id` yourself.\n","parameters":[{"name":"subscription_id","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Completed statements, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PeriodStatement"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/subscriptions":{"get":{"tags":["Billing"],"operationId":"listSubscriptions","summary":"List your subscriptions","description":"Every subscription across your domains.","responses":{"200":{"description":"Subscriptions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Subscription"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/subscriptions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Billing"],"operationId":"getSubscription","summary":"Get one subscription","description":"`{id}` is the numeric subscription id from `GET /subscriptions`, never a\ndomain: subscriptions are per domain, and every purchase, switch and renewal\nleaves its own row, so `expired`, `cancelled` and superseded ones stay\nreadable here indefinitely. The lookup is bound to your own `user_id`, so a\nsubscription on a domain that was merely shared with you is a `404` — read\nthat one through `GET /domains/{domain}/subscription`, which admits shared\nmembers. The payload is an envelope rather than a bare subscription:\n`subscription` (carrying `effective_features`, plus the quota window while it\nis active or in grace), `period_statement`, and `invoices` for this\nsubscription, newest `id` first. `period_statement` is the newest frozen\nstatement of *this* subscription; if it never closed one and is still active\nor in grace, the field falls back to a live estimate built for the domain's\ncurrent subscription, so compare the statement's `subscription_id` with\n`{id}` before attributing it. In every other case it is `null`.\n","responses":{"200":{"description":"Subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such subscription on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/subscriptions/current":{"get":{"tags":["Billing"],"operationId":"getCurrentSubscriptionDeprecated","summary":"Current subscription (removed)","deprecated":true,"description":"**Removed.** Subscriptions are per domain. Always returns `410`; use\n`GET /domains/{domain}/subscription` instead.\n","responses":{"401":{"$ref":"#/components/responses/Unauthorized"},"410":{"description":"Endpoint removed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"subscription_moved_to_domain"},"message":{"type":"string"}}}}}}}}},"/features":{"get":{"tags":["Billing"],"operationId":"getAccountFeatures","summary":"Plan summary per domain","description":"One row per domain, with the plan it is on and when that plan expires.","responses":{"200":{"description":"Per-domain plan summary.","content":{"application/json":{"schema":{"type":"object","properties":{"domains":{"type":"array","items":{"$ref":"#/components/schemas/DomainPlanSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/traffic-usage":{"get":{"tags":["Billing"],"operationId":"getAccountTrafficUsage","summary":"Traffic usage and pricing across your account","description":"Recent daily traffic rows (up to 90) with totals, plus the current\nper-gigabyte prices.\n\nTraffic is billed in three tiers — `cached` (served from cache),\n`proxied` (fetched through the edge proxy) and `direct` — each priced\nseparately. Older rows may carry only the legacy `bypass_bytes` column;\nthose are folded into the direct total.\n","responses":{"200":{"description":"Usage rows, totals and prices.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountTrafficUsage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/invoices":{"get":{"tags":["Billing"],"operationId":"listInvoices","summary":"List invoices","description":"Scoped to your own `user_id`: an invoice is raised against the domain's owner,\nso a domain shared with you contributes none of them here. Ordered by `id`\ndescending — issue order — and paginated with `limit` (default 200, which is\nalso the hard cap) and `offset` (default 0); an unparseable or out-of-range\nvalue falls back to the default instead of erroring. Line `items` come with\nevery row and `domain_name` is resolved, except on top-up invoices, which have\nno domain at all. The kinds you can see are `subscription` (plan purchase or\nrenewal), `period` (traffic overage for a closed billing period), `topup`\n(wallet credit, the one kind exempt from VAT) and `manual` (raised by\nsupport); the internal per-window traffic invoices are filtered out. Status is\n`paid` on everything the system issues — invoices are receipts written after\nthe wallet has already been debited, with `paid_at` stamped — and `cancelled`\nappears only after support cancels one; no current code path issues an\n`unpaid` invoice.\n","responses":{"200":{"description":"Invoices, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/invoices/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Billing"],"operationId":"getInvoice","summary":"Get one invoice","description":"The invoice with its line items.","responses":{"200":{"description":"Invoice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such invoice on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/subscription":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Billing"],"operationId":"getDomainSubscription","summary":"Get the domain's current subscription","description":"The active subscription, its period statement and its invoices. Returns\n`null` when the domain has no subscription. Readable by shared members,\nnot only the owner.\n","responses":{"200":{"description":"Current subscription, or `null`.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"type":"object","properties":{"subscription":{"$ref":"#/components/schemas/Subscription"},"period_statement":{"$ref":"#/components/schemas/PeriodStatement"},"invoices":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}]}}}},"400":{"description":"Missing domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/subscriptions":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Billing"],"operationId":"listDomainSubscriptions","summary":"List the domain's subscription history","description":"Owner only.","responses":{"200":{"description":"Subscriptions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Subscription"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/subscriptions/{id}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Billing"],"operationId":"getDomainSubscriptionById","summary":"Get one of the domain's subscriptions","description":"Owner only.","responses":{"200":{"description":"Subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or subscription not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/features":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Billing"],"operationId":"getDomainFeatures","summary":"Get the domain's effective plan entitlements","description":"What this domain's plan actually allows — the resolved values after any\nper-subscription overrides, so this is the authority on whether a\nfeature is available.\n\nA limit of `null` means unlimited. Use this before calling a gated\nendpoint rather than inferring capability from the plan name. Readable\nby shared members.\n","responses":{"200":{"description":"Effective entitlements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainFeatures"}}}},"400":{"description":"Missing domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/traffic-usage":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Billing"],"operationId":"getDomainTrafficUsage","summary":"Get the domain's traffic usage","description":"Daily traffic rows for this domain, with totals and current prices.\nSame three-tier model as the account-wide endpoint. Readable by shared\nmembers.\n","responses":{"200":{"description":"Usage rows, totals and prices.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountTrafficUsage"}}}},"400":{"description":"Missing domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/invoices":{"parameters":[{"$ref":"#/components/parameters/DomainName"}],"get":{"tags":["Billing"],"operationId":"listDomainInvoices","summary":"List the domain's invoices","description":"Owner only.","responses":{"200":{"description":"Invoices, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/DomainNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domains/{domain}/invoices/{id}":{"parameters":[{"$ref":"#/components/parameters/DomainName"},{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Billing"],"operationId":"getDomainInvoice","summary":"Get one of the domain's invoices","description":"Owner only.","responses":{"200":{"description":"Invoice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Domain or invoice not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/tickets":{"get":{"tags":["Support"],"operationId":"listTickets","summary":"List your support tickets","description":"Your tickets, most recently updated first.","responses":{"200":{"description":"Tickets.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TicketListItem"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Support"],"operationId":"createTicket","summary":"Open a support ticket","description":"Send JSON for a text-only ticket, or `multipart/form-data` to attach\nimages.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subject","message"],"properties":{"subject":{"type":"string"},"message":{"type":"string"}}}},"multipart/form-data":{"schema":{"type":"object","required":["subject","message"],"properties":{"subject":{"type":"string"},"message":{"type":"string"},"files":{"type":"array","description":"Image attachments.","items":{"type":"string","format":"binary"}}}}}}},"responses":{"201":{"description":"The ticket and its opening message, in an envelope. The body is\n`{\"ticket\": …, \"first_message\": …}` — not a bare ticket — and the\nstatus is `201`, not `200`. Both the JSON and the\n`multipart/form-data` branch answer this way. `ticket.messages` is\nempty here; the opening message is `first_message`, and any images\nyou uploaded come back on `first_message.attachments`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketCreated"}}}},"400":{"description":"Invalid body, or a missing/oversized subject or message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/tickets/unread-count":{"get":{"tags":["Support"],"operationId":"getTicketUnreadCount","summary":"Count tickets with unread replies","description":"Counts tickets, not messages: your `open` tickets whose newest message is from\nstaff and whose id is above the last-read message id recorded for you. A\nthread with five unread staff replies therefore counts once, a thread where\nyou replied last never counts, and a `closed` ticket never counts even when\nits final staff reply was never read. The only thing that marks a ticket read\nis `GET /tickets/{id}`, which records the thread's highest message id; there\nis no separate mark-read call, and listing tickets computes `is_unread` per\nrow without marking anything. The scope is the ticket owner's account, so a\nkey sees exactly the tickets its owner opened — nothing here is per domain,\nand support's own unread counter is a different endpoint.\n","responses":{"200":{"description":"Unread count.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/tickets/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Support"],"operationId":"getTicket","summary":"Get a ticket with its messages","description":"Fetching a ticket marks it read for you, so the unread count drops.\n","responses":{"200":{"description":"The ticket, including its message thread.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"400":{"description":"Invalid id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such ticket on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/tickets/{id}/messages":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"post":{"tags":["Support"],"operationId":"addTicketMessage","summary":"Reply to a ticket","description":"Send JSON for a text-only reply, or `multipart/form-data` to attach\nimages.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}},"multipart/form-data":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"},"files":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"200":{"description":"The created message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketMessage"}}}},"400":{"description":"Invalid id, invalid body, or an empty/oversized message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ReadOnlyKey"},"404":{"description":"No such ticket on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/proxy-ip/":{"get":{"tags":["Account"],"operationId":"getProxyIp","summary":"Get the edge proxy IP","description":"The IP address to point DNS at for an externally-hosted zone. For\nmanaged domains NSIN sets this automatically when you mark a record\nproxied.\n","responses":{"200":{"description":"Proxy IP.","content":{"application/json":{"schema":{"type":"object","properties":{"ip":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"`Authorization: Bearer nsin_…`. The token is an NSIN API key, not a JWT.\n"},"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"`X-Api-Key: nsin_…`. Equivalent to the bearer form — use whichever suits\nyour client.\n"}},"parameters":{"DomainName":{"name":"domain","in":"path","required":true,"description":"The domain **name** (for example `example.com`) — not a numeric id.\n","schema":{"type":"string"},"example":"example.com"},"DomainQuery":{"name":"domain","in":"query","required":true,"description":"The domain **name** (for example `example.com`). These endpoints take the\ndomain as a query parameter rather than a path segment.\n","schema":{"type":"string"},"example":"example.com"},"Period":{"name":"period","in":"query","description":"Time window, ending now. Buckets are hourly up to `24h` and daily for\n`7d` and `30d`. An unrecognised value falls back to `24h`.\n","schema":{"type":"string","enum":["3h","6h","12h","24h","7d","30d"],"default":"24h"}},"HostnameFilter":{"name":"hostname","in":"query","description":"Narrow to one subdomain. Matches the exact host, any subdomain of it, or\na bare label — so `example.com` matches `api.example.com`, and `api`\nmatches `api.example.com`, but `exam` matches neither.\n","schema":{"type":"string"}},"PathFilter":{"name":"path","in":"query","description":"Narrow to a URL path prefix.","schema":{"type":"string"}},"NodeFilter":{"name":"node","in":"query","description":"Narrow to one edge node. Use `name` from `GET /analytics/nodes`.","schema":{"type":"string"}},"RuleId":{"name":"ruleId","in":"path","required":true,"description":"Numeric id of the rule.","schema":{"type":"integer"}},"InviteId":{"name":"inviteId","in":"path","required":true,"description":"Numeric id of the invitation.","schema":{"type":"integer"}},"InviteToken":{"name":"token","in":"path","required":true,"description":"The invitation token from the accept link.","schema":{"type":"string"}},"RecordId":{"name":"recordId","in":"path","required":true,"description":"Numeric id of the DNS record.","schema":{"type":"integer"}}},"responses":{"Unauthorized":{"description":"Missing, malformed, revoked or expired API key, or a key whose owning\nuser row is gone. A key whose owning account has merely been deactivated\nis not this: that is `403` with `code: account_suspended`, because the\ncredential itself is intact and re-issuing it changes nothing.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalidKey":{"value":{"error":"invalid API key"}}}}}},"Forbidden":{"description":"The key is read-only, your role on the domain lacks the required\npermission, the domain's plan does not include the feature, or the\naccount that owns the key has been deactivated — that last one carries\n`code: account_suspended` and can arrive on any endpoint, including ones\nthat document no `403` of their own.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ReadOnlyKey":{"description":"The key is read-only and this endpoint is a write. Read-only keys may\nonly issue `GET`, `HEAD` and `OPTIONS`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"readOnly":{"value":{"error":"read-only API key"}}}}}},"DomainNotFound":{"description":"No such domain, or it is not visible to this account. Domains you cannot\naccess are reported as not found rather than forbidden.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RuleDomainNotFound":{"description":"No such domain, or you hold no role on it at all.\n\nIt no longer means \"your role is too low\". The rules endpoints resolve\naccess through the shared domain guard, which answers `403` when the\ncaller can see the domain but lacks `rules.edit`, and `409` with\n`code: domain_disabled` when the domain is switched off and the request\nis a write. Reading is open to every role, so a `GET` can only fail here.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notFound":{"value":{"error":"domain not found"}}}}}},"RuleNotFound":{"description":"The rule does not exist, or it belongs to another domain or another rule\ntype — a rule id is always checked against both the domain in the path\nand the type of the endpoint. It also covers a domain that does not\nexist or that you hold no role on.\n\nIt no longer means \"your role is too low\". A caller who may read the\ndomain but not write its rules is refused before the rule is even looked\nup, with `403`; a write against a disabled domain is refused with `409`\nand `code: domain_disabled`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RuleInvalid":{"description":"Malformed body, an invalid field value, or `record_ids` containing a\nrecord that does not belong to this domain.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"foreignRecords":{"value":{"error":"record_ids do not belong to this domain"}}}}}},"RulePlanLimited":{"description":"The key is read-only, your role on the domain lacks `rules.edit` (a\n`viewer` has `domain.view` and nothing more), or the domain's plan does\nnot include this rule type or allows fewer rules of it than you already\nhave.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"DomainQueryRequired":{"description":"The `domain` query parameter is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"value":{"error":"domain is required"}}}}}},"AnalyticsPlanLimited":{"description":"The domain's plan does not include the feature this endpoint needs\n(`monitoring` for most sections, `logs` for raw and top-N request data).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AnalyticsUnavailable":{"description":"The process holds no ClickHouse client at all - the connection was never\nopened at startup. It is built once and never rebuilt, so this state\nlasts until the backend restarts.\n\nIt is NOT what you get when the store is merely down, and that is the\ntrap. Startup opens the client and pings it as a separate step; a failed\nping is logged and the client is kept, so a ClickHouse that was\nunreachable at boot still leaves a usable handle behind and this 503\nstays rare. Every failure at query time answers **500** instead - the\nstore unreachable, or a query outrunning its timeout of 10s for windows\nup to `24h`, 20s for `7d`, 45s for `30d` - carrying either\n`{\"error\": \"query failed\"}` or a \"Could not load analytics right now\"\nmessage.\n\nSo retry on 500 exactly as you retry on 503. On the analytics endpoints\na 500 nearly always means the store could not answer, not that your\nrequest was wrong.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unavailable":{"value":{"error":"analytics unavailable"}}}}}},"CacheRegistryUnavailable":{"description":"The cache registry is temporarily unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"The key exceeded its request budget (300 requests per minute by default).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"limited":{"value":{"error":"rate limit exceeded"}}}}}}},"schemas":{"Error":{"type":"object","description":"The error shape used by every endpoint. `error` is always present. `code`\nis present only on the failures that have one — do not require it, and do\nnot parse `error` to recover it.\n","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable description of what went wrong."},"code":{"type":"string","description":"Stable machine-readable reason. Present on some failures only; the\nwording of `error` may change, this will not.\n\n* `account_suspended` — `403`. The account behind the credential has\n  been switched off, by an admin or by its provider. Every\n  authenticated route answers this, so treat it as terminal rather\n  than retrying.\n* `domain_disabled` — `409`, not `403`. You have every right to the\n  operation; the domain is simply switched off and is not being\n  served, so its configuration cannot change. It stays readable, and\n  writes work again once it is enabled.\n* `managed_by_reseller` — `403`. The account is a reseller's client\n  and this surface belongs to its provider. See *If your account is\n  managed by a reseller*.\n* `invite_email_mismatch` — `403` from `POST /invites/{token}/accept`.\n  The invitation was addressed to a different email; the body also\n  carries `invited_email`, masked.\n\nA panel session — not an API key — can additionally see\n`session_check_failed` on a `503`, which means the session could not\nbe verified, not that it is invalid. Retry it; do not discard the\ntoken.\n","examples":["account_suspended","domain_disabled","managed_by_reseller","invite_email_mismatch"]}},"examples":[{"error":"read-only API key"},{"error":"account not active","code":"account_suspended"}]},"Message":{"type":"object","properties":{"message":{"type":"string"}},"examples":[{"message":"deleted"}]},"Role":{"type":"string","description":"Your role on a domain. `owner` is implicit for the domain's creator and\nfor global admins; the other three are grantable via sharing.\n","enum":["owner","admin","editor","viewer"]},"Permission":{"type":"string","description":"One capability on a domain.","enum":["domain.view","domain.settings","domain.delete","records.edit","rules.edit","cache.edit","ssl.manage","analytics.view","billing","members.manage"]},"DomainStatus":{"type":"string","description":"* `pending` — managed domain waiting for its nameservers to point at NSIN.\n* `unverified` — external-DNS domain waiting for its TXT verification record.\n* `active` — serving.\n* `moved` — delegation has left NSIN; the domain keeps serving during a grace window.\n* `disabled` — not serving; re-enable with `POST /domains/{domain}/enable`.\n* `banned` — administratively blocked.\n","enum":["pending","unverified","active","moved","disabled","banned"]},"Domain":{"type":"object","description":"A domain and its edge configuration.","properties":{"id":{"type":"integer"},"name":{"type":"string","examples":["example.com"]},"status":{"$ref":"#/components/schemas/DomainStatus"},"dns_mode":{"type":"string","enum":["managed","external"],"description":"`managed` — NSIN hosts the DNS zone. `external` — you host DNS\nelsewhere and prove ownership with a TXT record.\n"},"user_id":{"type":"integer","description":"Id of the owning user."},"verification_started_at":{"type":"string","format":"date-time"},"cache_l2_max_gb":{"type":"integer","description":"Per-domain cap on disk (L2) cache size, in GB."},"cache_l2_ttl_days":{"type":"integer","minimum":1,"maximum":7,"description":"How long a disk-cache entry may live, in days. Maximum 7."},"cache_cap_mb":{"type":"integer","enum":[128,256,512,2048,4096],"description":"Largest response body NSIN will buffer and cache, in MB. Bigger\nresponses stream straight from origin and are never cached. The\nselectable ceiling depends on the domain's plan.\n"},"developer_mode_until":{"type":"string","format":"date-time","description":"While set and in the future, the edge bypasses cache reads and writes\nfor this domain. Absent when developer mode is off.\n"},"pending_since":{"type":"string","format":"date-time"},"moved_since":{"type":"string","format":"date-time"},"next_check_at":{"type":"string","format":"date-time","description":"When the background nameserver checker will next look at this domain."},"last_manual_ns_check_at":{"type":"string","format":"date-time","description":"Last user-triggered nameserver check; these are limited to one per hour."},"sec_no_sniff":{"type":"boolean","description":"Send `X-Content-Type-Options: nosniff`. Off by default — it can break\nan origin that mislabels asset MIME types.\n"},"sec_referrer_policy":{"type":"boolean","description":"Send `Referrer-Policy: strict-origin-when-cross-origin`."},"sec_strip_headers":{"type":"boolean","description":"Strip origin fingerprint headers from responses."},"markdown_for_agents":{"type":"boolean","description":"Serve a Markdown rendering of eligible HTML pages to clients sending\n`Accept: text/markdown`. Requires an active plan.\n"},"outage_alerts":{"type":"boolean","description":"Notify the owner when a subdomain suffers a sustained origin outage."},"uptime_threshold_pct":{"type":"integer","minimum":50,"maximum":100,"description":"Per-minute origin-error percentage that counts as \"down\"."},"uptime_window_min":{"type":"integer","minimum":2,"maximum":60,"description":"Minutes the domain must stay down before an incident opens."},"uptime_min_requests":{"type":"integer","description":"Minimum origin-eligible requests in the window — the traffic floor below which no incident opens."},"uptime_min_active_min":{"type":"integer","description":"Minimum populated one-minute buckets required in the window."},"uptime_recover_min":{"type":"integer","description":"Consecutive clear minutes before an incident resolves."},"suspended":{"type":"boolean","description":"Paused for billing. The edge refuses the domain's TLS handshake, so\nvisitors get a connection error. Clears automatically once the\nwallet is no longer negative.\n"},"suspended_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"DomainVerification":{"type":"object","description":"The TXT record to publish to prove ownership of an external-DNS domain.","properties":{"host":{"type":"string","description":"Name to create the TXT record at."},"type":{"type":"string","const":"TXT"},"value":{"type":"string","description":"Exact TXT value to publish."},"verified":{"type":"boolean"},"expires_at":{"type":"string","format":"date-time"},"seconds_remaining":{"type":"integer"}}},"DomainDetail":{"allOf":[{"$ref":"#/components/schemas/Domain"},{"type":"object","properties":{"verification":{"$ref":"#/components/schemas/DomainVerification"},"nsin_ns":{"type":"array","description":"The canonical (first) accepted nameserver set.","items":{"type":"string"}},"nsin_ns_sets":{"type":"array","description":"Every accepted nameserver set. The delegation must match exactly\none set in full — sets cannot be mixed.\n","items":{"type":"array","items":{"type":"string"}}},"current_ns":{"type":"array","description":"The nameservers currently observed in the parent zone.","items":{"type":"string"}},"my_role":{"$ref":"#/components/schemas/Role"},"my_permissions":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}},"ns_check_interval_seconds":{"type":"integer","description":"How often the background checker re-checks the delegation."}}}]},"DomainSslSummary":{"type":"object","properties":{"status":{"type":"string","examples":["active","pending","failed","missing"]},"expires_at":{"type":"string","format":"date-time"},"days_remaining":{"type":"integer"}}},"DomainWithSsl":{"allOf":[{"$ref":"#/components/schemas/Domain"},{"type":"object","properties":{"ssl":{"$ref":"#/components/schemas/DomainSslSummary"},"subscription":{"type":"object","additionalProperties":true,"description":"The domain's active subscription, when it has one."},"verification":{"$ref":"#/components/schemas/DomainVerification"},"my_role":{"$ref":"#/components/schemas/Role"}}}]},"DomainCreate":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"The domain to add, without scheme or trailing dot.","examples":["example.com"]},"dns_mode":{"type":"string","enum":["managed","external"],"default":"managed"}}},"DomainUpdate":{"type":"object","description":"Every field is optional; omitted fields are left unchanged.\n","properties":{"dns_mode":{"type":"string","enum":["managed","external"]},"cache_l2_max_gb":{"type":"integer","minimum":1},"cache_l2_ttl_days":{"type":"integer","minimum":1,"maximum":7},"cache_cap_mb":{"type":"integer","enum":[128,256,512,2048,4096]},"sec_no_sniff":{"type":"boolean"},"sec_referrer_policy":{"type":"boolean"},"sec_strip_headers":{"type":"boolean"},"markdown_for_agents":{"type":"boolean"}}},"DeveloperMode":{"type":"object","properties":{"active":{"type":"boolean"},"expires_at":{"type":"string","format":"date-time","description":"When developer mode switches itself off. Absent when inactive."}}},"NsCheckResult":{"type":"object","properties":{"ok":{"type":"boolean","description":"Whether the delegation matched an accepted set."},"status":{"$ref":"#/components/schemas/DomainStatus"},"message":{"type":"string"},"next_check_at":{"type":"string","format":"date-time"},"nsin_ns":{"type":"array","items":{"type":"string"},"description":"The nameservers the delegation is expected to match."},"current_ns":{"type":"array","items":{"type":"string"},"description":"The nameservers actually observed."}}},"VerifyResult":{"type":"object","properties":{"verified":{"type":"boolean","const":true},"domain":{"$ref":"#/components/schemas/DomainDetail"}}},"SslCoverageGap":{"type":"object","description":"A proxied hostname that the domain's certificate does not cover yet.","properties":{"hostname":{"type":"string"},"status":{"type":"string","enum":["pending","failed","missing"]},"failure_count":{"type":"integer"},"max_retries":{"type":"integer"},"next_retry_at":{"type":"string","format":"date-time"},"last_error":{"type":"string","description":"The certificate authority's own reason for the last failure."}}},"SslInfo":{"type":"object","properties":{"status":{"type":"string","examples":["active","pending","failed","missing"]},"expires_at":{"type":"string","format":"date-time"},"issued_at":{"type":"string","format":"date-time"},"days_remaining":{"type":"integer"},"issuer":{"type":"string"},"subject":{"type":"string"},"serial_number":{"type":"string"},"sans":{"type":"array","items":{"type":"string"}},"signature_algorithm":{"type":"string"},"key_size":{"type":"integer"},"is_wildcard":{"type":"boolean"},"auto_renewal":{"type":"boolean"},"has_private_key":{"type":"boolean"},"can_manual_issue":{"type":"boolean","description":"Whether `POST /domains/{domain}/ssl/issue` would be accepted right now."},"manual_issue_reason":{"type":"string","description":"Why manual issuance is unavailable, when `can_manual_issue` is false."},"next_manual_issue_at":{"type":"string","format":"date-time"},"last_issue_attempt_at":{"type":"string","format":"date-time"},"coverage":{"type":"array","description":"Proxied hostnames not yet on the certificate. Absent when coverage is\ncomplete.\n","items":{"$ref":"#/components/schemas/SslCoverageGap"}}}},"CustomCertificateUpload":{"type":"object","required":["certificate","private_key"],"properties":{"certificate":{"type":"string","description":"PEM-encoded certificate chain. Include intermediates — a leaf-only\nbundle makes clients fail chain verification.\n"},"private_key":{"type":"string","description":"PEM-encoded private key matching the certificate."},"hostnames":{"type":"array","items":{"type":"string"},"description":"Which of the certificate's SANs this upload should cover. Use the\n`eligible` list from `POST /domains/{domain}/ssl/parse`.\n"}}},"CustomCertificateResult":{"type":"object","properties":{"message":{"type":"string"},"domain":{"type":"string"},"hostnames":{"type":"array","items":{"type":"string"}},"expires_at":{"type":"string","format":"date-time"},"issued_at":{"type":"string","format":"date-time"},"subject":{"type":"string"},"sans":{"type":"array","items":{"type":"string"}}}},"ParsedCertificate":{"type":"object","properties":{"subject":{"type":"string"},"issuer":{"type":"string"},"sans":{"type":"array","items":{"type":"string"},"description":"Every SAN on the certificate."},"eligible":{"type":"array","items":{"type":"string"},"description":"The SANs that belong to this domain and may be passed as `hostnames` on upload."},"default_selection":{"type":"array","items":{"type":"string"},"description":"The subset of `eligible` covering the apex and its wildcard."},"expires_at":{"type":"string","format":"date-time"},"issued_at":{"type":"string","format":"date-time"}}},"RecordType":{"type":"string","enum":["A","AAAA","CNAME","ANAME","NS","TXT","MX","SRV","PTR","CAA","TLSA","SSHFP","URI"]},"RecordScheme":{"type":"string","description":"Protocol the edge uses to reach the origin for a proxied record.\n`Default` follows the request's own scheme; `Auto` probes.\n","enum":["Http","Https","Auto","Default"]},"Record":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string","description":"Record name relative to the domain. `@` is the apex.","examples":["www","@"]},"original_name":{"type":"string","description":"The fully-qualified name, with trailing dot.","examples":["www.example.com."]},"type":{"$ref":"#/components/schemas/RecordType"},"destination":{"type":"string","description":"The record's value. For a proxied record this is the **origin** the\nedge connects to, and the published DNS answer is the NSIN proxy IP\ninstead — see `dns_content`.\n"},"dns_content":{"type":"string","description":"What is actually published in DNS. Equals the proxy IP for proxied records."},"ttl":{"type":"integer","description":"TTL in seconds."},"proxied":{"type":"boolean","description":"Route this hostname through the NSIN edge. Only `A`, `AAAA`, `CNAME`\nand `ANAME` may be proxied.\n"},"captcha":{"type":"boolean","description":"Challenge visitors before passing them to the origin."},"editable":{"type":"boolean","description":"False for records NSIN manages on your behalf."},"user_id":{"type":"integer"},"domain_id":{"type":"integer"},"scheme":{"$ref":"#/components/schemas/RecordScheme"},"port":{"type":"integer","description":"Origin port for proxied records. Default: 443."},"host_header":{"type":"string","description":"Overrides the Host header (and SNI) sent to the origin."},"monitor":{"type":"boolean","description":"Include this record in uptime monitoring."},"dest_country":{"type":"string","description":"ISO country code of the destination, detected by NSIN."},"timeout":{"type":"integer","description":"Upstream timeout in seconds. Default: 15."},"mx_priority":{"type":"integer","minimum":0,"maximum":65535,"description":"Only meaningful for `MX`."},"comment":{"type":"string","maxLength":1024,"description":"Free-form note."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"OriginRuleTag":{"type":"object","description":"An enabled origin rule that overrides where a proxied record's traffic\ngoes, meaning the effective origin is not the record's `destination`.\n","properties":{"rule_id":{"type":"integer"},"type":{"type":"string","enum":["origin_route","origin_pool"]},"zone_wide":{"type":"boolean","description":"True when the rule applies to every proxied record of the domain."},"dry_run":{"type":"boolean","description":"The rule is evaluated but not enforced."}}},"RecordWithOriginRules":{"allOf":[{"$ref":"#/components/schemas/Record"},{"type":"object","properties":{"origin_rules":{"type":"array","description":"Absent when nothing overrides this record's origin. Routes are listed before pools.","items":{"$ref":"#/components/schemas/OriginRuleTag"}}}}]},"RecordCreate":{"type":"object","required":["name","type","destination"],"properties":{"name":{"type":"string","description":"Name relative to the domain. Use `@` for the apex."},"type":{"$ref":"#/components/schemas/RecordType"},"destination":{"type":"string","description":"IP address, hostname or text content."},"mx_priority":{"type":"integer","minimum":0,"maximum":65535},"proxied":{"type":"boolean","default":false},"captcha":{"type":"boolean","default":false},"scheme":{"$ref":"#/components/schemas/RecordScheme"},"port":{"type":"integer","default":443},"host_header":{"type":"string"},"monitor":{"type":"boolean"},"dest_country":{"type":"string"},"timeout":{"type":"integer","default":15},"comment":{"type":"string","maxLength":1024}}},"RecordUpdate":{"type":"object","description":"Every field is optional; omitted fields keep their current value.","properties":{"name":{"type":"string"},"destination":{"type":"string"},"mx_priority":{"type":"integer","minimum":0,"maximum":65535},"proxied":{"type":"boolean"},"captcha":{"type":"boolean"},"scheme":{"$ref":"#/components/schemas/RecordScheme"},"port":{"type":"integer"},"host_header":{"type":"string"},"monitor":{"type":"boolean"},"timeout":{"type":"integer"},"comment":{"type":"string","maxLength":1024}}},"BatchItemResult":{"type":"object","properties":{"id":{"type":"integer"},"ok":{"type":"boolean"},"error":{"type":"string","description":"Present only when `ok` is false."}}},"BatchResult":{"type":"object","description":"Outcome of a best-effort bulk operation. The status code is `200` even\nwhen some records failed — inspect `results`.\n","properties":{"succeeded":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/BatchItemResult"}}}},"ImportPreviewRecord":{"type":"object","properties":{"name":{"type":"string"},"type":{"$ref":"#/components/schemas/RecordType"},"destination":{"type":"string"},"ttl":{"type":"integer"},"mx_priority":{"type":"integer"},"proxied":{"type":"boolean"},"status":{"type":"string","enum":["new","overwrite","unsupported"],"description":"`overwrite` means an NSIN record with the same name and type already\nexists and would be replaced.\n"},"existing_id":{"type":"integer","description":"Set when `status` is `overwrite`."},"reason":{"type":"string","description":"Why an entry is `unsupported`."}}},"ImportRecordItem":{"type":"object","required":["name","type","destination"],"properties":{"name":{"type":"string"},"type":{"$ref":"#/components/schemas/RecordType"},"destination":{"type":"string"},"ttl":{"type":"integer"},"mx_priority":{"type":"integer"},"proxied":{"type":"boolean"}}},"ImportResult":{"type":"object","properties":{"created":{"type":"integer"},"failed":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"error":{"type":"string"}}}},"records":{"type":"array","description":"The domain's full record list after the import.","items":{"$ref":"#/components/schemas/Record"}}}},"RuleType":{"type":"string","enum":["cache","drop","redirect","rewrite","waf","captcha","rate_limit","bot_route","origin_pool","origin_route","fingerprint","error_page"]},"HostMatchType":{"type":"string","description":"How `host_pattern` is matched. The empty string means \"no host filter\",\nand is the only valid value when `host_pattern` is empty — the two\nfields are set and cleared together.\n","enum":["","exact","wildcard","regex"]},"ActionMode":{"type":"string","description":"* `enforce` — the rule acts (block, redirect, challenge, …).\n* `dry_run` — the rule matches and is logged as \"would have acted\", but\n  the request reaches the origin unchanged. Use it to test a rule\n  safely.\n\nNot every rule type honours this; cache ignores it.\n","enum":["enforce","dry_run"]},"RulePathMatchType":{"type":"string","description":"How `path_includes` and `path_excludes` are interpreted.","enum":["wildcard","regex"]},"RuleCommon":{"type":"object","description":"The fields every rule carries, whatever its type.","properties":{"id":{"type":"integer"},"domain_id":{"type":"integer"},"record_id":{"type":"integer","description":"Deprecated single-record scope. Prefer `record_ids`. Absent for\nzone-wide rules.\n"},"record_ids":{"type":"array","items":{"type":"integer"},"description":"The proxied DNS records this rule applies to. Empty or absent means\nzone-wide — every proxied record of the domain.\n"},"type":{"$ref":"#/components/schemas/RuleType"},"enabled":{"type":"boolean"},"priority":{"type":"integer","description":"Evaluation order; lower runs first. Defaults to 100."},"host_pattern":{"type":"string","description":"Optional hostname filter. Empty means the rule is not host-scoped."},"host_match_type":{"$ref":"#/components/schemas/HostMatchType"},"action_mode":{"$ref":"#/components/schemas/ActionMode"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"RuleCommonBody":{"type":"object","description":"The shared rule fields accepted by every create and update body. All are\noptional — on create they fall back to defaults, on update an omitted\nfield is left unchanged.\n","properties":{"record_id":{"type":["integer","null"],"description":"Deprecated single-record scope. Prefer `record_ids`."},"record_ids":{"type":"array","items":{"type":"integer"},"description":"Scope the rule to these proxied records. Omit or send an empty array\nfor a zone-wide rule. Every id must belong to this domain.\n"},"enabled":{"type":"boolean","default":true},"priority":{"type":"integer","default":100,"minimum":0},"host_pattern":{"type":"string"},"host_match_type":{"$ref":"#/components/schemas/HostMatchType"},"action_mode":{"$ref":"#/components/schemas/ActionMode"}}},"RulePathScope":{"type":"object","description":"Path matching shared by the rule types that filter on the URL path.","properties":{"path_match_type":{"$ref":"#/components/schemas/RulePathMatchType"},"path_includes":{"type":"array","items":{"type":"string"},"description":"Paths the rule applies to. Defaults to `[\"/*\"]` — everything."},"path_excludes":{"type":"array","items":{"type":"string"},"description":"Paths carved back out of `path_includes`."}}},"RuleReorderRequest":{"type":"array","description":"A bare array — not wrapped in an object.","items":{"type":"object","required":["id","priority"],"properties":{"id":{"type":"integer"},"priority":{"type":"integer","minimum":0}}}},"RuleReorderResult":{"type":"object","properties":{"updated":{"type":"integer","description":"How many rules were changed."}}},"CacheScope":{"type":"string","description":"What the rule caches among the paths it already matches.\n\n* `default` — static assets only, chosen by file extension.\n* `everything` — every cacheable response, HTML included.\n\nThere is no \"custom\" scope: narrow what you cache by scoping\n`path_includes` instead.\n","enum":["default","everything"]},"CacheRuleFields":{"type":"object","properties":{"ttl_sec":{"type":"integer","description":"How long an entry stays fresh, in seconds. `0` uses the default."},"refresh_sec":{"type":"integer","description":"Background refresh interval in seconds — the entry is re-fetched\nthis often while still being served. `0` disables it.\n"},"with_qs":{"type":"boolean","description":"Include the query string in the cache key. Off means `?a=1` and `?a=2` share one entry."},"scope":{"$ref":"#/components/schemas/CacheScope"},"bypass_authorization":{"type":"boolean","default":true,"description":"Skip caching requests that carry an `Authorization` header. Leave on\nunless you are certain the response is not user-specific.\n"},"bypass_set_cookie":{"type":"boolean","default":true,"description":"Skip caching responses that set a cookie. Turning this off can serve\none visitor's session to another — only do it for responses you know\nare anonymous.\n"},"respect_client_no_store":{"type":"boolean","default":true,"description":"Honour `Cache-Control: no-store` from the client."},"respect_origin_cache_control":{"type":"boolean","default":true,"description":"Honour the origin's `Cache-Control` directives."},"respect_origin_max_age":{"type":"boolean","default":true,"description":"Use the origin's `max-age` instead of `ttl_sec`."},"bypass_wp_admin":{"type":"boolean","default":true,"description":"Never cache WordPress admin and login paths."}}},"CacheRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/CacheRuleFields"}]},"CacheRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/CacheRuleFields"}]},"DropRuleFields":{"type":"object","properties":{"country_match_type":{"type":"string","enum":["include","exclude"],"description":"Whether `countries` is the set that IS dropped (`include`) or the\nonly set that is NOT dropped (`exclude`).\n"},"countries":{"type":"array","items":{"type":"string"},"description":"ISO 3166-1 alpha-2 country codes. Empty means no country filter."}}},"DropRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/DropRuleFields"}]},"DropRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/DropRuleFields"}]},"RedirectRuleFields":{"type":"object","properties":{"target":{"type":"string","description":"Where to send the visitor. Absolute URL, or a path when redirecting within the site."},"status_code":{"type":"integer","enum":[301,302,307,308],"default":302,"description":"The redirect status. `301`/`308` are permanent and cached hard by\nbrowsers — verify the rule with `302` first.\n"},"preserve_query":{"type":"boolean","default":true,"description":"Append the original query string to `target`."},"preserve_path":{"type":"boolean","description":"Append the original path to `target`."}}},"RedirectRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/RedirectRuleFields"}]},"RedirectRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/RedirectRuleFields"}]},"RewriteRuleFields":{"type":"object","properties":{"path_target":{"type":"string","description":"The path sent to the origin. With `path_match_type: regex` you may\nreference capture groups from `path_includes`.\n"},"query_mode":{"type":"string","enum":["preserve","replace","strip"],"description":"* `preserve` — pass the original query string through.\n* `replace` — substitute `query_target`.\n* `strip` — drop the query string entirely.\n"},"query_target":{"type":"string","description":"The replacement query string, used when `query_mode` is `replace`."}}},"RewriteRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/RewriteRuleFields"}]},"RewriteRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/RewriteRuleFields"}]},"WafRuleFields":{"type":"object","properties":{"paranoia":{"type":"integer","minimum":1,"maximum":4,"default":1,"description":"OWASP CRS paranoia level. Higher catches more attacks and produces\nmore false positives — raise it in `dry_run` first.\n"},"threshold":{"type":"integer","minimum":1,"maximum":100,"default":5,"description":"Anomaly score at which a request is blocked."},"body_cap_kb":{"type":"integer","minimum":0,"maximum":1024,"default":128,"description":"How much request body to inspect, in KB. `0` skips body inspection."},"rule_excludes":{"type":"array","items":{"type":"string"},"description":"CRS rule ids to disable, for tuning out false positives."}}},"WafRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/WafRuleFields"}]},"WafRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/WafRuleFields"}]},"CaptchaRuleFields":{"type":"object","properties":{"ttl_sec":{"type":"integer","description":"How long a solved challenge is remembered for that visitor, in seconds."}}},"CaptchaRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/CaptchaRuleFields"}]},"CaptchaRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/CaptchaRuleFields"}]},"RateLimitRuleFields":{"type":"object","properties":{"limit":{"type":"integer","description":"Requests allowed per `window_sec` for one key."},"window_sec":{"type":"integer","default":60,"description":"Length of the counting window, in seconds."},"key_by":{"type":"string","enum":["ip","ip_path"],"default":"ip","description":"How requests are bucketed. `ip` counts everything from one address\ntogether; `ip_path` counts each path separately per address.\n"},"on_breach":{"type":"string","enum":["drop","captcha"],"default":"drop","description":"What happens to requests above the limit."},"burst":{"type":"integer","description":"Extra requests tolerated momentarily above `limit`."}}},"RateLimitRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/RateLimitRuleFields"}]},"RateLimitRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/RateLimitRuleFields"}]},"BotKind":{"type":"string","description":"A bot the edge classifier recognises. `*` matches any of them and is\naccepted in `bot_kinds` even though it is not itself a catalogue entry;\n`generic-bot` is the catch-all user-agent heuristic.\n","enum":["*","gptbot","oai-searchbot","chatgpt-user","claudebot","claude-user","perplexitybot","perplexity-user","googlebot","google-extended","bingbot","ccbot","bytespider","meta-externalagent","amazonbot","applebot","duckduckbot","yandexbot","ahrefsbot","semrushbot","mj12bot","generic-bot"]},"BotRouteRuleFields":{"type":"object","properties":{"bot_kinds":{"type":"array","items":{"$ref":"#/components/schemas/BotKind"},"description":"Which bots this rule matches. Must not be empty."},"require_verified":{"type":"boolean","description":"Only match bots whose identity was verified (by reverse DNS or\npublished IP ranges), not merely self-declared in the user agent.\n"},"action":{"type":"string","enum":["block","alt_content","alt_origin","tag"],"description":"* `block` — refuse the request.\n* `alt_content` — serve `body` with `status` instead of the origin.\n* `alt_origin` — proxy to `alt_dest`:`alt_port` over `alt_scheme`.\n* `tag` — let it through, but tag it in telemetry.\n"},"status":{"type":"integer","default":200,"description":"Status code for `alt_content`."},"body":{"type":"string","description":"Response body for `alt_content`."},"alt_dest":{"type":"string","description":"Origin address for `alt_origin`."},"alt_port":{"type":"integer","description":"Origin port for `alt_origin`."},"alt_scheme":{"type":"string","enum":["http","https"],"description":"Scheme used to reach `alt_dest`."}}},"BotRouteRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/BotRouteRuleFields"}]},"BotRouteRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/BotRouteRuleFields"}]},"OriginEntry":{"type":"object","description":"One origin in a pool.","required":["address"],"properties":{"address":{"type":"string","description":"Origin IP address or hostname."},"port":{"type":"integer"},"scheme":{"type":"string","enum":["http","https"]},"weight":{"type":"integer","description":"Relative share of traffic under `round_robin` and `least_load`."},"node_ids":{"type":"array","items":{"type":"integer"},"description":"Under `lb_type: geo`, the edge nodes that use this origin."},"country":{"type":"string","description":"ISO country code of this origin."}}},"OriginPoolHealthCheck":{"type":"object","properties":{"enabled":{"type":"boolean"},"path":{"type":"string","default":"/","description":"Probe path."},"interval_sec":{"type":"integer","default":15,"description":"Seconds between active probes."},"timeout_sec":{"type":"integer","default":5,"description":"Probe timeout in seconds."},"unhealthy_threshold":{"type":"integer","default":3,"description":"Consecutive probe failures before an origin is marked down."},"healthy_threshold":{"type":"integer","default":2,"description":"Consecutive probe successes before an origin returns to service."},"eject_sec":{"type":"integer","default":30,"description":"How long a passively ejected origin stays out, in seconds."},"host":{"type":"string","description":"Host header override for the probe."}}},"OriginPoolRuleFields":{"type":"object","properties":{"lb_type":{"type":"string","enum":["round_robin","least_load","geo"],"description":"How traffic is spread across `origins`. `geo` routes by edge node —\nsee `node_ids` on each origin.\n"},"origins":{"type":"array","items":{"$ref":"#/components/schemas/OriginEntry"}},"health_check":{"$ref":"#/components/schemas/OriginPoolHealthCheck"},"host_header":{"type":"string","description":"Host header (and SNI) sent to the pool's origins."}}},"OriginPoolRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/OriginPoolRuleFields"}]},"OriginPoolRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/OriginPoolRuleFields"}]},"OriginRouteRuleFields":{"type":"object","properties":{"address":{"type":"string","description":"Origin IP address or hostname for the matched paths."},"port":{"type":"integer"},"scheme":{"type":"string","enum":["http","https"]},"host_header":{"type":"string","description":"Host header (and SNI) sent to this origin."},"country":{"type":"string","description":"ISO country code of the origin, detected by NSIN."}}},"OriginRouteRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/OriginRouteRuleFields"}]},"OriginRouteRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"$ref":"#/components/schemas/OriginRouteRuleFields"}]},"FingerprintRuleFields":{"type":"object","properties":{"match_ja4":{"type":"array","items":{"type":"string"},"description":"JA4 TLS fingerprints to match."},"match_ja4h":{"type":"array","items":{"type":"string"},"description":"JA4H HTTP fingerprints to match."},"action":{"type":"string","enum":["drop","captcha","tag"],"default":"captcha","description":"What to do with a matching request."}}},"FingerprintRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/FingerprintRuleFields"}]},"FingerprintRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/FingerprintRuleFields"}]},"ErrorPageRule":{"allOf":[{"$ref":"#/components/schemas/RuleCommon"},{"$ref":"#/components/schemas/RulePathScope"},{"type":"object","properties":{"mode":{"type":"string","enum":["nsin","custom","origin"],"description":"* `nsin` — the NSIN branded error page.\n* `custom` — your own HTML, from `content`.\n* `origin` — pass the origin's own response through untouched.\n"},"codes":{"type":"array","items":{"type":"integer"},"description":"The status codes this rule covers."},"content":{"type":"object","additionalProperties":{"type":"string"},"description":"Status code (as a decimal string) → HTML. The key `\"0\"` is the\nfallback used for any covered code without its own page.\n**Only populated when fetching a single rule** — the list\nendpoint omits it.\n"},"content_codes":{"type":"array","items":{"type":"integer"},"description":"Which codes have HTML, ascending (`0` first when present).\nAlways populated, including in the list response.\n"},"content_bytes":{"type":"object","additionalProperties":{"type":"integer"},"description":"Byte size per entry in `content`. Empty on the list endpoint."}}}]},"ErrorPageRuleBody":{"allOf":[{"$ref":"#/components/schemas/RuleCommonBody"},{"$ref":"#/components/schemas/RulePathScope"},{"type":"object","properties":{"mode":{"type":"string","enum":["nsin","custom","origin"]},"codes":{"type":"array","items":{"type":"integer"}},"content":{"type":"object","additionalProperties":{"type":"string"},"description":"Replaces the rule's entire HTML set. Keys are decimal status\ncodes; `\"0\"` is the fallback. Omit the field to leave existing\ncontent untouched.\n"}}}]},"OverviewItem":{"type":"object","properties":{"domain_id":{"type":"integer"},"domain_name":{"type":"string"},"total_requests":{"type":"integer"},"total_bandwidth":{"type":"integer","description":"Bytes."},"unique_visitors":{"type":"integer"}}},"OverviewSeriesPoint":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"count":{"type":"integer"}}},"DomainsOverviewItem":{"type":"object","properties":{"domain_id":{"type":"integer"},"domain_name":{"type":"string"},"total_requests":{"type":"integer"},"total_bandwidth":{"type":"integer","description":"Bytes."},"error_rate":{"type":"number","description":"Share of requests that failed, 0–1."},"latest_event":{"type":"string","format":"date-time","description":"Most recent request seen for this domain or any subdomain. Null when there is no traffic."},"series":{"type":"array","description":"Requests over time, for a sparkline.","items":{"$ref":"#/components/schemas/OverviewSeriesPoint"}}}},"GlobalSummary":{"type":"object","properties":{"total_requests":{"type":"integer"},"total_bandwidth":{"type":"integer","description":"Bytes."},"unique_visitors":{"type":"integer"},"error_rate":{"type":"number","description":"Share of requests that failed, 0–1."},"cache_hit_rate":{"type":"number","description":"Share of cacheable requests served from cache, 0–1."},"peak_rps":{"type":"integer","description":"Highest request count in any single second of the period."},"domains":{"type":"integer","description":"How many domains contributed."}}},"AnalyticsSummary":{"type":"object","properties":{"total_requests":{"type":"integer"},"total_bandwidth":{"type":"integer","description":"Bytes."},"unique_visitors":{"type":"integer"},"error_rate":{"type":"number","description":"Share of requests that failed, 0–1."},"avg_response_time":{"type":"number","description":"Mean response time in milliseconds."},"p90":{"type":"number","description":"90th percentile response time, ms."},"p95":{"type":"number","description":"95th percentile response time, ms."},"p99":{"type":"number","description":"99th percentile response time, ms."}}},"RequestsDataPoint":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"count":{"type":"integer"}}},"BandwidthDataPoint":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"bytes_in":{"type":"integer"},"bytes_out":{"type":"integer"}}},"OriginBandwidthPoint":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"up":{"type":"integer","description":"Bytes sent to the origin."},"down":{"type":"integer","description":"Bytes received from the origin."}}},"DomainBandwidth":{"type":"object","properties":{"domain":{"type":"string"},"up":{"type":"integer"},"down":{"type":"integer"},"ratio":{"type":"number","description":"`min(up,down) / max(up,down)`."},"flagged":{"type":"boolean","description":"Set when `ratio` is close to 1.0, which is unusual for web traffic."}}},"OriginBandwidthResponse":{"type":"object","properties":{"series":{"type":"array","items":{"$ref":"#/components/schemas/OriginBandwidthPoint"}},"domains":{"type":"array","items":{"$ref":"#/components/schemas/DomainBandwidth"}}}},"TunnelSuspect":{"type":"object","properties":{"remote_addr":{"type":"string"},"network":{"type":"string","description":"The client's network operator (AS organisation)."},"country":{"type":"string"},"hostname":{"type":"string"},"transport":{"type":"string","enum":["ws","grpc","xhttp","http"]},"reqs":{"type":"integer","description":"All requests from this client to this host."},"tunnel_reqs":{"type":"integer","description":"Requests with opaque payloads."},"tunnel_paths":{"type":"integer","description":"Distinct paths used — around 1 for a tunnel."},"up":{"type":"integer","description":"Client-to-edge bytes."},"down":{"type":"integer","description":"Edge-to-client bytes."},"balance":{"type":"number","description":"`min/max` of up and down. Informational only."},"max_secs":{"type":"integer","description":"Longest single connection, in seconds."},"sample_path":{"type":"string","description":"The heaviest single path."},"ja4":{"type":"string","description":"TLS fingerprint."},"ja4h":{"type":"string","description":"HTTP fingerprint."},"ua":{"type":"string"}}},"TunnelSuspectsResponse":{"type":"object","properties":{"suspects":{"type":"array","items":{"$ref":"#/components/schemas/TunnelSuspect"}}}},"TopUri":{"type":"object","properties":{"uri":{"type":"string"},"request_count":{"type":"integer"}}},"TopRequestRow":{"type":"object","description":"One ranked row. Which fields are populated depends on the `metric` — the\nrest are omitted.\n","properties":{"key":{"type":"string","description":"The ranked value — path, country, user agent, hostname or `AS<number>`."},"label":{"type":"string","description":"Network operator name, for the `networks` metric."},"hostname":{"type":"string","description":"Owning host, for path-based metrics."},"asn":{"type":"integer"},"requests":{"type":"integer"},"bytes":{"type":"integer"},"avg_duration":{"type":"number","description":"Milliseconds."},"max_duration":{"type":"number","description":"Milliseconds."}}},"CountryStats":{"type":"object","properties":{"country":{"type":"string","description":"ISO country code."},"requests":{"type":"integer"},"bytes":{"type":"integer"},"unique_visitors":{"type":"integer"}}},"AsnStats":{"type":"object","properties":{"asn":{"type":"integer"},"asn_org":{"type":"string","description":"Network operator name."},"requests":{"type":"integer"},"bytes":{"type":"integer"}}},"ProtocolStats":{"type":"object","properties":{"protocol":{"type":"string","description":"`h1`, `h2`, `h3` or `other`."},"requests":{"type":"integer"}}},"StatusCodeStats":{"type":"object","properties":{"status_code":{"type":"integer"},"count":{"type":"integer"}}},"UnreachableReason":{"type":"object","properties":{"reason":{"type":"string","description":"Canonical key, e.g. `origin_closed`."},"label":{"type":"string","description":"Short headline."},"fault":{"type":"string","enum":["client","origin","network","config"],"description":"Who is responsible."},"meaning":{"type":"string","description":"Plain-language explanation."},"count":{"type":"integer"},"status":{"type":"integer","description":"Representative HTTP status the visitor saw."}}},"UserAgentCategoryStats":{"type":"object","properties":{"category":{"type":"string"},"requests":{"type":"integer"}}},"CacheAnalytics":{"type":"object","properties":{"hits":{"type":"integer"},"misses":{"type":"integer"},"bypass":{"type":"integer"},"hit_rate":{"type":"number","description":"`hits / (hits + misses)`; `0` when there were no cache lookups."},"bypass_reasons":{"type":"object","additionalProperties":{"type":"integer"},"description":"Why requests bypassed the cache, by reason."}}},"TrafficByCacheDataPoint":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"cached_bytes":{"type":"integer"},"miss_bytes":{"type":"integer"},"bypass_bytes":{"type":"integer"}}},"TrafficByReqStatusDataPoint":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"cache_bytes":{"type":"integer"},"proxied_bytes":{"type":"integer"},"direct_bytes":{"type":"integer"}}},"TrafficByNodeStats":{"type":"object","properties":{"node":{"type":"string"},"node_label":{"type":"string"},"country":{"type":"string"},"requests":{"type":"integer"},"bytes_out":{"type":"integer"},"cached_bytes":{"type":"integer"},"miss_bytes":{"type":"integer"},"bypass_bytes":{"type":"integer"},"cached_requests":{"type":"integer"},"miss_requests":{"type":"integer"},"bypass_requests":{"type":"integer"}}},"OriginNodeStats":{"type":"object","properties":{"node":{"type":"string"},"node_label":{"type":"string"},"country":{"type":"string"},"requests":{"type":"integer"},"failed":{"type":"integer"},"errors_5xx":{"type":"integer"},"avg_upstream":{"type":"number","description":"Mean origin response time, ms."},"bytes_down":{"type":"integer","description":"Bytes received from the origin."}}},"OriginStats":{"type":"object","properties":{"origin_addr":{"type":"string"},"requests":{"type":"integer"},"failed":{"type":"integer"},"errors_5xx":{"type":"integer"},"avg_upstream":{"type":"number","description":"Mean origin response time, ms."},"p95_upstream":{"type":"number","description":"95th percentile origin response time, ms."},"bytes_down":{"type":"integer"},"nodes":{"type":"array","description":"The per-edge-node split behind these totals.","items":{"$ref":"#/components/schemas/OriginNodeStats"}}}},"LogEntry":{"type":"object","description":"One request. Header and body fields are retained for a shorter window\nthan the rest of the row, so older entries return them empty.\n","properties":{"domain_id":{"type":"integer"},"timestamp":{"type":"string","format":"date-time"},"hostname":{"type":"string"},"method":{"type":"string"},"uri":{"type":"string","description":"Percent-encoded exactly as the client sent it."},"status":{"type":"integer","description":"Status returned to the visitor."},"remote_addr":{"type":"string"},"country":{"type":"string"},"duration":{"type":"number","description":"Total request duration in ms. For WebSockets this spans the whole connection."},"bytes_in":{"type":"integer"},"bytes_out":{"type":"integer"},"cache_status":{"type":"string","enum":["hit","miss","bypass"]},"bypass_reason":{"type":"string"},"req_status":{"type":"string","enum":["cache","proxied","direct"]},"user_agent":{"type":"string"},"headers":{"type":"string","description":"Request headers as captured by the edge."},"origin_req_headers":{"type":"string","description":"Headers the edge sent to the origin."},"origin_headers":{"type":"string","description":"Headers the origin returned."},"client_resp_headers":{"type":"string","description":"Headers returned to the visitor."},"body":{"type":"string"},"is_ws":{"type":"boolean"},"content_type":{"type":"string"},"error":{"type":"string"},"node":{"type":"string","description":"Edge node that served the request."},"ray_id":{"type":"string","description":"Unique id for this request."},"protocol":{"type":"string","description":"Client-to-edge protocol, e.g. `HTTP/2.0`."},"origin_protocol":{"type":"string","description":"Edge-to-origin protocol. Empty on a cache hit."},"origin_status":{"type":"integer","description":"Status the origin returned. `0` on a cache hit."},"origin_error_body":{"type":"string","description":"Bounded prefix of the body the origin sent with a 5xx, which the edge replaced with an error page."},"origin_addr":{"type":"string","description":"Origin `IP:port` the edge connected to."},"tls_version":{"type":"string"},"tls_cipher":{"type":"string"},"tls_resumed":{"type":"boolean"},"content_encoding":{"type":"string"},"referer":{"type":"string"},"cache_age":{"type":"integer","description":"Seconds the served object had been cached."},"asn":{"type":"integer"},"asn_org":{"type":"string"},"bot_kind":{"type":"string","description":"Bot classification, when the request was identified as one."},"bot_verified":{"type":"boolean","description":"Whether the bot's identity was verified, rather than merely claimed."},"detect_action":{"type":"string","description":"Action a detection rule took."},"detect_dry_run":{"type":"boolean","description":"True when the rule was in dry-run, so nothing was enforced."},"waf_score":{"type":"integer","description":"WAF anomaly score."},"waf_rule_ids":{"type":"string","description":"CRS rule ids that fired."},"ja4":{"type":"string"},"ja4h":{"type":"string"},"md_converted":{"type":"boolean","description":"The response was served as Markdown."},"md_tokens":{"type":"integer"},"orig_tokens":{"type":"integer"},"md_fail_reason":{"type":"string"}}},"LogsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}},"total":{"type":"integer","description":"Rows matching the filters, before paging."},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"WafLogEntry":{"type":"object","properties":{"ts":{"type":"string","format":"date-time"},"domainId":{"type":"integer"},"recordId":{"type":"integer"},"hostname":{"type":"string"},"node":{"type":"string"},"rayId":{"type":"string"},"clientIp":{"type":"string"},"country":{"type":"string"},"clientPort":{"type":"integer"},"method":{"type":"string"},"uri":{"type":"string"},"httpVersion":{"type":"string"},"action":{"type":"string"},"blocked":{"type":"boolean"},"dryRun":{"type":"boolean","description":"True when the rule only logged; the request was not blocked."},"score":{"type":"integer","description":"Anomaly score reached."},"paranoia":{"type":"integer"},"threshold":{"type":"integer"},"status":{"type":"integer"},"ja4":{"type":"string"},"ja4h":{"type":"string"},"userAgent":{"type":"string"},"headers":{"type":"string"},"body":{"type":"string"},"ruleIds":{"type":"array","items":{"type":"integer"}},"messages":{"type":"array","items":{"type":"string"}},"ruleData":{"type":"array","items":{"type":"string"}},"variables":{"type":"array","items":{"type":"string"}},"severities":{"type":"array","items":{"type":"integer"}},"tags":{"type":"array","items":{"type":"string"}}}},"MarkdownTesterFetch":{"type":"object","properties":{"status":{"type":"integer"},"content_type":{"type":"string"},"content_length":{"type":"integer","description":"Full body length observed, before truncation."},"body":{"type":"string"},"truncated":{"type":"boolean"},"binary":{"type":"boolean","description":"The body was not valid UTF-8, so `body` is omitted."},"cache_status":{"type":"string"},"markdown_tokens":{"type":"integer"},"original_tokens":{"type":"integer"},"converted":{"type":"boolean","description":"The response came back as `text/markdown`."},"error":{"type":"string"}}},"MarkdownTesterResult":{"type":"object","properties":{"url":{"type":"string","description":"The URL that was fetched."},"feature_enabled":{"type":"boolean","description":"The domain's `markdown_for_agents` setting at test time."},"html":{"$ref":"#/components/schemas/MarkdownTesterFetch"},"markdown":{"$ref":"#/components/schemas/MarkdownTesterFetch"}}},"AnalyticsQueryResult":{"type":"object","properties":{"columns":{"type":"array","description":"Column names, in result order.","items":{"type":"string"}},"rows":{"type":"array","description":"One entry per row, keyed by column name.","items":{"type":"object","additionalProperties":true}},"row_count":{"type":"integer"},"truncated":{"type":"boolean","description":"True when the 10 000-row cap was reached and results were cut short."}}},"OutageIncident":{"type":"object","properties":{"id":{"type":"integer"},"hostname":{"type":"string"},"state":{"type":"string","description":"`open` while ongoing, `resolved` once recovered."},"ongoing":{"type":"boolean"},"started_at":{"type":"string","format":"date-time"},"resolved_at":{"type":"string","format":"date-time","description":"Absent while the incident is ongoing."},"duration_seconds":{"type":"integer"},"peak_err_pct":{"type":"number","description":"Highest origin-error percentage reached."},"sample_reqs":{"type":"integer","description":"Requests observed over the incident."}}},"UptimeLiveStatus":{"type":"object","properties":{"hostname":{"type":"string"},"requests":{"type":"integer"},"errors":{"type":"integer","description":"Origin-attributable 5xx responses."},"error_pct":{"type":"number","description":"Origin-error percentage across the window."},"down":{"type":"boolean","description":"Currently meets this domain's alert thresholds."},"incident":{"type":"boolean","description":"An incident is currently open for this host."}}},"UptimeLive":{"type":"object","properties":{"window_min":{"type":"integer","description":"Length of the trailing window, in minutes."},"hosts":{"type":"array","items":{"$ref":"#/components/schemas/UptimeLiveStatus"}}}},"UptimeSettingsBounds":{"type":"object","description":"Valid range for each configurable field.","properties":{"threshold_pct_min":{"type":"integer"},"threshold_pct_max":{"type":"integer"},"window_min_min":{"type":"integer"},"window_min_max":{"type":"integer"},"min_requests_min":{"type":"integer"},"recover_min_min":{"type":"integer"},"recover_min_max":{"type":"integer"}}},"UptimeSettings":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether outage alerts are sent for this domain."},"threshold_pct":{"type":"integer","description":"Per-minute origin-error percentage that counts as down."},"window_min":{"type":"integer","description":"Minutes the host must stay down before an incident opens."},"min_requests":{"type":"integer","description":"Traffic floor — below this, no incident opens."},"min_active_min":{"type":"integer","description":"Minimum populated one-minute buckets required in the window."},"recover_min":{"type":"integer","description":"Consecutive clear minutes before an incident resolves."},"bounds":{"$ref":"#/components/schemas/UptimeSettingsBounds"}}},"UptimeSettingsUpdate":{"type":"object","description":"Every field is optional; omitted fields keep their current value.","properties":{"enabled":{"type":"boolean"},"threshold_pct":{"type":"integer"},"window_min":{"type":"integer"},"min_requests":{"type":"integer"},"min_active_min":{"type":"integer"},"recover_min":{"type":"integer"}}},"RecommendationAction":{"type":"object","description":"Where to go to act on the recommendation.","properties":{"label":{"type":"string"},"feature":{"type":"string","description":"Logical target, e.g. `cache`."},"query":{"type":"object","additionalProperties":{"type":"string"},"description":"Parameters that pre-filter the target view."}}},"Recommendation":{"type":"object","properties":{"key":{"type":"string","description":"Stable identifier — pass it to the dismiss endpoints."},"status":{"type":"string","enum":["ok","warn"],"description":"`ok` is a passing check; `warn` needs action."},"severity":{"type":"string","enum":["high","medium","low"]},"category":{"type":"string","enum":["speed","seo","reachability","security"]},"title":{"type":"string"},"detail":{"type":"string"},"stats":{"type":"object","additionalProperties":true,"description":"Supporting figures behind the finding."},"action":{"$ref":"#/components/schemas/RecommendationAction"},"dismissed":{"type":"boolean","description":"Dismissed by the calling user. Dismissals are per user, not per domain."}}},"PurgeResult":{"type":"object","properties":{"deleted":{"type":"integer"},"accepted":{"type":"boolean","description":"The purge was queued to run in the background, so `deleted` is not yet known."}}},"CacheKeyRow":{"type":"object","description":"One cached object. `host`/`path`/`query` are the readable request URL;\n`hostname`/`store_path`/`key_hash`/`node` are the stored identity to\necho back when purging this specific row.\n","properties":{"domain_id":{"type":"integer"},"domain":{"type":"string"},"host":{"type":"string","description":"Exact request host, e.g. `sub.example.com`."},"path":{"type":"string","description":"Exact request path, e.g. `/assets/app.js`."},"query":{"type":"string","description":"Raw query string, without the leading `?`."},"variant":{"type":"string","description":"Cache-key suffix separating this entry from other variants of the\nsame URL (device, image format, CORS origin, …). Empty for the plain\nvariant.\n"},"method":{"type":"string"},"node":{"type":"string","description":"Edge node that cached it."},"hostname":{"type":"string","description":"Storage namespace host — `*.example.com` for a wildcard record."},"store_path":{"type":"string","description":"Raw stored path. Needed for purging; not for display."},"key_hash":{"type":"string"},"cache_key":{"type":"string"},"l2_key":{"type":"string","description":"Reconstructed storage key, for debugging."},"size":{"type":"integer","description":"Bytes."},"cached_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}}},"CacheKeysPage":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/CacheKeyRow"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"CacheNodeTotal":{"type":"object","properties":{"node":{"type":"string"},"entries":{"type":"integer"},"size_bytes":{"type":"integer"}}},"CacheTotals":{"type":"object","properties":{"entries":{"type":"integer"},"size_bytes":{"type":"integer"},"by_node":{"type":"array","items":{"$ref":"#/components/schemas/CacheNodeTotal"}}}},"CachePurgeTarget":{"type":"object","description":"One entry's stored identity. Note the camelCase field names — they differ\nfrom the snake_case used in the listing response.\n","required":["hostname","keyHash","node"],"properties":{"domainId":{"type":"integer"},"hostname":{"type":"string","description":"The listing's `hostname`."},"storePath":{"type":"string","description":"The listing's `store_path`."},"keyHash":{"type":"string","description":"The listing's `key_hash`."},"node":{"type":"string","description":"The listing's `node`."}}},"CachePurgeKeysRequest":{"type":"object","description":"Supply either `entries` or `filter`.","properties":{"mode":{"type":"string","enum":["delete","refresh"],"default":"delete","description":"`delete` removes the entry and drops it from the listing;\n`refresh` only evicts the stored copy so the next visitor re-fills it.\n"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/CachePurgeTarget"}},"filter":{"type":"object","description":"Purge everything matching this filter.","properties":{"hostname":{"type":"string"},"node":{"type":"string"},"path":{"type":"string","description":"Path wildcard, e.g. `/assets/*`."}}}}},"CachePurgeKeysResult":{"type":"object","properties":{"deleted":{"type":"integer"},"mode":{"type":"string","enum":["delete","refresh"]},"truncated":{"type":"boolean","description":"The filter matched more entries than one call may touch. Repeat the\nrequest until this is false.\n"}}},"Member":{"type":"object","properties":{"user_id":{"type":"integer"},"email":{"type":"string"},"name":{"type":"string"},"role":{"$ref":"#/components/schemas/Role"},"is_owner":{"type":"boolean"},"is_self":{"type":"boolean","description":"True for the account this key belongs to."},"joined_at":{"type":"string","format":"date-time"},"notify_domain":{"type":"boolean","description":"Receive domain status notifications."},"notify_uptime":{"type":"boolean","description":"Receive outage notifications."},"notify_ssl":{"type":"boolean","description":"Receive certificate notifications."}}},"Membership":{"type":"object","description":"The stored membership row, as written. This is what\n`PATCH /domains/{domain}/members/{userId}` returns — the member LIST is\na different, enriched shape (`Member`), joined against the user account.\nNothing here identifies the user beyond `user_id`.\n","properties":{"id":{"type":"integer","description":"Membership row id","not the user id.":null},"domain_id":{"type":"integer"},"user_id":{"type":"integer"},"role":{"type":"string","description":"The stored role — `admin`, `editor` or `viewer` for a granted\nmember, or `provider` on the row that projects the domain's service\nprovider. `provider` is not grantable and cannot be set here.\n"},"notify_domain":{"type":"boolean"},"notify_uptime":{"type":"boolean"},"notify_ssl":{"type":"boolean"},"invited_by":{"type":"integer","description":"User who granted the access; 0 when unknown."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"MemberList":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/Member"}},"my_role":{"$ref":"#/components/schemas/Role"},"can_edit":{"type":"boolean","description":"Whether you may manage membership on this domain."}}},"GrantableRole":{"type":"string","description":"A role that may be assigned to a member or invitation. `owner` is not\ngrantable — it always follows domain ownership.\n","enum":["admin","editor","viewer"]},"MemberUpdate":{"type":"object","description":"Partial patch — send only what you want to change. At least one field is\nrequired.\n","properties":{"role":{"$ref":"#/components/schemas/GrantableRole"},"notify_domain":{"type":"boolean"},"notify_uptime":{"type":"boolean"},"notify_ssl":{"type":"boolean"}}},"Invite":{"type":"object","properties":{"id":{"type":"integer"},"domain_id":{"type":"integer"},"email":{"type":"string","description":"The address the invitation is bound to."},"role":{"$ref":"#/components/schemas/GrantableRole"},"invited_by":{"type":"integer","description":"User id of the inviter."},"expires_at":{"type":"string","format":"date-time"},"max_uses":{"type":"integer"},"uses":{"type":"integer"},"revoked_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"link":{"type":"string","description":"The accept URL. Returned when the invitation is created or resent."},"is_link":{"type":"boolean","description":"True for a shareable link rather than an emailed invitation."},"exhausted":{"type":"boolean","description":"True when `uses` has reached `max_uses`."}}},"InviteCreate":{"type":"object","required":["email","role"],"properties":{"email":{"type":"string","format":"email"},"role":{"$ref":"#/components/schemas/GrantableRole"},"expires_in_hours":{"type":"integer","description":"Lifetime of the invitation. Omit for the default."}}},"InvitePreview":{"type":"object","properties":{"domain":{"type":"string"},"role":{"$ref":"#/components/schemas/Role"},"inviter":{"type":"string","description":"Display name of whoever sent it."},"is_link":{"type":"boolean"},"email_match":{"type":"boolean","description":"Whether the invitation was addressed to the calling account.\nAccepting fails when this is false.\n"},"expires_at":{"type":"string","format":"date-time"},"already_member":{"type":"boolean","description":"You already have access; the other fields describe your existing role."},"is_owner":{"type":"boolean"},"invited_email":{"type":"string","description":"Masked target address. Present only when `email_match` is false."}}},"InviteAcceptResult":{"type":"object","properties":{"accepted":{"type":"boolean"},"already_member":{"type":"boolean","description":"Returned instead of `accepted` when you already had access."},"domain":{"type":"string"},"role":{"$ref":"#/components/schemas/Role"}}},"InviteMismatch":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","const":"invite_email_mismatch"},"invited_email":{"type":"string","description":"Masked address the invitation was actually sent to."}}},"Wallet":{"type":"object","properties":{"id":{"type":"integer"},"user_id":{"type":"integer"},"balance_rials":{"type":"integer"},"negative_since":{"type":"string","format":"date-time","description":"When the balance first went below zero in the current debt cycle.\nAbsent while non-negative. Staying negative past the grace window\nsuspends paid domains.\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"WalletTransaction":{"type":"object","properties":{"id":{"type":"integer"},"wallet_id":{"type":"integer"},"user_id":{"type":"integer"},"type":{"type":"string","enum":["topup","purchase","refund","admin_adjust"]},"amount_rials":{"type":"integer","description":"Signed — positive credits, negative debits."},"balance_after":{"type":"integer"},"description":{"type":"string"},"ref_type":{"type":"string","description":"What the row refers to — `payment`, `subscription`, `traffic` or `manual`."},"ref_id":{"type":"integer"},"ref_code":{"type":"string","description":"Payment gateway reference, for top-ups."},"created_at":{"type":"string","format":"date-time"},"domain_id":{"type":"integer","description":"Set on traffic charges."},"domain_name":{"type":"string","description":"Set on traffic charges."}}},"TrafficDomainBreakdown":{"type":"object","description":"One domain's share of a single traffic charge.","properties":{"domain_id":{"type":"integer"},"domain_name":{"type":"string"},"cached_bytes":{"type":"integer"},"proxied_bytes":{"type":"integer"},"direct_bytes":{"type":"integer"},"bypass_bytes":{"type":"integer","description":"Legacy two-tier column, on historical rows only."},"charged_rials":{"type":"integer"}}},"WalletTransactionDetail":{"allOf":[{"$ref":"#/components/schemas/WalletTransaction"},{"type":"object","properties":{"node":{"type":"string"},"billing_window_key":{"type":"string","description":"Identifies the billing window a traffic charge covers."},"by_domain":{"type":"array","description":"Per-domain contribution to this charge. Traffic charges only.","items":{"$ref":"#/components/schemas/TrafficDomainBreakdown"}},"cached_bytes":{"type":"integer","description":"Total across `by_domain`."},"proxied_bytes":{"type":"integer","description":"Total across `by_domain`."},"direct_bytes":{"type":"integer","description":"Total across `by_domain`."},"bypass_bytes":{"type":"integer","description":"Total across `by_domain`."}}}]},"Subscription":{"type":"object","description":"A plan attached to one domain. Entitlements are **not** read from the\nplan directly — use `GET /domains/{domain}/features`, which resolves any\nper-subscription overrides.\n","properties":{"id":{"type":"integer"},"user_id":{"type":"integer"},"domain_id":{"type":"integer"},"domain_name":{"type":"string"},"plan_id":{"type":"integer"},"plan":{"type":"object","additionalProperties":true,"description":"The plan this subscription is on."},"plan_term_id":{"type":"integer"},"plan_term":{"type":"object","additionalProperties":true,"description":"The billing term purchased."},"status":{"type":"string","description":"For example `active`, `expired`, `grace` or `cancelled`."},"started_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"grace_until":{"type":"string","format":"date-time"},"auto_renew":{"type":"boolean"},"quota_reset_days":{"type":"integer","description":"Traffic-allowance reset cadence, frozen at purchase time so later\nplan changes cannot shift an existing subscriber's quota window.\n"},"is_trial":{"type":"boolean","description":"The free trial granted at signup. Downgrades to the free plan on\nexpiry rather than entering grace.\n"}}},"DomainPlanSummary":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"plan_slug":{"type":"string"},"plan_name":{"type":"string"},"status":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}}},"DomainFeatures":{"type":"object","description":"The domain's effective entitlements, after per-subscription overrides.\nA `null` limit means unlimited.\n","properties":{"plan_id":{"type":"integer"},"plan_name":{"type":"string"},"plan_slug":{"type":"string"},"has_active_plan":{"type":"boolean"},"max_records":{"type":"integer","description":"Null means unlimited."},"max_traffic_gb":{"type":"integer","description":"Null means unlimited."},"max_rules_per_set":{"type":"integer","description":"Rules allowed per rule type. Null means unlimited."},"max_cache_cap_mb":{"type":"integer","description":"Ceiling for the domain's `cache_cap_mb`."},"logs_enabled":{"type":"boolean","description":"Gates the raw-log and top-N analytics endpoints."},"monitoring_enabled":{"type":"boolean","description":"Gates most analytics sections."},"rules_enabled":{"type":"boolean"},"cache_purge_enabled":{"type":"boolean","description":"Gates the cache purge endpoints."},"custom_ssl_enabled":{"type":"boolean","description":"Gates custom certificate upload."},"ws_enabled":{"type":"boolean","description":"WebSocket support."},"host_header_edit_enabled":{"type":"boolean"},"dedicated_support_enabled":{"type":"boolean"},"domain_usage":{"type":"array","description":"Current usage against the limits above.","items":{"type":"object","additionalProperties":true}},"plan_term_id":{"type":"integer"},"billing_duration_days":{"type":"integer"},"quota_reset_days":{"type":"integer"},"quota_period_start":{"type":"string","format":"date-time"},"quota_period_end":{"type":"string","format":"date-time"}}},"DomainTrafficUsageRow":{"type":"object","description":"One day's traffic for one domain.","properties":{"id":{"type":"integer"},"domain_id":{"type":"integer"},"user_id":{"type":"integer"},"date":{"type":"string","format":"date"},"billing_window_key":{"type":"string"},"cached_bytes":{"type":"integer"},"proxied_bytes":{"type":"integer"},"direct_bytes":{"type":"integer"},"bypass_bytes":{"type":"integer","description":"Legacy two-tier column, present on historical rows. Folded into the\ndirect total for display.\n"},"charged_rials":{"type":"integer"},"window_start":{"type":"string","format":"date-time"},"window_end":{"type":"string","format":"date-time"},"billed_cached_bytes":{"type":"integer","description":"The portion above the plan's free allowance."},"billed_proxied_bytes":{"type":"integer","description":"The portion above the plan's free allowance."},"billed_direct_bytes":{"type":"integer","description":"The portion above the plan's free allowance."},"charged_cached_rials":{"type":"integer"},"charged_proxied_rials":{"type":"integer"},"charged_direct_rials":{"type":"integer"},"invoice_id":{"type":"integer"},"processed_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"AccountTrafficUsage":{"type":"object","properties":{"usage":{"type":"array","items":{"$ref":"#/components/schemas/DomainTrafficUsageRow"}},"total_cached_bytes":{"type":"integer"},"total_proxied_bytes":{"type":"integer"},"total_direct_bytes":{"type":"integer"},"total_charged_rials":{"type":"integer"},"cached_price_per_gb":{"type":"integer","description":"Rials per GB of cache-served traffic."},"proxied_price_per_gb":{"type":"integer","description":"Rials per GB of proxied traffic."},"direct_price_per_gb":{"type":"integer","description":"Rials per GB of direct traffic."}}},"InvoiceItem":{"type":"object","properties":{"id":{"type":"integer"},"invoice_id":{"type":"integer"},"description":{"type":"string"},"quantity":{"type":"integer"},"unit_price_rials":{"type":"integer"},"total_rials":{"type":"integer"}}},"Invoice":{"type":"object","properties":{"id":{"type":"integer"},"number":{"type":"string","description":"Human-facing invoice number, sequential per Jalali year."},"user_id":{"type":"integer"},"domain_id":{"type":"integer"},"domain_name":{"type":"string"},"subscription_id":{"type":"integer"},"payment_id":{"type":"integer"},"kind":{"type":"string","enum":["subscription","topup","manual"]},"status":{"type":"string","enum":["paid","unpaid","cancelled"]},"subtotal_rials":{"type":"integer"},"tax_rials":{"type":"integer"},"total_rials":{"type":"integer"},"issued_at":{"type":"string","format":"date-time"},"paid_at":{"type":"string","format":"date-time"},"notes":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceItem"}}}},"PeriodDomainTraffic":{"type":"object","properties":{"domain":{"type":"string"},"cached_bytes":{"type":"integer"},"proxied_bytes":{"type":"integer"},"direct_bytes":{"type":"integer"},"estimated_charged_rials":{"type":"integer"}}},"PeriodStatement":{"type":"object","description":"One billing period's cost. For the period in progress the traffic\nfigures are a running estimate.\n","properties":{"id":{"type":"integer"},"subscription_id":{"type":"integer"},"plan_id":{"type":"integer"},"plan_name":{"type":"string"},"plan_term_id":{"type":"integer"},"billing_duration_days":{"type":"integer"},"quota_reset_days":{"type":"integer"},"period_start":{"type":"string"},"period_end":{"type":"string"},"plan_price_rials":{"type":"integer"},"traffic_cached_bytes":{"type":"integer"},"traffic_proxied_bytes":{"type":"integer"},"traffic_direct_bytes":{"type":"integer"},"traffic_bypass_bytes":{"type":"integer","description":"Legacy two-tier column, on historical periods only."},"traffic_charged_rials":{"type":"integer"},"by_domain":{"type":"array","items":{"$ref":"#/components/schemas/PeriodDomainTraffic"}}}},"TicketAttachment":{"type":"object","properties":{"id":{"type":"integer"},"message_id":{"type":"integer"},"original_name":{"type":"string"},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"url":{"type":"string","description":"Where to download the attachment."}}},"TicketMessage":{"type":"object","properties":{"id":{"type":"integer"},"ticket_id":{"type":"integer"},"author_user_id":{"type":"integer"},"author":{"type":"object","additionalProperties":true,"description":"The message author."},"body":{"type":"string"},"is_staff":{"type":"boolean","description":"True when written by support staff."},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/TicketAttachment"}},"created_at":{"type":"string","format":"date-time"}}},"Ticket":{"type":"object","properties":{"id":{"type":"integer"},"user_id":{"type":"integer"},"subject":{"type":"string"},"status":{"type":"string","description":"For example `open` or `closed`."},"closed_at":{"type":"string","format":"date-time"},"closed_by_user_id":{"type":"integer"},"user_last_read_message_id":{"type":"integer"},"staff_last_read_message_id":{"type":"integer"},"messages":{"type":"array","description":"The thread. Populated when fetching a single ticket.","items":{"$ref":"#/components/schemas/TicketMessage"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TicketListItem":{"allOf":[{"$ref":"#/components/schemas/Ticket"},{"type":"object","properties":{"is_unread":{"type":"boolean","description":"There are replies you have not read."}}}]},"TicketCreated":{"type":"object","description":"The envelope `POST /tickets` answers with. The two halves come back\ntogether because the ticket and its first message are written in one\ntransaction, and the ticket in this response is not thread-loaded — only\n`GET /tickets/{id}` populates `messages`.\n","properties":{"ticket":{"$ref":"#/components/schemas/Ticket"},"first_message":{"$ref":"#/components/schemas/TicketMessage"}}}}}}