{"openapi":"3.1.0","info":{"title":"Merlonix Public API","version":"1.0.0","description":"Programmatic access to your Merlonix monitoring data — assets, SSL/DNS check history, alerts, clients, and status pages.\n\n## Authentication\n\nAuthenticate every request with an API key as a bearer token:\n\n```\nAuthorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nCreate and revoke keys in the dashboard under **Settings → API keys**. The plaintext key is shown exactly once at creation.\n\n## Scopes\n\nA key carries one of two scope sets:\n\n- **`read`** (default) — `GET` requests only. Any mutating method returns `403`.\n- **`read,write`** — additionally permits `POST`/`PATCH`/`DELETE` to create, update, and delete assets, channels, clients, and status pages. Choose the scope when minting the key.\n\nA write key onboards assets one at a time (`POST /v1/assets`) or in bulk (`POST /v1/assets/bulk`, up to 100 per call). Plan asset quotas and the per-tier minimum check interval are enforced identically to the dashboard.\n\n## Rate limits\n\nRequests are rate-limited per tenant by plan tier. Exceeding the limit returns `429` with a `Retry-After` header.","contact":{"name":"Merlonix Support","url":"https://merlonix.com/support"}},"servers":[{"url":"https://api.merlonix.com","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Assets","description":"Monitored domains, hostnames, and certificates."},{"name":"Checks","description":"SSL and DNS check history (30-day hot window)."},{"name":"Alerts","description":"Fired alerts and their lifecycle state."},{"name":"Channels","description":"Alert notification channels (email, webhooks, SMS, PagerDuty, …)."},{"name":"Clients","description":"Client buckets that group assets."},{"name":"Status pages","description":"Per-client public status pages."}],"paths":{"/v1/assets":{"get":{"tags":["Assets"],"summary":"List assets","description":"Cursor-paginated list of the tenant's monitored assets.","operationId":"listAssets","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size (1–200, default 50).","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"Opaque pagination cursor from a prior response `page.next_cursor`.","schema":{"type":"string"}},{"name":"asset_type","in":"query","required":false,"description":"Filter by asset type.","schema":{"type":"string","enum":["domain","hostname","certificate","dns_record"]}},{"name":"client_id","in":"query","required":false,"description":"Filter by client bucket.","schema":{"type":"string","format":"uuid"}},{"name":"include_inactive","in":"query","required":false,"description":"Include soft-deleted / paused assets (default false).","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"A page of assets.","content":{"application/json":{"schema":{"type":"object","required":["data","page"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"page":{"$ref":"#/components/schemas/PageInfo"}}}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Assets"],"summary":"Create an asset","description":"Register a new monitored asset. Requires a `write`-scoped key. Enforces the plan asset quota and the per-tier minimum check interval; a hostname that fails the abuse filter is rejected (`422`). TD-2480: if this hostname was previously deleted, the existing (soft-deleted) asset is RESTORED with its full check history rather than a new one being created — the response is `200` with `revived: true` and the ORIGINAL asset id. A `409` therefore means an asset you can still see in `GET /v1/assets` already has this hostname and type.","operationId":"createAsset","requestBody":{"required":true,"description":"The asset to create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCreate"}}}},"responses":{"200":{"description":"A previously-deleted asset with this hostname was restored (TD-2480). The id is the ORIGINAL asset id and its check history is intact.","content":{"application/json":{"schema":{"type":"object","required":["asset","revived"],"properties":{"asset":{"$ref":"#/components/schemas/Asset"},"revived":{"type":"boolean","enum":[true]}}}}}},"201":{"description":"The created asset.","content":{"application/json":{"schema":{"type":"object","required":["asset"],"properties":{"asset":{"$ref":"#/components/schemas/Asset"}}}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/assets/bulk":{"post":{"tags":["Assets"],"summary":"Bulk-create assets","description":"Create up to 100 assets in one request — for onboarding a client estate programmatically. Requires a `write`-scoped key. The plan asset quota is enforced across the whole batch (the request is rejected with `402` if it would exceed the ceiling). Per-asset failures are reported in `errors` without aborting the batch; valid rows are still created. Returns `201` when at least one asset was created or revived, `207` when every row failed. TD-2480: a row whose hostname belongs to a previously-deleted asset RESTORES that asset (with its check history) and is reported in `revived` / `revived_count`, not in `created` — `skipped` now means only that an asset you can still see already has that hostname.","operationId":"bulkCreateAssets","requestBody":{"required":true,"description":"The assets to create (1–100).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetBulkCreate"}}}},"responses":{"201":{"description":"Batch result (≥1 created).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetBulkResult"}}}},"207":{"description":"Batch result (every row failed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetBulkResult"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/assets/{id}":{"get":{"tags":["Assets"],"summary":"Get an asset","operationId":"getAsset","parameters":[{"name":"id","in":"path","required":true,"description":"Asset UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The asset.","content":{"application/json":{"schema":{"type":"object","required":["asset"],"properties":{"asset":{"$ref":"#/components/schemas/Asset"}}}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Assets"],"summary":"Update an asset","description":"Update mutable fields of an asset (label, client, check interval, enabled checks, active state). Requires a `write`-scoped key.","operationId":"updateAsset","parameters":[{"name":"id","in":"path","required":true,"description":"Asset UUID.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"The fields to update.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetUpdate"}}}},"responses":{"200":{"description":"The updated asset.","content":{"application/json":{"schema":{"type":"object","required":["asset"],"properties":{"asset":{"$ref":"#/components/schemas/Asset"}}}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Assets"],"summary":"Delete an asset","description":"Soft-delete an asset (monitoring stops). Requires a `write`-scoped key.","operationId":"deleteAsset","parameters":[{"name":"id","in":"path","required":true,"description":"Asset UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted — no content."},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/checks":{"get":{"tags":["Checks"],"summary":"List checks for an asset","description":"Keyset-paginated check history for one asset, for any monitored kind reported by /v1/checks/latest (TD-2432). Results are clamped to the most recent 30 days. `crawl` rows are ordered and windowed on started_at; every other kind uses checked_at.","operationId":"listChecks","parameters":[{"name":"asset_id","in":"query","required":true,"description":"Asset UUID to scope checks to.","schema":{"type":"string","format":"uuid"}},{"name":"type","in":"query","required":true,"description":"Check kind.","schema":{"type":"string","enum":["ssl","dns","uptime","heartbeat","port","psi","health","dnsbl","crawl","injection_scan","dns_security","email_auth","security_headers","ct_watch"]}},{"name":"since","in":"query","required":false,"description":"ISO-8601 lower bound (clamped to 30 days ago).","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"description":"ISO-8601 upper bound.","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"Page size (1–200, default 50).","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"Opaque pagination cursor from a prior response `page.next_cursor`.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of checks.","content":{"application/json":{"schema":{"type":"object","required":["data","page"],"properties":{"data":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/SslCheck"},{"$ref":"#/components/schemas/DnsCheck"},{"$ref":"#/components/schemas/UptimeCheck"},{"type":"object","description":"For the TD-2432 kinds (heartbeat, port, psi, health, dnsbl, crawl, injection_scan, dns_security, email_auth, security_headers, ct_watch): the same read-only snapshot shape /v1/checks/latest returns for that kind.","additionalProperties":true}]}},"page":{"$ref":"#/components/schemas/PageInfo"}}}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/checks/latest":{"get":{"tags":["Checks"],"summary":"Latest checks for an asset","description":"The most recent SSL, DNS, and (when enabled) uptime check for an asset.","operationId":"getLatestChecks","parameters":[{"name":"asset_id","in":"query","required":true,"description":"Asset UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Latest SSL + DNS + uptime check (each null when none exists).","content":{"application/json":{"schema":{"type":"object","required":["ssl","dns"],"properties":{"ssl":{"oneOf":[{"$ref":"#/components/schemas/SslCheck"},{"type":"null"}]},"dns":{"oneOf":[{"$ref":"#/components/schemas/DnsCheck"},{"type":"null"}]},"uptime":{"oneOf":[{"$ref":"#/components/schemas/UptimeCheck"},{"type":"null"}]}}}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/alerts":{"get":{"tags":["Alerts"],"summary":"List alerts","description":"Cursor-paginated alert list with optional lifecycle and severity filters.","operationId":"listAlerts","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size (1–200, default 50).","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","required":false,"description":"Opaque pagination cursor from a prior response `page.next_cursor`.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Filter by derived lifecycle status.","schema":{"type":"string","enum":["open","acknowledged","muted","resolved"]}},{"name":"severity","in":"query","required":false,"description":"Filter by severity.","schema":{"type":"string","enum":["info","warning","critical"]}},{"name":"asset_id","in":"query","required":false,"description":"Filter to one asset.","schema":{"type":"string","format":"uuid"}},{"name":"since","in":"query","required":false,"description":"ISO-8601 lower bound on fired_at.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"A page of alerts.","content":{"application/json":{"schema":{"type":"object","required":["data","page"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Alert"}},"page":{"$ref":"#/components/schemas/PageInfo"}}}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/alerts/{id}":{"get":{"tags":["Alerts"],"summary":"Get an alert","operationId":"getAlert","parameters":[{"name":"id","in":"path","required":true,"description":"Alert UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The alert.","content":{"application/json":{"schema":{"type":"object","required":["alert"],"properties":{"alert":{"$ref":"#/components/schemas/Alert"}}}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/channels":{"get":{"tags":["Channels"],"summary":"List channels","description":"All alert notification channels configured for the tenant.","operationId":"listChannels","responses":{"200":{"description":"All channels.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}}}}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Channels"],"summary":"Create a channel","description":"Create an alert notification channel. Requires a `write`-scoped key. The `target` shape depends on `kind` (email address, https webhook URL, `<bot_token>:<chat_id>` for telegram, routing/integration key for pagerduty/opsgenie, or an E.164 phone for sms) and is validated per-kind (`422` on a mismatch). For webhook kinds a signing secret is server-minted and returned only as a `signing_secret_preview`.","operationId":"createChannel","requestBody":{"required":true,"description":"The channel to create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelCreate"}}}},"responses":{"201":{"description":"The created channel.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Channel"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/channels/{id}":{"patch":{"tags":["Channels"],"summary":"Update a channel","description":"Update mutable fields of a channel (name, target, active state, severity filter). At least one field is required. Requires a `write`-scoped key.","operationId":"updateChannel","parameters":[{"name":"id","in":"path","required":true,"description":"Channel UUID.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"The fields to update.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelUpdate"}}}},"responses":{"200":{"description":"The updated channel.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Channel"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Channels"],"summary":"Delete a channel","description":"Delete a notification channel. Requires a `write`-scoped key.","operationId":"deleteChannel","parameters":[{"name":"id","in":"path","required":true,"description":"Channel UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted — no content."},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/clients":{"get":{"tags":["Clients"],"summary":"List clients","description":"All client buckets for the tenant, each with its active asset count.","operationId":"listClients","responses":{"200":{"description":"All clients.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Client"}}}}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Clients"],"summary":"Create a client","description":"Create a client bucket to group assets. Requires a `write`-scoped key. The `slug` is derived from the name when omitted and must be unique per tenant (`409` on collision).","operationId":"createClient","requestBody":{"required":true,"description":"The client to create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientCreate"}}}},"responses":{"201":{"description":"The created client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/clients/{id}":{"get":{"tags":["Clients"],"summary":"Get a client","operationId":"getClient","parameters":[{"name":"id","in":"path","required":true,"description":"Client UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Clients"],"summary":"Update a client","description":"Update mutable fields of a client (name, slug, logo, brand color). Requires a `write`-scoped key. The catch-all Unassigned bucket cannot be edited (`403`).","operationId":"updateClient","parameters":[{"name":"id","in":"path","required":true,"description":"Client UUID.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"The fields to update.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientUpdate"}}}},"responses":{"200":{"description":"The updated client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Clients"],"summary":"Delete a client","description":"Delete a client bucket; its assets fall back to Unassigned. Requires a `write`-scoped key. The Unassigned bucket cannot be deleted (`403`).","operationId":"deleteClient","parameters":[{"name":"id","in":"path","required":true,"description":"Client UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted — no content."},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/status-pages":{"get":{"tags":["Status pages"],"summary":"List status pages","description":"All per-client status pages for the tenant, each with its component count.","operationId":"listStatusPages","responses":{"200":{"description":"All status pages.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StatusPage"}}}}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Status pages"],"summary":"Create a status page","description":"Create a public status page for a client. Requires a `write`-scoped key. The free tier is capped at one status page (`402` beyond it); paid tiers allow one page per client. Add components, incidents, and subscribers through the dashboard sub-resources.","operationId":"createStatusPage","requestBody":{"required":true,"description":"The status page to create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusPageCreate"}}}},"responses":{"201":{"description":"The created status page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusPage"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/status-pages/{id}":{"patch":{"tags":["Status pages"],"summary":"Update a status page","description":"Update mutable fields of a status page (title, slug, public visibility, uptime display). Requires a `write`-scoped key.","operationId":"updateStatusPage","parameters":[{"name":"id","in":"path","required":true,"description":"Status page UUID.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"The fields to update.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusPageUpdate"}}}},"responses":{"200":{"description":"The updated status page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusPage"}}}},"400":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Status pages"],"summary":"Delete a status page","description":"Delete a status page and its components. Requires a `write`-scoped key.","operationId":"deleteStatusPage","parameters":[{"name":"id","in":"path","required":true,"description":"Status page UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted — no content."},"401":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Error — canonical envelope with a trace_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"mk_live_…","description":"A Merlonix API key. Create one under Settings → API keys."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","trace_id"],"properties":{"code":{"type":"string","description":"Stable machine-readable error code."},"message":{"type":"string","description":"Human-readable explanation."},"trace_id":{"type":"string","description":"Request trace id — quote it in support tickets."},"details":{"type":"array","description":"Per-field validation issues, when applicable.","items":{"type":"object","properties":{"path":{"type":"string"},"issue":{"type":"string"}}}}}}}},"PageInfo":{"type":"object","description":"Pagination cursor. Pass `next_cursor` back as the `cursor` query param to fetch the next page.","required":["next_cursor","has_more","limit"],"properties":{"next_cursor":{"type":["string","null"],"description":"Opaque cursor for the next page, or null when exhausted."},"has_more":{"type":"boolean","description":"True when more rows are available."},"limit":{"type":"integer","description":"Page size applied to this response."}}},"Asset":{"type":"object","required":["id","tenant_id","attestation_id","asset_type","hostname","label","client_name","client_id","check_interval_sec","ssl_enabled","dns_enabled","uptime_enabled","domain_enabled","dnsbl_enabled","dns_security_enabled","security_headers_enabled","email_auth_enabled","ct_watch_enabled","agent_readiness_enabled","mcp_health_enabled","heartbeat_enabled","heartbeat_period_sec","heartbeat_grace_sec","heartbeat_token","port_enabled","port_check_port","psi_enabled","psi_check_url","last_psi_at","health_enabled","health_check_url","health_check_config","crawl_enabled","crawl_root_url","last_crawl_at","injection_scan_enabled","injection_scan_last_at","uptime_check_url","uptime_check_config","is_active","paused_reason","vendor_dependencies","created_at","updated_at","last_checked_at"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"attestation_id":{"type":["string","null"],"format":"uuid"},"asset_type":{"type":"string","enum":["domain","hostname","certificate","dns_record"]},"hostname":{"type":"string"},"label":{"type":["string","null"]},"client_name":{"type":["string","null"]},"client_id":{"type":["string","null"],"format":"uuid"},"check_interval_sec":{"type":"integer"},"ssl_enabled":{"type":"boolean"},"dns_enabled":{"type":"boolean"},"uptime_enabled":{"type":"boolean"},"domain_enabled":{"type":"boolean"},"dnsbl_enabled":{"type":"boolean"},"dns_security_enabled":{"type":"boolean"},"security_headers_enabled":{"type":"boolean"},"email_auth_enabled":{"type":"boolean"},"ct_watch_enabled":{"type":"boolean"},"agent_readiness_enabled":{"type":"boolean"},"mcp_health_enabled":{"type":"boolean"},"heartbeat_enabled":{"type":"boolean"},"heartbeat_period_sec":{"type":["integer","null"]},"heartbeat_grace_sec":{"type":["integer","null"]},"heartbeat_token":{"type":["string","null"]},"port_enabled":{"type":"boolean"},"port_check_port":{"type":["integer","null"]},"psi_enabled":{"type":"boolean"},"psi_check_url":{"type":["string","null"]},"last_psi_at":{"type":["string","null"],"format":"date-time"},"health_enabled":{"type":"boolean"},"health_check_url":{"type":["string","null"]},"health_check_config":{"type":["object","null"],"description":"TD-941 application-health monitor config ({ headers?, stale_after_sec? }); null when not configured."},"crawl_enabled":{"type":"boolean"},"crawl_root_url":{"type":["string","null"]},"last_crawl_at":{"type":["string","null"],"format":"date-time"},"injection_scan_enabled":{"type":"boolean"},"injection_scan_last_at":{"type":["string","null"],"format":"date-time"},"uptime_check_url":{"type":["string","null"]},"uptime_check_config":{"type":["object","null"],"description":"TD-935 structured uptime probe config (method/headers/body/expected_status/keyword_match/expected_final_url); null when uptime is not configured."},"is_active":{"type":"boolean"},"paused_reason":{"type":["string","null"]},"vendor_dependencies":{"type":"array","items":{"type":"string","enum":["stripe","cloudflare","aws","openai","anthropic","vercel","github","shopify","slack","google_cloud","azure"]}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"last_checked_at":{"type":["string","null"],"format":"date-time"}}},"AssetCreate":{"type":"object","description":"Request body for creating a monitored asset (TD-925).","required":["asset_type","hostname"],"properties":{"asset_type":{"type":"string","enum":["domain","hostname","certificate","dns_record"]},"hostname":{"type":"string","description":"The hostname / domain to monitor."},"attestation_id":{"type":"string","format":"uuid","description":"Optional ownership attestation; must belong to the tenant."},"label":{"type":"string","maxLength":120,"description":"Optional human label."},"client_name":{"type":"string","maxLength":120},"client_id":{"type":"string","format":"uuid","description":"Optional client bucket; defaults to Unassigned."},"check_interval_sec":{"type":"integer","minimum":60,"maximum":86400,"description":"Check cadence. Clamped up to the plan minimum if faster than the tier allows."},"ssl_enabled":{"type":"boolean"},"dns_enabled":{"type":"boolean"},"domain_enabled":{"type":"boolean","description":"Opt into monitored domain-expiry (RDAP) checks."},"dnsbl_enabled":{"type":"boolean","description":"Opt into monitored DNS-blocklist (DNSBL) checks."},"dns_security_enabled":{"type":"boolean","description":"Opt into monitored DNSSEC + DANE/TLSA posture checks (informational)."},"security_headers_enabled":{"type":"boolean","description":"Opt into the monitored HTTP security-header check (re-grades the six core headers on cadence + alerts when one disappears; informational)."},"email_auth_enabled":{"type":"boolean","description":"Opt into the monitored email-auth (SPF/DMARC/MTA-STS/BIMI) posture check."},"ct_watch_enabled":{"type":"boolean","description":"Opt into Certificate-Transparency rogue/shadow-cert monitoring. Paid monitoring tiers only (team/agency/compliance)."},"agent_readiness_enabled":{"type":"boolean","description":"Opt into monitored Agent-Readiness checks."},"mcp_health_enabled":{"type":"boolean","description":"Opt into monitored MCP-server health checks (discovers the MCP endpoint from /.well-known/mcp.json and runs a real JSON-RPC handshake + tools/list; alerts on downtime and tool/schema drift)."},"heartbeat_enabled":{"type":"boolean","description":"Opt into the inbound heartbeat (dead-man's-switch) monitor. The ingest token is server-minted on first enable."},"heartbeat_period_sec":{"type":"integer","minimum":1,"description":"Expected heartbeat interval in seconds."},"heartbeat_grace_sec":{"type":"integer","minimum":0,"description":"Tolerance in seconds before a missed beat is declared dead."},"port_enabled":{"type":"boolean","description":"Opt into the outbound TCP-port monitor."},"port_check_port":{"type":"integer","minimum":1,"maximum":65535,"description":"TCP port to probe on the asset's hostname."},"psi_enabled":{"type":"boolean","description":"Opt into the PageSpeed Insights (Lighthouse) performance monitor. Paid monitoring tiers only (team/agency/compliance)."},"psi_check_url":{"type":"string","format":"uri","description":"Optional override URL to measure with PageSpeed Insights (defaults to https://<hostname>)."},"health_enabled":{"type":"boolean","description":"Opt into the application-health monitor (polls a JSON health endpoint). Paid monitoring tiers only (team/agency/compliance)."},"health_check_url":{"type":"string","format":"uri","description":"JSON health endpoint to poll (laravel-health / Oh Dear shape, with a generic fallback). SSRF-guarded."},"health_check_config":{"type":"object","description":"TD-941 application-health config. headers: auth header(s) sent with the GET (≤10, envelope headers rejected, values never logged); stale_after_sec: freshness window in seconds (30..86400, default 600).","properties":{"headers":{"type":"object","additionalProperties":{"type":"string","maxLength":2048}},"stale_after_sec":{"type":"integer","minimum":30,"maximum":86400}}},"crawl_enabled":{"type":"boolean","description":"Opt into the broken-links / mixed-content crawler. Paid monitoring tiers only (team/agency/compliance)."},"crawl_root_url":{"type":"string","format":"uri","description":"Optional crawl root URL (defaults to https://<hostname>)."},"injection_scan_enabled":{"type":"boolean","description":"Opt into the prompt-injection / hidden-text scan (computed-DOM render). Paid monitoring tiers only (team/agency/compliance)."},"uptime_check_url":{"type":"string","format":"uri","description":"Enables HTTP uptime checks when set (GET probe). For method/headers/body/redirect assertions use uptime_check_config."},"uptime_check_config":{"type":"object","description":"TD-935 structured uptime probe (wins over uptime_check_url). method=GET|HEAD|POST|PUT|PATCH; optional custom headers (≤20, forbidden envelope headers rejected), body (≤8KiB, body-bearing methods only), expected_status, keyword_match, and expected_final_url (redirect-target assertion — DOWN if the final URL after one redirect hop differs). Timing reported: ttfb_ms (headers received) and response_time_ms (total/body read). Cloudflare Workers fetch() exposes no DNS/TCP/TLS phase breakdown, so those are not reported.","required":["check_url"],"properties":{"check_url":{"type":"string","format":"uri"},"method":{"type":"string","enum":["GET","HEAD","POST","PUT","PATCH"]},"expected_status":{"type":"integer","minimum":100,"maximum":599},"keyword_match":{"type":["string","null"],"maxLength":200},"timeout_ms":{"type":"integer","minimum":1000,"maximum":30000},"headers":{"type":["object","null"],"additionalProperties":{"type":"string","maxLength":1024}},"body":{"type":["string","null"],"maxLength":8192},"expected_final_url":{"type":["string","null"],"format":"uri"}}},"vendor_dependencies":{"type":"array","maxItems":11,"items":{"type":"string","enum":["stripe","cloudflare","aws","openai","anthropic","vercel","github","shopify","slack","google_cloud","azure"]}}}},"AssetUpdate":{"type":"object","description":"Request body for updating an asset (all fields optional; TD-925).","properties":{"label":{"type":["string","null"]},"client_name":{"type":["string","null"]},"client_id":{"type":["string","null"],"format":"uuid"},"check_interval_sec":{"type":"integer","minimum":60,"maximum":86400},"ssl_enabled":{"type":"boolean"},"dns_enabled":{"type":"boolean"},"domain_enabled":{"type":"boolean","description":"Toggle monitored domain-expiry (RDAP) checks."},"dnsbl_enabled":{"type":"boolean","description":"Toggle monitored DNS-blocklist (DNSBL) checks."},"dns_security_enabled":{"type":"boolean","description":"Toggle monitored DNSSEC + DANE/TLSA posture checks (informational)."},"security_headers_enabled":{"type":"boolean","description":"Toggle the monitored HTTP security-header check (informational)."},"email_auth_enabled":{"type":"boolean","description":"Toggle the monitored email-auth (SPF/DMARC/MTA-STS/BIMI) posture check."},"ct_watch_enabled":{"type":"boolean","description":"Toggle Certificate-Transparency rogue/shadow-cert monitoring. Paid monitoring tiers only (team/agency/compliance)."},"agent_readiness_enabled":{"type":"boolean","description":"Toggle monitored Agent-Readiness checks."},"mcp_health_enabled":{"type":"boolean","description":"Toggle monitored MCP-server health checks."},"heartbeat_enabled":{"type":"boolean","description":"Toggle the inbound heartbeat (dead-man's-switch) monitor."},"heartbeat_period_sec":{"type":"integer","minimum":1,"description":"Expected heartbeat interval in seconds."},"heartbeat_grace_sec":{"type":"integer","minimum":0,"description":"Tolerance in seconds before a missed beat is declared dead."},"port_enabled":{"type":"boolean","description":"Toggle the outbound TCP-port monitor."},"port_check_port":{"type":"integer","minimum":1,"maximum":65535,"description":"TCP port to probe on the asset's hostname."},"psi_enabled":{"type":"boolean","description":"Toggle the PageSpeed Insights (Lighthouse) performance monitor. Paid monitoring tiers only (team/agency/compliance)."},"psi_check_url":{"type":["string","null"],"format":"uri","description":"Optional override URL to measure with PageSpeed Insights; null clears it."},"health_enabled":{"type":"boolean","description":"Toggle the application-health monitor. Paid monitoring tiers only (team/agency/compliance)."},"health_check_url":{"type":["string","null"],"format":"uri","description":"JSON health endpoint to poll; null clears it."},"health_check_config":{"type":["object","null"],"description":"TD-941 application-health config ({ headers?, stale_after_sec? }); null clears it."},"crawl_enabled":{"type":"boolean","description":"Toggle the broken-links / mixed-content crawler. Paid monitoring tiers only (team/agency/compliance)."},"crawl_root_url":{"type":["string","null"],"format":"uri","description":"Optional crawl root URL; null clears the override."},"injection_scan_enabled":{"type":"boolean","description":"Toggle the prompt-injection / hidden-text scan. Paid monitoring tiers only (team/agency/compliance)."},"uptime_check_url":{"type":["string","null"],"format":"uri"},"is_active":{"type":"boolean"},"paused_reason":{"type":["string","null"],"maxLength":500},"vendor_dependencies":{"type":"array","maxItems":11,"items":{"type":"string","enum":["stripe","cloudflare","aws","openai","anthropic","vercel","github","shopify","slack","google_cloud","azure"]}}}},"AssetBulkCreate":{"type":"object","description":"Bulk asset-creation request — 1 to 100 assets (TD-925).","required":["assets"],"properties":{"assets":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/AssetCreate"}}}},"AssetBulkError":{"type":"object","description":"A single failed row in a bulk create.","required":["index","error"],"properties":{"index":{"type":"integer","description":"Zero-based position in the request `assets` array."},"hostname":{"type":"string","description":"The hostname that failed (when known)."},"error":{"type":"string","description":"Human-readable failure reason."}}},"AssetBulkSkipped":{"type":"object","description":"A bulk row skipped because the asset already existed (TD-962: bulk is idempotent — a re-sent batch inserts 0 duplicate rows).","required":["index","hostname","reason"],"properties":{"index":{"type":"integer","description":"Zero-based position in the request `assets` array."},"hostname":{"type":"string","description":"The hostname that already existed."},"reason":{"type":"string","description":"Always 'already_exists'."}}},"AssetBulkResult":{"type":"object","description":"Result of a bulk asset-creation request (TD-925).","required":["requested","created_count","error_count","skipped_count","created","errors","skipped"],"properties":{"requested":{"type":"integer","description":"Number of assets in the request."},"created_count":{"type":"integer"},"error_count":{"type":"integer"},"skipped_count":{"type":"integer","description":"Rows skipped because the asset already existed (idempotent retry)."},"created":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"errors":{"type":"array","items":{"$ref":"#/components/schemas/AssetBulkError"}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/AssetBulkSkipped"}}}},"SslCheck":{"type":"object","required":["kind","id","tenant_id","asset_id","checked_at","status","not_before","not_after","days_until_expiry","issuer","subject","response_ms","tls_version","cipher_suite","cipher_strength","chain_length","chain_complete","chain_issues","posture_observed","posture_unobserved_reason"],"properties":{"kind":{"type":"string","enum":["ssl"]},"id":{"type":"integer"},"tenant_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"checked_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["valid","expiring_soon","expired","invalid","unreachable","error","inconclusive"]},"not_before":{"type":["string","null"],"format":"date-time"},"not_after":{"type":["string","null"],"format":"date-time"},"days_until_expiry":{"type":["integer","null"]},"issuer":{"type":["string","null"]},"subject":{"type":["string","null"]},"response_ms":{"type":["integer","null"]},"tls_version":{"type":["string","null"]},"cipher_suite":{"type":["string","null"]},"cipher_strength":{"type":["string","null"],"enum":["strong","acceptable","weak",null]},"chain_length":{"type":["integer","null"]},"chain_complete":{"type":["boolean","null"]},"chain_issues":{"type":["array","null"],"items":{"type":"string"}},"posture_observed":{"type":["boolean","null"]},"posture_unobserved_reason":{"type":["string","null"]}}},"DnsCheck":{"type":"object","required":["kind","id","tenant_id","asset_id","checked_at","record_type","resolver","records_found","drift_detected","drift_summary","status","response_ms","cname_observed","cname_chain","cname_target"],"properties":{"kind":{"type":"string","enum":["dns"]},"id":{"type":"integer"},"tenant_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"checked_at":{"type":"string","format":"date-time"},"record_type":{"type":"string"},"resolver":{"type":"string"},"records_found":{"description":"Resolved records — shape varies by record_type."},"drift_detected":{"type":"boolean"},"drift_summary":{"type":["string","null"]},"status":{"type":"string","enum":["ok","drift","nxdomain","servfail","timeout","error"]},"response_ms":{"type":["integer","null"]},"cname_observed":{"type":"boolean","description":"Whether this check observed the CNAME delegation at all. False on any row written before 2026-08-19 — it does NOT mean the hostname publishes no CNAME."},"cname_chain":{"type":"array","description":"Hops from the monitored hostname to the name that finally answered. Empty when observed and none is published.","items":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string"},"to":{"type":"string"}}}},"cname_target":{"type":["string","null"],"description":"The first hop target — the CNAME record published in the customer own zone, and the only hop delegation alerts key on."}}},"UptimeCheck":{"type":"object","required":["kind","id","tenant_id","asset_id","checked_at","up","status_code","response_time_ms","ttfb_ms","keyword_found","error","consecutive_failures","final_url","redirect_ok"],"properties":{"kind":{"type":"string","enum":["uptime"]},"id":{"type":"integer"},"tenant_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"checked_at":{"type":"string","format":"date-time"},"up":{"type":"boolean"},"status_code":{"type":["integer","null"]},"response_time_ms":{"type":"integer","description":"Total round-trip (body fully read), ms."},"ttfb_ms":{"type":["integer","null"],"description":"Time-to-first-byte (response headers received), ms; null when unmeasured. No DNS/TCP/TLS phase breakdown is available on CF Workers."},"keyword_found":{"type":["boolean","null"]},"error":{"type":["string","null"]},"consecutive_failures":{"type":"integer"},"final_url":{"type":["string","null"],"description":"Final URL after at most one redirect hop (TD-935)."},"redirect_ok":{"type":["boolean","null"],"description":"Redirect-target assertion outcome; null when not configured."}}},"Alert":{"type":"object","required":["id","tenant_id","asset_id","vendor_slug","source_check_table","source_check_id","severity","title","summary","ai_classification","ai_confidence","recommended_action","ai_rationale","fired_at","acknowledged_at","acknowledged_by","resolved_at","resolved_by","suppressed_until","dedupe_key","status"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"asset_id":{"type":["string","null"],"format":"uuid"},"vendor_slug":{"type":["string","null"]},"source_check_table":{"type":"string","enum":["ssl_checks","dns_checks","vendor_status_snapshots","pipeline_failures","uptime_checks","domain_checks","dnsbl_checks","agent_readiness_checks","heartbeat_checks","port_checks","psi_checks","health_checks","crawl_runs","injection_scan_results","dns_security_checks","security_header_checks","email_auth_checks","ct_watch_checks","mcp_health_checks","journey_runs","answer_presence_results"]},"source_check_id":{"type":"integer"},"severity":{"type":"string","enum":["info","warning","critical"]},"title":{"type":"string"},"summary":{"type":"string"},"ai_classification":{"type":["string","null"],"enum":["real_issue","false_positive","informational","uncertain",null]},"ai_confidence":{"type":["number","null"],"minimum":0,"maximum":1},"recommended_action":{"type":["string","null"],"description":"TD-1731/TD-1732: deterministic \"what to do\" hint enum (humanize client-side); null when none."},"ai_rationale":{"type":["string","null"],"description":"TD-1732: classifier prose \"why\"; null when none."},"fired_at":{"type":"string","format":"date-time"},"acknowledged_at":{"type":["string","null"],"format":"date-time"},"acknowledged_by":{"type":["string","null"],"format":"uuid"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by":{"type":["string","null"],"format":"uuid"},"suppressed_until":{"type":["string","null"],"format":"date-time"},"dedupe_key":{"type":"string"},"status":{"type":"string","enum":["open","acknowledged","muted","resolved"]}}},"Channel":{"type":"object","description":"An alert notification channel (mirrors AlertChannelPublicSchema).","required":["id","tenant_id","kind","name","target","signing_secret_preview","is_active","severity_filter","created_at","last_delivery"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["email","slack_webhook","generic_webhook","teams_webhook","discord_webhook","telegram","google_chat","pagerduty","opsgenie","sms"]},"name":{"type":"string"},"target":{"type":"string","description":"Destination; shape depends on kind (email / webhook URL / token:chat / routing key / E.164)."},"signing_secret_preview":{"type":["string","null"],"description":"First 6 chars of the webhook signing secret; null for email + slack channels."},"is_active":{"type":"boolean"},"severity_filter":{"type":"array","items":{"type":"string","enum":["info","warning","critical"]}},"created_at":{"type":"string","format":"date-time"},"last_delivery":{"type":["object","null"],"description":"TD-1220: most-recent alert delivery to this channel; null when none.","properties":{"status":{"type":"string"},"at":{"type":"string","format":"date-time"},"error_message":{"type":["string","null"]}}}}},"ChannelCreate":{"type":"object","description":"Request body for creating a notification channel (TD-943).","required":["kind","name","target"],"properties":{"kind":{"type":"string","enum":["email","slack_webhook","generic_webhook","teams_webhook","discord_webhook","telegram","google_chat","pagerduty","opsgenie","sms"]},"name":{"type":"string","minLength":1,"maxLength":100},"target":{"type":"string","minLength":1,"maxLength":500,"description":"email address for kind=email; https webhook URL for slack/teams/discord/google_chat/generic; \"<bot_token>:<chat_id>\" for telegram; routing/integration key for pagerduty; GenieKey for opsgenie; E.164 phone (+15551234567) for sms. Validated per-kind."},"severity_filter":{"type":"array","minItems":1,"description":"Severities that page this channel (default: all three).","items":{"type":"string","enum":["info","warning","critical"]}}}},"ChannelUpdate":{"type":"object","description":"Request body for updating a channel — at least one field required (TD-943).","minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"target":{"type":"string","minLength":1,"maxLength":500},"is_active":{"type":"boolean"},"severity_filter":{"type":"array","minItems":1,"items":{"type":"string","enum":["info","warning","critical"]}}}},"Client":{"type":"object","required":["id","tenant_id","slug","name","logo_url","brand_color","is_unassigned","asset_count","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"logo_url":{"type":["string","null"]},"brand_color":{"type":["string","null"]},"is_unassigned":{"type":"boolean","description":"True for the catch-all Unassigned bucket."},"asset_count":{"type":"integer","minimum":0},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ClientCreate":{"type":"object","description":"Request body for creating a client bucket (TD-943).","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string","description":"URL-safe slug; derived from name when omitted. Unique per tenant."},"logo_url":{"type":"string","format":"uri","description":"https logo URL."},"brand_color":{"type":"string","description":"Hex color (e.g. #1a2b3c)."}}},"ClientUpdate":{"type":"object","description":"Request body for updating a client (all fields optional; TD-943).","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string"},"logo_url":{"type":["string","null"],"format":"uri"},"brand_color":{"type":["string","null"]}}},"StatusPage":{"type":"object","required":["id","tenant_id","client_id","slug","title","is_public","show_uptime","list_in_public_directory","custom_domain","display_timezone","component_count","public_url","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"title":{"type":"string"},"is_public":{"type":"boolean"},"show_uptime":{"type":"boolean"},"list_in_public_directory":{"type":"boolean"},"custom_domain":{"type":["string","null"]},"display_timezone":{"type":["string","null"]},"component_count":{"type":"integer","minimum":0},"public_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"StatusPageCreate":{"type":"object","description":"Request body for creating a status page (TD-943).","required":["client_id"],"properties":{"client_id":{"type":"string","format":"uuid","description":"The client this status page belongs to."},"title":{"type":"string","minLength":1,"maxLength":160,"description":"Display title; defaults from the client name."},"slug":{"type":"string","description":"URL-safe slug; derived when omitted. Unique per tenant."}}},"StatusPageUpdate":{"type":"object","description":"Request body for updating a status page (all fields optional; TD-943).","properties":{"title":{"type":"string","minLength":1,"maxLength":160},"slug":{"type":"string"},"is_public":{"type":"boolean"},"show_uptime":{"type":"boolean"},"list_in_public_directory":{"type":"boolean"}}}}}}