{"openapi":"3.1.0","info":{"title":"canopy public-server","description":"Internet-facing API for the canopy fleet. Device-authenticated endpoints require an mTLS client certificate.\n\nRequest bodies may be sent compressed, and this is recommended for large payloads: the server transparently decodes `Content-Encoding: gzip`, `br`, `deflate`, or `zstd`. The accepted encodings are also listed structurally in the `x-request-compression` extension.","contact":{"name":"BES Developers","email":"contact@bes.au"},"license":{"name":"GPL-3.0-or-later"},"version":""},"paths":{"/artifacts/{version}/{artifact_type}/{platform}":{"post":{"tags":["artifacts"],"summary":"Register a downloadable artifact for a version or version range.","description":"Requires a device certificate with the releaser role (or admin). The\npath identifies the version the artifact belongs to — either an exact\nversion (e.g. `2.10.5`) or a semver range pattern (e.g. `2.10.x`,\n`^2.10.0`) — followed by the artifact's type and target platform. The\nrequest body is the plain-text URL clients should download the\nartifact from.\n\nWhen an exact version is given and it doesn't exist yet, it is created\nautomatically as an unpublished draft so the artifact has a version to\nattach to; publishing that version later (via the version-creation\nendpoint) is a separate step. When a range pattern is given instead,\nthe artifact isn't tied to one version — it matches whichever\npublished version currently satisfies the range at lookup time.\n\nReturns the created artifact record. Returns 400 if the version or\nrange syntax can't be parsed.","operationId":"register_artifact","parameters":[{"name":"version","in":"path","description":"Exact semver (e.g. `2.10.5`) or range pattern (e.g. `2.10.x`, `^2.10.0`).","required":true,"schema":{"type":"string"}},{"name":"artifact_type","in":"path","required":true,"schema":{"type":"string"}},{"name":"platform","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Download URL for the artifact, as a plain-text body.","content":{"text/plain":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Artifact"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"releaser-device":[]}]}},"/backup-capabilities":{"post":{"tags":["backup"],"summary":"Register the backup types this server can run.","description":"Declares the set of backup types the calling device is able to execute on\nits server. Types not seen before are added to the server's capability set,\nstarting out enabled or disabled according to the fleet-wide default for\nthat type (disabled if the type has no default configured). Types already\nregistered keep whatever enabled/disabled state an operator has set for\nthem, so re-registering on every startup is safe and expected.\n\nOnly types that are registered here (and enabled) can later be issued\ncredentials via `POST /backup-credentials`, outside of explicit\noperator-requested runs.\n\nErrors: 412 when the calling device is not bound to a live server; 409 when\nthe server is not in a group.","operationId":"register_backup_capabilities","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackupCapabilitiesArgs"}}},"required":true},"responses":{"204":{"description":"Capabilities registered."},"409":{"description":"Server is not in a group.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"Device is not bound to a live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/backup-credentials":{"post":{"tags":["backup"],"summary":"Mint short-lived S3 credentials for a backup or restore run.","description":"Issues temporary AWS credentials scoped to the backup storage of the\ncalling server's group, in the `credential_process` output format. With\n`purpose: backup` the credentials can upload and manage backup data but\ncannot destroy existing backups; with `purpose: restore` they are strictly\nread-only. They expire after at most one hour, so request a fresh set for\neach run rather than caching them. Every issuance is recorded for audit.\n\nThe storage coordinates the credentials apply to (bucket, prefix, region)\ncome from `GET /backup-target`.\n\nErrors: 409 when the server is not in a group, when the group's backup\nconfiguration is not ready, when a `backup` type is neither an enabled\ncapability of this server nor the subject of a pending \"backup now\" request,\nor when a `restore` is requested but the server's restore window is not\nopen; 412 when the device is not bound to a live server; 502 when the\ncredential issuer is unavailable or not configured.","operationId":"mint_backup_credentials","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackupCredentialsArgs"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialProcessOutput"}}}},"409":{"description":"Server ungrouped, no ready config, backup type not enabled, or restore window not open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"Device is not bound to a live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"502":{"description":"STS issuance failed or is not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/backup-progress":{"post":{"tags":["backup"],"summary":"Report progress for a run that is still in flight.","description":"Optional throughout: a run that never reports progress is recorded and\ndisplayed exactly as it is today. Reporting it lets Canopy show how far a\nlong-running backup has got, at what rate, and when it last heard from the\ndevice — which for a multi-hour backup is the difference between \"running\"\nand \"running, and moving\".\n\n**Every counter is cumulative from the start of the run**, not an interval\ndelta. Send totals-so-far each time. A dropped or repeated report then costs\nonly resolution, never the accuracy of a total, and the last report Canopy\nreceived can stand in for a figure the final report omits. Omit any counter\nyou do not measure rather than sending zero.\n\nCanopy timestamps each report on receipt, so no clock agreement is needed —\nexcept for `snapshot_taken_at`, which is necessarily the device's own claim\nabout its filesystem.\n\nUnlike `POST /backup-credentials`, this does not require the group's backup\nconfiguration to be ready or the type to be an enabled capability: it\ndescribes a run already under way, and refusing it would blind Canopy exactly\nwhen something is misconfigured.\n\nA refused report is never a reason to abandon a run — this is telemetry.\nReporting progress for a run that has already been reported complete is\naccepted rather than refused, so a report racing the completion is not an\nerror.\n\nErrors: 412 when the calling device is not bound to a live server; 409 when\nthe server is not in a group; 429 when reporting faster than Canopy accepts.","operationId":"report_backup_progress","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgressArgs"}}},"required":true},"responses":{"204":{"description":"Progress recorded."},"409":{"description":"Server is not in a group.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"Device is not bound to a live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"429":{"description":"Reporting progress too frequently.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/backup-report":{"post":{"tags":["backup"],"summary":"Report the outcome of a backup or restore run.","description":"Records the run against the calling server and its group. Send one report\nper run, on success and on failure alike. Reporting also clears any pending\noperator-requested run for the same type and purpose — regardless of\noutcome, since an operator request is for one attempt — so the server's\nstatus responses stop asking for it (see the `backup_now` field of the\nstatus-push response).\n\nErrors: 409 when the server is not in a group, or when the `run_id` has\nalready been reported; 412 when the device is not bound to a live server.","operationId":"report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportArgs"}}},"required":true},"responses":{"204":{"description":"Run recorded."},"409":{"description":"Server ungrouped, or duplicate run_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"Device is not bound to a live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/backup-target":{"get":{"tags":["backup"],"summary":"Fetch the backup storage target for this server's group.","description":"Returns the bucket, prefix, region, and repository passphrase the device\nneeds to connect to its group's backup repository. Call it on every run\nrather than caching the result, as the target can change. S3 credentials\nare obtained separately via `POST /backup-credentials`.\n\nErrors: 409 when the server is not in a group or the group's backup\nconfiguration is not ready; 412 when the device is not bound to a live\nserver; 502 when the passphrase store is unavailable or not configured.","operationId":"target","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackupTarget"}}}},"409":{"description":"Server ungrouped or no ready config.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"Device is not bound to a live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"502":{"description":"Repo-password Secret unavailable or kube not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/bestool/snippets":{"get":{"tags":["bestool"],"summary":"List all current bestool SQL snippets.","description":"Returns the library of named SQL snippets that devices running bestool\nfetch and run, keyed by snippet name. Only the current version of each\nsnippet is included: if a snippet has been superseded by a newer one\nunder the same name, only the newer version is returned, and\nsoft-deleted snippets are omitted entirely. This endpoint does not\nrequire device authentication.","operationId":"list_snippets","responses":{"200":{"description":"Current bestool snippets, keyed by name.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SnippetResponse"},"propertyNames":{"type":"string"}}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}}}},"/certificates/request":{"post":{"tags":["certificates"],"summary":"Ask for a certificate, and collect it once there is one.","description":"The same call does both, and is safe to repeat: a name and key Canopy already\nholds a certificate for is answered from what it holds rather than ordering\nagain, so a server that lost its local copy costs the authority nothing. A\nrequest naming a different key opens a new order.\n\nProving control of a name through DNS takes far longer than any client waits\nmid-handshake, so a first request records the order and answers `pending`;\ncall again to collect. A server is expected to hold a certificate before it\nneeds one rather than to obtain one while a client waits.","operationId":"certificate_request","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCertificateArgs"}}},"required":true},"responses":{"200":{"description":"The order as it stands, with the chain if there is one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificateResponse"}}}},"400":{"description":"The signing request is unparseable, unsigned, asks for another name, or carries a name besides the one requested.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"403":{"description":"The server lacks the TLS grant, or the name is not within its group's domains.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"409":{"description":"The server is paused, no managed zone covers the name, or the key was revoked as compromised and will not be certified again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"The device is not attached to any live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"mtls-certificate":[]}]}},"/names/entitlements":{"get":{"tags":["names"],"summary":"What this server may act on, and what it already holds.","description":"Answers the boundary rather than making an agent discover it by being\nrefused: the domains its group controls, the grants it holds, whether it is\npaused, and the names and certificates it already has. Enough to request a\ncertificate before anything asks for one, and to renew before expiry.\n\nA server with no grants, or whose group controls no domain, gets an empty\nanswer rather than an error — asking what one may do is not a privileged act.\nThe same content rides on the response to a status push.","operationId":"name_entitlements","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}},"412":{"description":"The device is not attached to any live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"mtls-certificate":[]}]}},"/names/register":{"post":{"tags":["names"],"summary":"Register the addresses a name should resolve to.","description":"Replaces whatever addresses were registered for the name; an empty list\nwithdraws it. Canopy publishes what it is told — it does not verify that an\naddress is really this server's, the grant being the trust boundary.\n\nPublishing happens in the background, so the response says what Canopy will\npublish and what it has published so far rather than waiting for the zone.","operationId":"name_register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterNameArgs"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisteredName"}}}},"403":{"description":"The server lacks the DNS grant, or the name is not within its group's domains.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"409":{"description":"The server is paused, another server holds the name, or no managed zone covers it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"The device is not attached to any live server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"mtls-certificate":[]}]}},"/restore-capabilities":{"post":{"tags":["restore"],"summary":"Register the restore intents this device can satisfy.","description":"Declares the restore intents the calling device supports, replacing any\npreviously advertised set. Only worklist entries whose intent is currently\nadvertised are dispatched to this device via `GET /restore-worklist`, so\nregister on startup and whenever the supported set changes.","operationId":"register_restore_capabilities","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestoreCapabilitiesArgs"}}},"required":true},"responses":{"204":{"description":"Capability set registered."}},"security":[{"backup-restore-device":[]}]}},"/restore-credentials":{"post":{"tags":["restore"],"summary":"Mint read-only credentials for a group's backup repository.","description":"Issues temporary AWS credentials — always strictly read-only, scoped to the\ngroup's backup storage — together with the repository passphrase, so the\ndevice can read the snapshot named in a worklist entry. Credentials expire\nafter at most one hour; request a fresh set per restore rather than caching\nthem. Every issuance is recorded for audit.\n\nThe device must hold an enabled restore declaration covering the requested\ngroup and type (i.e. the pair must appear in its worklist configuration);\notherwise the request is rejected with 403.\n\nErrors: 403 when no enabled declaration authorizes this group and type;\n409 when the group has no ready backup configuration; 502 when the\ncredential issuer or the passphrase store is unavailable or not configured.","operationId":"mint_restore_credentials","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestoreCredentialsArgs"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestoreCredentials"}}}},"403":{"description":"No enabled declaration authorizes this (group, type).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"409":{"description":"Group has no ready backup config.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"502":{"description":"STS issuance or repo-password read failed or is not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"backup-restore-device":[]}]}},"/restore-verification":{"post":{"tags":["restore"],"summary":"Report the outcome of a restore attempt and the replica's health.","description":"Records a verification report for a restore the device performed from its\nworklist. Send one report per attempt, on success and on failure alike. A\nreport with a `success` outcome and `replica_healthy: true` marks the\nsnapshot as verified; for run-once intents this is what removes the entry\nfrom `GET /restore-worklist` until a newer snapshot appears.\n\nAuthorization matches `POST /restore-credentials`: the device must hold an\nenabled restore declaration covering the reported group and type,\notherwise the request is rejected with 403.","operationId":"verification","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationArgs"}}},"required":true},"responses":{"204":{"description":"Report recorded."},"403":{"description":"No enabled declaration authorizes this (group, type).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"backup-restore-device":[]}]}},"/restore-worklist":{"get":{"tags":["restore"],"summary":"Fetch the full set of replicas this device should maintain.","description":"Returns the device's complete desired state, computed fresh on every call:\neach enabled restore declaration whose intent this device currently\nadvertises, expanded into one entry per server it covers. A group-wide\ndeclaration expands to every live server in its group; a server-scoped\ndeclaration yields a single entry and takes precedence over a group-wide\none covering the same server, type, and intent. Entries for groups whose\nbackup configuration is not ready are omitted, and entries for run-once\nintents disappear once the latest snapshot has a healthy verification\nreport, reappearing when a newer snapshot exists.\n\nAn empty array means there is nothing to do. Poll this endpoint and\nreconcile: create or refresh the replicas listed, and tear down any the\ndevice is maintaining that no longer appear.","operationId":"worklist","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorklistEntry"}}}}}},"security":[{"backup-restore-device":[]}]}},"/servers":{"get":{"tags":["servers"],"summary":"List publicly-listed central servers.","description":"Returns every central server that has both a public display name and a\nreachable host configured, ordered by environment tier (production\nfirst, then clone, demo, test, dev) and then by name. Used by clients\nto let a user pick which server to connect to.","operationId":"list_servers","responses":{"200":{"description":"Publicly-listed central servers, ordered by rank then name.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicServer"}}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}}}},"/servers/register/begin":{"post":{"tags":["servers"],"summary":"Start device enrollment against a server.","description":"Validates the enrollment token against the given server and, if valid,\nissues a short-lived (5 minute) signed challenge bound to the server\nID, the token, and the caller's public key. The device must sign this\nchallenge and submit it to the completion endpoint to finish\nenrollment; the token itself is validated here but not yet consumed.\n\nThis endpoint is rate-limited per source IP and per target server; a\ntripped limit returns 429. Any other failure — an unknown or deleted\nserver, or an invalid or expired token — is surfaced as a generic 403,\ndeliberately not distinguishing which check failed.","operationId":"register_begin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeginArgs"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeginResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}}}},"/servers/register/complete":{"post":{"tags":["servers"],"summary":"Complete device enrollment by presenting a signed challenge.","description":"Verifies the signature over the challenge transcript using the public\nkey supplied here, then binds the device to the server: an existing\ndevice re-enrolling with the same key is reused as-is; a device\nre-enrolling with a different key replaces the server's previous\ndevice (revoking that device's access); otherwise a new device\nidentity is created. On success the device is granted the server\nrole, the enrollment token is consumed, and the server is marked as\nregistered.\n\nEnrollment is refused if the presented public key is already bound to\na different live server. Like the start-enrollment endpoint, this one\nis rate-limited per source IP and per target server (429 on a tripped\nlimit) and reports every other kind of failure as a generic 403.","operationId":"register_complete","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteArgs"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}}}},"/servers/self":{"get":{"tags":["servers"],"summary":"Report the calling device's own identity.","description":"Resolves the caller from its device certificate and returns the server\nit is enrolled as together with its own device ID — the same pair\nreturned when the device completed enrollment. A device authenticates\nentirely from its certificate, so it never needs these IDs to make\ncalls; this endpoint lets one that has lost track of them recover them.\n\n- **401**: the request has no client certificate, or the certificate\n doesn't match a known device.\n- **409**: the calling device is attached to more than one server, which\n should not normally happen; contact support if you see this.\n- **412**: the device is registered but has not yet been attached to a\n server.","operationId":"server_self","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SelfResponse"}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/status/{server_id}":{"post":{"tags":["statuses"],"summary":"Submit a status heartbeat for a server.","description":"Records a periodic status push against the server identified in the\npath: overall self-reported health, a per-check breakdown, and any\nfree-form extra data. Each failed or warning check opens (or keeps\nopen) an issue at that check's operator-configured severity, and each\npassed check closes any issue it previously opened; the server's\ntracked software version is also updated from the payload.\n\nThe calling device must be the one enrolled for this exact server (or\nhold the admin role). The response carries only return-path\ninstructions: a `backup_now` list of backup types the server should\nback up immediately — devices should treat a non-empty list as a\nprompt to run those backups and report them afterwards — a\n`check_severities` map describing how canopy classifies each known\nhealthcheck for this server (`skip`/`warn`/`fail`), and the server's\neffective `tags` (as served by `GET /tags`). The stored status record\nis not echoed back.","operationId":"submit_status","parameters":[{"name":"server_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"Status push. A body without a `health` array is the legacy Tamanu direct-report format, treated as the `tamanu` source reporting a single always-passing `tasks` heartbeat check.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusPayload"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/status/{server_id}/check-severities":{"get":{"tags":["statuses"],"summary":"Fetch the effective healthcheck severity mapping for a server.","description":"Returns, for every healthcheck the `alertd` source reports, how that\ncheck is handled for this server: `skip` (the check is silenced for\nthis server — at server or group scope — or its policy ceiling means it\nnever alerts), `warn` (graded at most a warning), or `fail` (failures\ncount as failures). Keys are check names as reported in\n`health[].check` on status pushes. Only the static policy ceiling is\nreflected; operator-defined conditional rules are evaluated per push\nand not included here. The same mapping also rides along every\nstatus-push response as `check_severities`, scoped to the pushing\nsource.\n\nThe calling device must be the one enrolled for this exact server (or\nhold the admin role).","operationId":"check_severities","parameters":[{"name":"server_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Effective handling for each known check, keyed by check name.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/CheckSeverity"},"propertyNames":{"type":"string"}}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/tags":{"get":{"tags":["tags"],"summary":"Get the tags for the calling device's own server.","description":"Returns the effective set of tags for the server the calling device is\nregistered as: any tags set on the server itself, overlaid onto any tags\ninherited from its server group (a tag set on the server takes precedence\nover a group tag with the same key). If the server isn't in a group,\nthis returns just its own tags.\n\nThe result also includes a few read-only, synthetic tags describing the\nserver, under the reserved `canopy:` key prefix: `canopy:kind`,\n`canopy:rank` (if the server has one set), and `canopy:group-id` /\n`canopy:group-name` (if the server belongs to a group). Operators cannot\nset tags under that prefix, so these never collide with tags you set\nyourself.\n\nWhen the server belongs to a group, the effective `billing.*` labels are\nalso included, matching the labels canopy attributes to cloud resources:\n`billing.product`, `billing.deployment`, and `billing.stage` (the last\nderived from *this* server's own rank, and omitted when the server has no\nrank). The stage is per-server, not the group's highest rank, so a `clone`\nserver reports `billing.stage=clone` rather than the group's `prod`.\n\nThese are only defaults: a stored `billing.*` tag is honoured over the\ncomputed value — the server's own tag first, then the group's. So an\noperator can pin any billing label on a specific server or the whole group.\n\n- **401**: the request has no client certificate, or the certificate\n doesn't match a known device.\n- **409**: the calling device is attached to more than one server, which\n should not normally happen; contact support if you see this.\n- **412**: the device is registered but has not yet been attached to a\n server.","operationId":"get_self","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagMap"}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"412":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"server-device":[]}]}},"/versions":{"get":{"tags":["versions"],"summary":"List published, ready-to-serve versions.","description":"Returns every version currently in the published state, excluding any\nversion a recorded known-issue range still covers (whether that issue\nis still open or has since been fixed in a later patch). Ordered\nnewest first.","operationId":"list_versions","responses":{"200":{"description":"All published versions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Version"}}}}}}}},"/versions/update-for/{version}":{"get":{"tags":["versions"],"summary":"Check for available updates from a given version.","description":"The path parameter is the caller's currently-installed exact version.\nFor each later minor release line within the same major version,\nreturns the latest published version that hasn't been excluded by a\nrecorded known-issue range — falling back to an older ready patch\nwithin that same minor line rather than dropping the line entirely, if\nthe newest patch isn't ready. Clients use this to discover and offer\navailable updates.","operationId":"update_for","parameters":[{"name":"version","in":"path","description":"Currently-installed version (exact semver).","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Published versions above the given one, used by clients to discover updates.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ViewVersion"}}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}}}},"/versions/{version}":{"post":{"tags":["versions"],"summary":"Publish a version with its changelog.","description":"Requires a device certificate with the releaser role (or admin). The\npath parameter is the exact version being published (e.g. `2.10.5`);\nthe request body is the changelog for that version, as up to 1 MiB of\nmarkdown text.\n\nIf the version already exists in the draft state — for example\nbecause an artifact was registered against it before its changelog\nwas written — the draft is published in place, with this changelog\nreplacing whatever it had before. Otherwise a new version is created\ndirectly in the published state. Publishing a version that already\nexists and is not a draft (already published, or yanked) fails.\n\nReturns the resulting version record.","operationId":"create_version","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Changelog markdown text, up to 1 MiB.","content":{"text/plain":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Version"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"releaser-device":[]}]},"delete":{"tags":["versions"],"summary":"Yank a version.","description":"Requires a device certificate with the admin role. Marks the given\nexact version as yanked, hiding it from listings, update checks, and\nartifact lookups without deleting its history.","operationId":"remove","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version marked yanked."},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"401":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}},"security":[{"admin-device":[]}]}},"/versions/{version}/artifacts":{"get":{"tags":["versions"],"summary":"List the artifacts available for a version or version range.","description":"The path parameter accepts either an exact version or a semver range\npattern (e.g. `2.10.x`, `^2.10.0`). It resolves to the latest\npublished, ready version satisfying the input, then returns that\nversion's artifacts — both ones registered against the exact version\nand ones registered against a range pattern that covers it. Returns\n404 if no published, ready version matches.","operationId":"list_artifacts","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Artifacts that match the given exact version or range.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Artifact"}}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetailsSchema"}}}}}}}},"components":{"schemas":{"Artifact":{"type":"object","description":"A downloadable artifact belonging to a release version: an installer,\npackage, or other file published for a given type and platform.","required":["id","artifact_type","platform","download_url"],"properties":{"artifact_type":{"type":"string","description":"What kind of artifact this is (e.g. an installer or package name)."},"device_id":{"type":["string","null"],"format":"uuid","description":"The device that registered this artifact, if it was registered by a\nreleaser device rather than created by an operator."},"download_url":{"type":"string","description":"URL the artifact can be downloaded from."},"id":{"type":"string","format":"uuid","description":"Unique identifier of the artifact."},"platform":{"type":"string","description":"The platform the artifact targets (e.g. an OS or architecture name)."},"version_id":{"type":["string","null"],"format":"uuid","description":"The exact version this artifact belongs to. `null` for range\nartifacts, which apply to every version matching\n`version_range_pattern` instead."},"version_range_pattern":{"type":["string","null"],"description":"Semver range this artifact applies to (e.g. `^2.10.0`), for artifacts\nshared across a range of versions rather than pinned to one. `null`\nfor exact-version artifacts."}}},"BTreeMap":{"type":"object","additionalProperties":{"type":"object","description":"Describes one configurable parameter that a replica of a restore intent\naccepts.","required":["type"],"properties":{"default":{"description":"The value used when the parameter is left unset. `None` means an\nunset parameter is sent as JSON `null` rather than a default value."},"type":{"$ref":"#/components/schemas/ParamType","description":"The parameter's data type, which determines how its value is\nvalidated."}}},"propertyNames":{"type":"string"}},"BackupCapabilitiesArgs":{"type":"object","description":"Request body for registering the backup types a server can run.","required":["types"],"properties":{"types":{"type":"array","items":{"type":"string"},"description":"The backup types this server is able to run. Each type is a plain\nstring (e.g. `tamanu-postgres`); custom type names are accepted."}}},"BackupCredentialsArgs":{"type":"object","description":"Request body for minting short-lived S3 credentials for a backup or\nrestore run.","required":["type"],"properties":{"purpose":{"$ref":"#/components/schemas/BackupPurpose","description":"What the credentials will be used for. `backup` (the default) grants\nwrite access for uploading backups; `restore` grants strictly read-only\naccess. Either way the credentials are scoped to the group's backup\nstorage only."},"run_id":{"type":["string","null"],"format":"uuid","description":"This must be the run-uuid the client minted for this run.\nThe field is optional only so older clients don't break; it WILL be made\nmandatory in future."},"type":{"type":"string","description":"The backup type the credentials are for (e.g. `tamanu-postgres`). For a\n`backup`, the type must be an enabled capability of this server or the\nsubject of a pending \"backup now\" request; for a `restore`, the server's\nrestore window must be open (an operator allows restores for it in\ncanopy). Otherwise the request is rejected with 409."}}},"BackupPurpose":{"type":"string","description":"Why a backup credential was issued, or what a reported run was for.\nDetermines the access the credential grants: a `backup` credential can\nwrite new data but not delete existing data, while a `restore`\ncredential is read-only.","enum":["backup","restore"]},"BackupTarget":{"type":"object","description":"The backup storage target for the calling server's group: where the backup\nrepository lives and the passphrase to open it.","required":["storage","bucket","prefix","region","repo_password"],"properties":{"bucket":{"type":"string","description":"Name of the S3 bucket holding the group's backup repository."},"prefix":{"type":"string","description":"Key prefix within the bucket under which the repository lives. Normally\nempty (the repository is at the bucket root)."},"region":{"type":"string","description":"AWS region of the bucket."},"repo_password":{"type":"string","description":"Passphrase for the group's backup repository (a Kopia repository)."},"storage":{"type":"string","description":"Kind of storage backend. Always `\"s3\"`."}}},"BeginArgs":{"type":"object","description":"Request to start device enrollment against a server.","required":["server_id","token"],"properties":{"server_id":{"type":"string","format":"uuid","description":"ID of the server to enroll against."},"spki":{"type":["string","null"],"description":"Base64-standard-encoded DER SubjectPublicKeyInfo (SPKI) of the\ndevice's public key. Only required when enrolling over a transport\nwith no client certificate to read the key from (e.g. over\nTailscale); omit it when enrolling over mTLS, where the key is\ntaken from the presented client certificate. The returned challenge\nis bound to this key, so the same value must be supplied again when\ncompleting enrollment."},"token":{"type":"string","description":"The enrollment token issued by an operator for this server."}}},"BeginResponse":{"type":"object","description":"A freshly-issued enrollment challenge to sign and return.","required":["nonce","channel_binding_required"],"properties":{"channel_binding_required":{"type":"boolean","description":"True if the server requires channel-binding data (the connection's\nTLS exported keying material) to be folded into the signed\ntranscript. Only relevant when enrolling over mTLS; it never\napplies on a transport without TLS channel binding."},"nonce":{"type":"string","description":"Base64-standard-encoded 32-byte challenge nonce. Sign it, together\nwith the server ID, device public key, and channel-binding data if\nrequired, and submit the signature when completing enrollment."}}},"CertificateResponse":{"type":"object","description":"Where a certificate request stands, and the chain once there is one.","required":["name","state","usable","revoked","key_must_be_replaced"],"properties":{"chain":{"type":["string","null"],"description":"The chain, PEM, once Canopy holds one — including while a renewal is\nunder way, the chain in hand staying valid until the new one lands."},"key_must_be_replaced":{"type":"boolean","description":"Whether the key must be replaced before asking again, rather than just the\ncertificate."},"last_error":{"type":["string","null"],"description":"Why the last attempt failed, if one did. Present while Canopy is still\nretrying."},"name":{"type":"string","description":"The name the certificate is (or will be) for, as Canopy normalised it."},"not_after":{"type":["string","null"],"description":"When it expires."},"profile":{"type":["string","null"],"description":"The profile it was issued under, if the authority named one."},"revoked":{"type":"boolean","description":"Whether an operator revoked it. Stop serving it and ask again."},"state":{"type":"string","description":"`pending`, `issued`, `failed`, or `revoked`."},"usable":{"type":"boolean","description":"Whether the chain can be served now."}}},"CheckResult":{"type":"string","description":"Outcome of a single health check reported in a server's status update.\n\nOlder reports may send a plain pass/fail flag instead of one of these\noutcomes; when that happens a passing flag is treated as `passed` and a\nfailing flag as `failed`.","enum":["passed","warning","failed","broken","skipped"]},"CheckSeverity":{"type":"string","description":"How a server should treat one of its healthchecks, distilled from\ncanopy's operator-side configuration (the policy catalog and the\nsilences) into a three-level device-facing vocabulary.","enum":["skip","warn","fail"]},"CompleteArgs":{"type":"object","description":"Request to complete device enrollment by presenting a signed\nchallenge obtained from the start-enrollment endpoint.","required":["server_id","nonce","signature"],"properties":{"nonce":{"type":"string","description":"Base64-standard-encoded challenge nonce returned when enrollment\nwas started."},"server_id":{"type":"string","format":"uuid","description":"ID of the server being enrolled against. Must match the value used\nwhen starting enrollment."},"signature":{"type":"string","description":"Base64-standard-encoded ASN.1 DER ECDSA (P-256, SHA-256) signature\nover the challenge transcript, proving possession of the device's\nprivate key. The transcript is the byte concatenation of: the raw\nchallenge nonce, the raw 16 bytes of the server ID, the DER SPKI of\nthe device public key, and — only when channel binding was flagged\nas required — the connection's TLS exported keying material."},"spki":{"type":["string","null"],"description":"Base64-standard-encoded DER SPKI of the device's public key. Only\nrequired when enrolling over a transport with no client\ncertificate to read the key from; must match the key used when\nenrollment was started."}}},"CompleteResponse":{"type":"object","description":"Result of a successful enrollment.","required":["server_id","device_id"],"properties":{"device_id":{"type":"string","format":"uuid","description":"The device identity created or reused for this enrollment. The\ndevice authenticates as this ID from now on."},"server_id":{"type":"string","format":"uuid","description":"The server the device is now enrolled against."}}},"CredentialProcessOutput":{"type":"object","description":"Short-lived AWS credentials in the AWS `credential_process` output format,\nso they can be consumed directly by AWS SDKs and tools. Field names use the\nexact casing (`Version`, `AccessKeyId`, ...) that format requires.","required":["Version","AccessKeyId","SecretAccessKey","SessionToken","Expiration"],"properties":{"AccessKeyId":{"type":"string","description":"The temporary AWS access key ID."},"Expiration":{"type":"string","description":"When the credentials expire, as an RFC 3339 / ISO 8601 UTC instant.\nCredentials last at most one hour; request a fresh set per run."},"SecretAccessKey":{"type":"string","description":"The temporary AWS secret access key."},"SessionToken":{"type":"string","description":"The session token that must accompany the temporary key pair."},"Version":{"type":"integer","format":"int32","description":"Version of the `credential_process` format. Always the literal `1`.","minimum":0}}},"Entitlements":{"type":"object","description":"What a server is entitled to do with names, and what it already holds.","required":["may_manage_dns","may_manage_tls","paused","domains","registered_names","certificates"],"properties":{"certificates":{"type":"array","items":{"$ref":"#/components/schemas/HeldCertificate"},"description":"The certificates Canopy holds for this server."},"domains":{"type":"array","items":{"type":"string"},"description":"The domains this server's group controls. Any name at or beneath one of\nthese is a name this server may act on — which is what lets an agent\nrequest a certificate before anything asks for one."},"may_manage_dns":{"type":"boolean","description":"Whether this server may manage its own DNS records."},"may_manage_tls":{"type":"boolean","description":"Whether this server may obtain its own TLS certificates."},"paused":{"type":"boolean","description":"Whether Canopy is currently making no new changes on this server's\nbehalf. While true, requests are refused and an agent should wait."},"registered_names":{"type":"array","items":{"type":"string"},"description":"The names this server has registered addresses for."}}},"HealthCheck":{"allOf":[{"type":"object","description":"Arbitrary additional fields specific to this check (shown in the\nstatus UI as a key/value block, and available to operator-defined\nseverity rules)."},{"type":"object","required":["check"],"properties":{"check":{"type":"string","description":"Name of the check. Must be a non-empty string, and should stay stable\nacross pushes: results for the same name are correlated over time, so\nsuccessive failures and the eventual recovery land on the same issue."},"healthy":{"type":["boolean","null"],"description":"Legacy pass/fail form: `true` means `passed`, `false` means `failed`.\nMutually exclusive with `result`."},"result":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CheckResult","description":"Outcome of the check: `passed`, `warning`, `failed`, `broken`, or\n`skipped`. Exactly one of `result` / `healthy` must be present per\nentry. `warning` and `failed` open the check's issue as graded by\nits policy; `broken` (the check itself errored, not the system under\ntest) neither confirms nor clears a known failure — the issue stays\nopen, retaining its contribution; `skipped` (a precondition was\nnot met) and `passed` open nothing and close prior issues."}]}}}],"description":"One health-check result within a status push."},"HeldCertificate":{"type":"object","description":"A certificate Canopy holds for the asking server, as the server needs to see\nit: enough to decide whether to renew, and nothing about anyone else.","required":["name","key_fingerprint","usable","revoked","key_must_be_replaced"],"properties":{"key_fingerprint":{"type":"string","description":"Hex SHA-256 of the certified key's subject public key info, so an agent\ncan tell whether this covers a key it still holds."},"key_must_be_replaced":{"type":"boolean","description":"Whether the key itself is condemned, not just the certificate — the key\npair has to be replaced before asking again."},"name":{"type":"string","description":"The name it covers."},"not_after":{"type":["string","null"],"description":"When it expires."},"profile":{"type":["string","null"],"description":"The profile it was issued under, if the authority named one."},"revoked":{"type":"boolean","description":"Whether an operator has revoked it. Stop serving it."},"usable":{"type":"boolean","description":"Whether it can still be served: not revoked, not expired. True even while\na renewal is under way, the chain in hand staying valid until the new one\nlands."}}},"IntentDescriptor":{"type":"object","description":"One restore purpose a consumer advertises support for: the behaviours it\nopts into and the settings it accepts per replica.","required":["intent"],"properties":{"description":{"type":["string","null"],"description":"Human-readable description of the intent, if provided."},"intent":{"type":"string","description":"Name of the intent: an arbitrary identifier chosen by the consumer\n(e.g. `verify`); any name may be advertised."},"params":{"$ref":"#/components/schemas/BTreeMap","description":"Configurable parameters this intent accepts per replica, keyed by\nparameter name."},"semantics":{"type":"array","items":{"type":"string"},"description":"Behaviours this intent opts into. Recognised values are `check` (a\nhealth report is expected for each replica), `once` (a given snapshot\nis only ever dispatched to a replica once, rather than repeatedly\nuntil overdue), and `url` (a replica's health report includes a link\nto it). Unrecognised values are stored but have no effect."}}},"MigrationArgs":{"type":"object","description":"How the target version's migrations went against the restored replica.","required":["target_version_id","total_elapsed_seconds","data_bytes_before","data_bytes_after","timings"],"properties":{"data_bytes_after":{"type":"integer","format":"int64","description":"Size of the data after they ran. The growth between the two is what shows\na migration that backfills heavily."},"data_bytes_before":{"type":"integer","format":"int64","description":"Size of the data before the migrations ran."},"failed_migration":{"type":["string","null"],"description":"The migration that failed, when one did."},"target_version_id":{"type":"string","format":"uuid","description":"The version whose migrations were applied, taken from the worklist\nentry's `target_version_id`."},"timings":{"type":"array","items":{"$ref":"#/components/schemas/MigrationTimingArgs"},"description":"One entry per migration that ran, in the order they ran."},"total_elapsed_seconds":{"type":"integer","format":"int64","description":"Whole seconds the whole migration run took."}}},"MigrationTimingArgs":{"type":"object","description":"How long one migration took.","required":["name","elapsed_seconds"],"properties":{"elapsed_seconds":{"type":"integer","format":"int64","description":"Whole seconds it took."},"name":{"type":"string","description":"The migration's name, as the migration runner reports it."}}},"ParamType":{"type":"string","description":"The data type of a restore-replica configuration parameter, which\ndetermines how its value is validated. `duration` and `bytes` values must\nbe non-negative integers (a count of seconds and of bytes, respectively);\n`integer` accepts any whole number, positive or negative; `boolean` is a\nJSON boolean; `text` is a JSON string.","enum":["duration","bytes","boolean","integer","text"]},"ProblemDetailsSchema":{"type":"object","description":"Standard error response body, returned for every non-2xx response.\n\nThis follows the RFC 7807 \"Problem Details\" shape: a stable machine-readable\n`type`, a short `title`, the repeated HTTP `status` code, and an optional\n`detail` string with specifics of this particular occurrence.","required":["type","title","status"],"properties":{"detail":{"type":["string","null"],"description":"Human-readable explanation specific to this occurrence of the\nproblem, if any extra detail is available."},"status":{"type":"integer","format":"int32","description":"HTTP status code of the response, repeated here for convenience.","example":404,"minimum":0},"title":{"type":"string","description":"Short, human-readable summary of the problem type. Does not vary\nbetween occurrences of the same `type`."},"type":{"type":"string","format":"uri","description":"A URI reference identifying the problem type. Stable across\noccurrences of the same error, so callers can match on it.","example":"/errors/resource-not-found"}}},"ProgressArgs":{"type":"object","description":"A progress sample from a run still in flight.","required":["run_id","type"],"properties":{"bytes_cached":{"type":["integer","null"],"format":"int64","description":"Bytes found already present in the repository, and so not re-uploaded."},"bytes_estimated":{"type":["integer","null"],"format":"int64","description":"Total bytes this run currently expects to handle. May be revised upward."},"bytes_hashed":{"type":["integer","null"],"format":"int64","description":"Bytes processed (hashed, compressed) so far."},"bytes_read":{"type":["integer","null"],"format":"int64","description":"Source bytes read so far."},"bytes_uploaded":{"type":["integer","null"],"format":"int64","description":"Bytes uploaded to the repository so far."},"current_path":{"type":["string","null"],"description":"What the run is working on right now, for display."},"errors":{"type":["integer","null"],"format":"int64","description":"Errors hit so far."},"extra":{"type":"object","description":"Any further detail the backup engine emits. Canopy makes no commitment\nabout its shape: it is stored and shown verbatim, never interpreted."},"files_done":{"type":["integer","null"],"format":"int64","description":"Files finished so far."},"files_estimated":{"type":["integer","null"],"format":"int64","description":"Total files this run currently expects to handle."},"ignored_errors":{"type":["integer","null"],"format":"int64","description":"Errors hit and deliberately ignored so far."},"purpose":{"$ref":"#/components/schemas/BackupPurpose","description":"Whether this is a `backup` or a `restore` run."},"run_id":{"type":"string","format":"uuid","description":"The run-uuid the client minted for this run — the same one it passes to\n`POST /backup-credentials` and reports under at `POST /backup-report`."},"s3_received_payload_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of decoded object payload received from S3 so far."},"s3_received_raw_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of raw HTTP traffic received from S3 so far."},"s3_sent_payload_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of decoded object payload sent to S3 so far."},"s3_sent_raw_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of raw HTTP traffic sent to S3 so far, including protocol and\nsigning overhead."},"snapshot_taken_at":{"type":["string","null"],"format":"date-time","description":"When this run froze the data it is backing up — the point in time the\nbackup represents, as opposed to when its upload finishes. Send it as soon\nas it is known (before any transfer starts). Recorded once per run: the\nfirst value Canopy sees stands, whether it arrives here or on the report."},"type":{"type":"string","description":"The backup type being run (e.g. `tamanu-postgres`)."}}},"PublicServer":{"type":"object","description":"A publicly-listed central server that a client can connect to.","required":["name","host"],"properties":{"host":{"$ref":"#/components/schemas/UrlField","description":"The server's reachable base URL."},"name":{"type":"string","description":"Public-facing display name of the server."},"rank":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ServerRank","description":"The server's environment tier (production, clone, demo, test, or\ndev), if set. Used to order the listing and to let clients label\nnon-production entries."}]}}},"RedactionArgs":{"type":"object","description":"How the masking manifest went against the restored replica.\n\nReported when the redaction settles, which for a failure is before any\nswitchover: the restore itself succeeded and is reported healthy, and the\nreplica stays on the data it was already serving.","required":["outcome"],"properties":{"columns_masked":{"type":["integer","null"],"format":"int64","description":"How many columns the manifest masked."},"columns_skipped":{"type":["integer","null"],"format":"int64","description":"How many columns the manifest named but could not mask. Non-zero is\nwhat makes an outcome `partial`."},"error":{"type":["string","null"],"description":"Why the redaction failed, when it did."},"manifest_version":{"type":["string","null"],"description":"The version resolved into the manifest URL. Omit when the URL named no\nversion to resolve."},"outcome":{"type":"string","description":"How far the manifest got: `complete`, `partial`, or `failed`."}}},"RegisterNameArgs":{"type":"object","description":"The name a server should be reachable at, and where.","required":["name","addresses"],"properties":{"addresses":{"type":"array","items":{"type":"string"},"description":"Every external address this server is reachable at. IPv4 addresses become\nA records and IPv6 addresses AAAA records, replacing whatever was\nregistered before. An empty list withdraws the name."},"name":{"type":"string","description":"The name to publish records at. Must sit within a domain this server's\ngroup controls."}}},"RegisteredName":{"type":"object","description":"What Canopy holds for a registered name.","required":["name","addresses","published_addresses","published"],"properties":{"addresses":{"type":"array","items":{"type":"string"},"description":"The addresses Canopy will publish."},"last_error":{"type":["string","null"],"description":"Why the last publish attempt failed, if it did."},"name":{"type":"string","description":"The name, as Canopy normalised it."},"published":{"type":"boolean","description":"Whether the zone has caught up with what was asked for."},"published_addresses":{"type":"array","items":{"type":"string"},"description":"The addresses Canopy has published so far. Differs from `addresses` until\nthe change has been reconciled into the zone."}}},"ReportArgs":{"type":"object","description":"Report of a completed backup or restore run.","required":["run_id","type","purpose","outcome"],"properties":{"bytes_uploaded":{"type":["integer","null"],"format":"int64","description":"Total bytes of backup data uploaded during the run, if known."},"error":{"type":["string","null"],"description":"Human-readable error detail, when the run failed."},"outcome":{"$ref":"#/components/schemas/RunOutcome","description":"Whether the run succeeded (`success`) or failed (`failure`)."},"purpose":{"$ref":"#/components/schemas/BackupPurpose","description":"Whether the run was a `backup` or a `restore`."},"run_id":{"type":"string","format":"uuid","description":"Client-generated UUID identifying this run, minted at run start. Each\nrun must use a fresh UUID: reporting the same `run_id` twice is\nrejected with 409."},"s3_received_payload_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of decoded object payload received from S3 during the run."},"s3_received_raw_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of raw HTTP traffic received from S3 during the run, including\nprotocol overhead."},"s3_sent_payload_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of decoded object payload sent to S3 during the run (excluding\nprotocol and signing overhead)."},"s3_sent_raw_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of raw HTTP traffic sent to S3 during the run, including protocol\nand signing overhead. Report on both success and failure; omit when\ntraffic was not measured."},"snapshot_id":{"type":["string","null"],"description":"Identifier of the repository snapshot the run produced, for a\nsuccessful backup."},"snapshot_taken_at":{"type":["string","null"],"format":"date-time","description":"When this run froze the data it backed up — the point in time the backup\nrepresents, as opposed to when its upload finished. Often a filesystem-level\nsnapshot taken before the transfer, in which case it is not recoverable\nfrom the repository and only the device can report it. Recorded once per\nrun: if progress reports already carried it, that value stands."},"type":{"type":"string","description":"The backup type that ran (e.g. `tamanu-postgres`)."}}},"RequestCertificateArgs":{"type":"object","description":"A request to certify a key for a name.","required":["name","csr"],"properties":{"csr":{"type":"string","description":"The certificate signing request, DER, base64. Must ask for exactly `name`\nand nothing else — a request carrying any other name is refused rather\nthan trimmed."},"name":{"type":"string","description":"The name to certify. Must sit within a domain this server's group\ncontrols."}}},"RestoreCapabilitiesArgs":{"type":"object","description":"Request body for registering the restore intents a consumer device can\nsatisfy.","required":["intents"],"properties":{"intents":{"type":"array","items":{"$ref":"#/components/schemas/IntentDescriptor"},"description":"The intents this device can satisfy — arbitrary consumer-chosen\nidentifiers (e.g. `verify`) — each with its description, the semantics\nit opts into, and the schema of the parameters it accepts. Replaces the\ndevice's previously advertised set wholesale."}}},"RestoreCredentials":{"type":"object","description":"Read-only S3 credentials plus the repository passphrase for one group and\nbackup type: everything needed to open the group's backup repository and\nread a snapshot out of it.","required":["credentials","repo_password"],"properties":{"credentials":{"$ref":"#/components/schemas/CredentialProcessOutput","description":"Temporary read-only AWS credentials in the `credential_process` output\nformat, valid for at most one hour."},"repo_password":{"type":"string","description":"Passphrase for the group's backup repository (a Kopia repository)."}}},"RestoreCredentialsArgs":{"type":"object","description":"Request body for minting read-only restore credentials.","required":["group","type"],"properties":{"group":{"type":"string","format":"uuid","description":"The server group whose backup repository to read."},"run_id":{"type":["string","null"],"format":"uuid","description":"This must be the run-uuid the client minted for this run.\nThe field is optional only so older clients don't break; it WILL be made\nmandatory in future."},"type":{"type":"string","description":"The backup type to restore (e.g. `tamanu-postgres`)."}}},"RunOutcome":{"type":"string","description":"Outcome of a reported backup or restore run.","enum":["success","failure"]},"SelfResponse":{"type":"object","description":"The calling device's own identity, as assigned at enrollment.","required":["server_id","device_id"],"properties":{"device_id":{"type":"string","format":"uuid","description":"The calling device's own identity."},"server_id":{"type":"string","format":"uuid","description":"The server the calling device is enrolled as."}}},"ServerRank":{"type":"string","description":"The environment tier of a server, from `production` down to `dev`.","enum":["production","clone","demo","test","dev"]},"SnippetResponse":{"type":"object","description":"A single named SQL snippet from the bestool snippet library.","required":["sql"],"properties":{"description":{"type":["string","null"],"description":"Human-readable explanation of what the snippet does, if the author\nprovided one."},"sql":{"type":"string","description":"The snippet's SQL text."}}},"StatusPayload":{"allOf":[{"type":"object","description":"Free-form additional data (uptime, database version, timezone,\nhostname, etc.). Stored verbatim and surfaced as raw JSON in the\nstatus view.\n\nA `tamanuVersion` field here is used as the server's tracked version\n(compared against the published version catalog), superseding the\nlegacy `X-Version` request header. If both are present,\n`tamanuVersion` wins; if neither is, the status is recorded without a\nversion."},{"type":"object","required":["health"],"properties":{"health":{"type":"array","items":{"$ref":"#/components/schemas/HealthCheck"},"description":"Per-check breakdown. A push without a `health` array is the legacy\nTamanu direct-report format: it is treated as the `tamanu` source\nreporting a single always-passing `tasks` heartbeat check. May be\nempty (`[]`) for a source that genuinely runs no checks — which\nrecovers every check it previously reported. Each entry must\ninclude a non-empty `check` name and exactly one of `result` /\n`healthy`; any additional fields per check (latency, free disk %,\ncertificate expiry, etc.) are passed through verbatim and shown in the\nstatus UI.\n\nEvery check name seen — whatever its result — is added to the\noperator-facing check catalog, where the policy grading its results\ncan be reviewed and adjusted. A check whose effective result is\nfailed or warning opens (or keeps open) its issue; a broken check\nkeeps the same issue open, retaining a known failure's contribution\nwhile warning the check itself is broken; effective passed and\nskipped results open nothing and close prior issues."},"healthy":{"type":["boolean","null"],"description":"Overall self-reported health of the server. **Absent means `true`**,\nso senders that predate this field are never treated as unhealthy by\nomission. Recorded for historical analysis and display, but **not\nconsulted for incident or severity decisions** — those are derived\nfrom the per-check results in `health`, with each check's severity\ncontrolled by an operator-managed catalog."},"source":{"type":["string","null"],"description":"The name of the source pushing this status: the reporting agent, e.g.\n`alertd`. Multiple sources may report on one server, each with its own\nset of checks; a source's push only opens and recovers its own checks.\n\n**Transitionally optional: this field will become mandatory.** A push\nwithout a `source` is attributed to `alertd`; new reporters must send\ntheir own name. Must be a non-empty string; the names `canopy` and\n`manual` are reserved for canopy itself and are rejected."}}}],"description":"A status push: a server's periodic heartbeat carrying its self-reported\nhealth.\n\nBesides the reserved `healthy` and `health` keys described here, any\nadditional top-level fields are accepted and stored verbatim as extra\nstatus data."},"StatusResponse":{"type":"object","description":"The status-push response: only the return-path instructions the device\ncan act on. The stored status record is deliberately not echoed back —\nthe device already has everything it sent.","required":["backup_now","check_severities","names","tags"],"properties":{"backup_now":{"type":"array","items":{"type":"string"},"description":"Backup types the server should back up now: operator-requested\none-offs plus scheduled backups that are due. Each serializes as a\nplain string (e.g. `\"tamanu-postgres\"`). The device should run each\nlisted type, then report via `POST /backup-report`; an empty list\nmeans nothing to do. Only sent to `alertd` pushes (the agent that\nruns backups); other sources always receive an empty list."},"check_severities":{"type":"object","description":"The effective handling of every healthcheck canopy knows about, keyed\nby check name (as reported in `health[].check`): `skip` (silenced for\nthis server, or classified below warning), `warn` (warning), or `fail`\n(error or critical). Only the static severity baseline is reflected —\noperator-defined conditional rules are evaluated per push and not\nincluded. Checks absent from the map are new to canopy and default to\n`warn`. Clients that predate this field can safely ignore it; the\nsame mapping is served on demand at `GET /status/{server_id}/check-severities`.","additionalProperties":{"$ref":"#/components/schemas/CheckSeverity"},"propertyNames":{"type":"string"}},"names":{"$ref":"#/components/schemas/Entitlements","description":"What this server is entitled to do with names: the domains its group\ncontrols, the grants it holds, whether it is paused, and the names and\ncertificates it already has. A server-wide fact, so returned to every\nsource — an agent already reporting status learns of a new domain or a\nnewly granted permission without asking separately. Identical to what\n`GET /names/entitlements` returns. Clients that predate this field can\nsafely ignore it."},"tags":{"$ref":"#/components/schemas/TagMap","description":"The server's effective tags: its own tags overlaid on its group's,\nplus the synthetic read-only `canopy:` tags and effective `billing.*`\nlabels. Identical to what the standalone `GET /tags` endpoint\nreturns — see that endpoint for the full contract. Clients that\npredate this field can safely ignore it."}}},"TagMap":{"type":"object","description":"Free-form key/value tags, as a JSON object whose values are all strings.","additionalProperties":{"type":"string"}},"UrlField":{"type":"string","format":"uri","description":"A URL, given as a plain string. Any trailing slash is stripped when the\nvalue is returned."},"VerificationArgs":{"type":"object","description":"Report of a restore attempt and the health of the resulting replica.","required":["group","server_id","type","intent","outcome","replica_healthy","observed_at"],"properties":{"error":{"type":["string","null"],"description":"Human-readable error detail, when the restore failed."},"group":{"type":"string","format":"uuid","description":"The server group whose backup was restored."},"health_details":{"description":"Arbitrary structured health data to record alongside the report\n(database statistics, whether indexes needed rebuilding, and so on).\nStored and displayed as-is."},"intent":{"type":"string","description":"The restore intent this attempt was performed under."},"migration":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MigrationArgs","description":"What the migrations did, for a report under a `migrate` intent. Omit for\nevery other intent."}]},"observed_at":{"type":"string","description":"When the restore result was observed, as an RFC 3339 timestamp."},"outcome":{"type":"string","description":"Whether the restore succeeded (`success`) or failed (`failure`)."},"postgres_version":{"type":["string","null"],"description":"Version of the PostgreSQL server the data was restored into, if\napplicable."},"redaction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/RedactionArgs","description":"What the masking manifest did, for a replica that redacts. Omit for a\nreplica that doesn't."}]},"replica_healthy":{"type":"boolean","description":"Whether the restored database came up healthy and passed readiness\nchecks. A replica only counts as verified when the outcome is\n`success` and this is `true`."},"replica_id":{"type":["string","null"],"format":"uuid","description":"The declaration this report concerns, taken from the worklist entry's\n`replica_id`. Optional so a report is still accepted when the\ndeclaration was retired while the restore was in flight."},"run_id":{"type":["string","null"],"format":"uuid","description":"This must be the run-uuid the client minted for this run.\nThe field is optional only so older clients don't break; it WILL be made\nmandatory in future."},"s3_received_payload_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of decoded object payload received from S3 during the restore."},"s3_received_raw_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of raw HTTP traffic received from S3 during the restore,\nincluding protocol overhead."},"s3_sent_payload_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of decoded object payload sent to S3 during the restore."},"s3_sent_raw_bytes":{"type":["integer","null"],"format":"int64","description":"Bytes of raw HTTP traffic sent to S3 during the restore, including\nprotocol and signing overhead. Omit when traffic was not measured."},"server_id":{"type":"string","format":"uuid","description":"The server whose backup was restored."},"snapshot_id":{"type":["string","null"],"description":"Identifier of the snapshot that was restored. Omit on a failure that\nnever got as far as selecting a snapshot."},"type":{"type":"string","description":"The backup type that was restored (e.g. `tamanu-postgres`)."}}},"Version":{"type":"object","description":"A release version of the monitored software, with its publication\nstatus and changelog.","required":["id","created_at","updated_at","major","minor","patch","status","changelog"],"properties":{"changelog":{"type":"string","description":"Changelog text for this version, as Markdown."},"created_at":{"type":"string","format":"date-time","description":"When the version record was created."},"device_id":{"type":["string","null"],"format":"uuid","description":"The releaser device that published this version, if it was published\nby a device rather than created by an operator."},"id":{"type":"string","format":"uuid","description":"Unique identifier of the version."},"major":{"type":"integer","format":"int32","description":"Major version number."},"minor":{"type":"integer","format":"int32","description":"Minor version number."},"patch":{"type":"integer","format":"int32","description":"Patch version number."},"status":{"$ref":"#/components/schemas/VersionStatus","description":"Publication status: `draft`, `published`, or `yanked`."},"updated_at":{"type":"string","format":"date-time","description":"When the version record was last changed."}}},"VersionStatus":{"type":"string","description":"Publication status of a release version.","enum":["draft","published","yanked"]},"ViewVersion":{"type":"object","description":"A release version as returned by the version-listing endpoints: the\nversion numbers, publication status, and changelog.","required":["id","major","minor","patch","status","changelog"],"properties":{"changelog":{"type":"string","description":"Changelog text for this version, as Markdown."},"id":{"type":"string","format":"uuid","description":"Unique identifier of the version."},"major":{"type":"integer","format":"int32","description":"Major version number."},"minor":{"type":"integer","format":"int32","description":"Minor version number."},"patch":{"type":"integer","format":"int32","description":"Patch version number."},"status":{"$ref":"#/components/schemas/VersionStatus","description":"Publication status: `draft`, `published`, or `yanked`."}}},"WorklistEntry":{"type":"object","description":"One replica the consumer device should currently maintain: an operator\ndeclaration expanded against a single server, carrying the snapshot to\nrestore and the repository coordinates to find it. S3 credentials and the\nrepository passphrase are obtained separately via\n`POST /restore-credentials`.","required":["replica_id","group_id","server_id","type","intent","name","params","storage","bucket","prefix","region"],"properties":{"bucket":{"type":"string","description":"Name of the S3 bucket holding the group's backup repository."},"group_id":{"type":"string","format":"uuid","description":"The server group whose backup repository holds the snapshot."},"intent":{"type":"string","description":"The restore intent this entry is for; one of the intents this device\nadvertised via `POST /restore-capabilities`."},"name":{"type":"string","description":"Operator-assigned label for the declaration."},"overdue_after_seconds":{"type":["integer","null"],"format":"int64","description":"Bound, in whole seconds, after which the replica counts as overdue;\n`null` means no bound. Interpreted per the intent's semantics: for a\nrun-once (`once`) intent, how long the latest snapshot may go without a\nhealthy verification report; for a standing replica, how stale its last\nhealthy report may be."},"params":{"type":"object","description":"Resolved parameter values for this replica: one key per parameter the\nintent advertises. Parameters the operator left unset carry the\nintent's declared default, or JSON `null` when there is none."},"prefix":{"type":"string","description":"Key prefix within the bucket under which the repository lives. Normally\nempty (the repository is at the bucket root)."},"region":{"type":"string","description":"AWS region of the bucket."},"replica_id":{"type":"string","format":"uuid","description":"Identifier of the declaration this entry was expanded from. Echo it\nback in `POST /restore-verification` reports."},"server_id":{"type":"string","format":"uuid","description":"The server whose backup should be restored."},"snapshot_at":{"type":["string","null"],"description":"When that snapshot was reported, as an RFC 3339 timestamp; `null` if\nunknown."},"snapshot_id":{"type":["string","null"],"description":"Identifier of the snapshot to restore — the latest successful backup\nfor this server and type. `null` when no successful backup is known\nyet."},"storage":{"type":"string","description":"Kind of storage backend. Always `\"s3\"`."},"target_version":{"type":["string","null"],"description":"For a `migrate` intent, the version whose schema migrations to apply\nafter restoring. Obtain them from that version's published artefacts, the\nsame way a server being upgraded does. `null` for every other intent."},"target_version_id":{"type":["string","null"],"format":"uuid","description":"Identifier of that version. Echo it back in the migration-test report."},"type":{"type":"string","description":"The backup type to restore (e.g. `tamanu-postgres`)."}}}},"securitySchemes":{"admin-device":{"type":"mutualTLS","description":"mTLS client certificate for a device with the `admin` role."},"backup-restore-device":{"type":"mutualTLS","description":"mTLS client certificate for a device with the `backup-restore` role (or `admin`)."},"releaser-device":{"type":"mutualTLS","description":"mTLS client certificate for a device with the `releaser` role (or `admin`)."},"server-device":{"type":"mutualTLS","description":"mTLS client certificate for a device with the `server` role (or `admin`)."}}},"tags":[{"name":"artifacts","description":"Per-version artifact registration by releaser devices."},{"name":"backup","description":"Device backup credential minting, target config, capability registration, and run reporting."},{"name":"bestool","description":"Bestool SQL snippet read API."},{"name":"certificates","description":"TLS certificates for a server's own names: requesting one for a signing request, and collecting it once Canopy has obtained it."},{"name":"names","description":"Public names a server may act on: what it is entitled to, and the addresses to publish for them."},{"name":"restore","description":"Managed restore replicas: consumer capability registration, worklist, and read-only restore credentials."},{"name":"servers","description":"Server registry — listing for the public, self-registration for server devices."},{"name":"statuses","description":"Heartbeat / status submissions from server devices."},{"name":"tags","description":"Key/value tags describing a server."},{"name":"versions","description":"Canopy release versions and their downloadable artifacts."}],"x-request-compression":{"accepted":["gzip","br","deflate","zstd"],"header":"Content-Encoding","recommended":true}}