# Install the agent on many servers The InfraNest agent reports CPU, memory, disk, load and service health from Linux and Windows servers. It only ever sends: it takes no instructions, runs no commands, opens no ports, and runs as its own unprivileged user. It is open source: . Before you roll it out: [Network requirements](network-requirements.md) lists what it connects to (one outbound HTTPS host, nothing inbound), and [What it sends](what-it-sends.md) lists every field. **Runnable code:** an Ansible role (Linux and Windows) and a `cloud-init.yaml` that install the agent with one server token per host are in [`infranest-examples/examples/agent-install`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/agent-install). Until enrollment keys arrive (below), each host needs its own token. ## One server Open the server in InfraNest, go to **Metrics** and choose **Install agent**. It gives you one line with the token already in it: ```sh curl -fsSL https://get.infranest.io/agent.sh | sudo sh -s -- --token sat_YOUR_TOKEN ``` On Windows: ```powershell irm https://get.infranest.io/agent.ps1 -OutFile install.ps1 .\install.ps1 -Token sat_YOUR_TOKEN ``` ## Many servers (Coming) An **enrollment key**, created under **Settings → Server agent**, will install the agent on as many servers as you allow from one cloud-init file or Ansible role. Each server still gets its own token on first start, so revoking one server never touches the others. Until then, each server needs its own token from the app. ## See what it sends Run this on any machine to print the exact JSON it would send. It sends nothing: ```sh infranest-agent print ``` The full field list is in the repository's `PAYLOAD.md`. ## Check it worked ```sh sudo infranest-agent status ``` The server shows live metrics in InfraNest within a minute. --- # Network requirements What the InfraNest agent connects to, so you can allow it through a firewall. In short: **one outbound HTTPS connection, to `ingest.infranest.io`, and nothing inbound, ever.** The agent opens no port and takes no instructions. Installing and upgrading it also downloads from `get.infranest.io`. ## What it connects to | Host | Port | When | What for | |---|---|---|---| | `ingest.infranest.io` | TCP 443 (HTTPS) | Every interval, 60 seconds by default | Sends readings to `POST /api/metrics/push` | | `get.infranest.io` | TCP 443 (HTTPS) | Only while you install or upgrade | The installer and the agent itself | | `github.com` and the download host it redirects to | TCP 443 (HTTPS) | Only while you install or upgrade, and only if `get.infranest.io` cannot be reached | The installer's fallback: the same release files | | Your DNS resolver | 53 | When it looks up the names above | Name resolution, as for any program | That is the whole list. The agent sends nothing to analytics, error reporting or a CDN, and contacts no other host. Its only other connection is local: on Linux it reads services and containers from the systemd D-Bus socket on the same machine. - **Encrypted, always.** The agent refuses an `INFRANEST_URL` that is not `https://`, and refuses one with a username or password in it. - **IPv4 and IPv6.** `ingest.infranest.io` has both an IPv4 and an IPv6 address, and the agent uses whichever your network gives it, so an IPv6-only server works. `get.infranest.io` serves over both too. GitHub has no IPv6 address, which is why the installer tries `get.infranest.io` first. - **Allow it by name, not by address.** The addresses behind `ingest.infranest.io` can change without notice. If your firewall only takes addresses, use a proxy that allows by name (below). - **It follows no HTTP redirect.** A push response can move the agent to another ingest host, but only over HTTPS and only to a host under `infranest.io`. Anyone who manages to answer one push cannot point your servers somewhere else. A move to a different domain needs the installer run again, and would be announced first. - **Your own address.** Self-hosted installations set `INFRANEST_URL` to their own host; the same rules apply. ## Nothing inbound InfraNest never connects to your server. There is no listening port, no remote shell and no command channel: the agent only sends. You do not need to open anything in a firewall or a cloud security group. ## Behind a proxy The agent uses the standard `HTTPS_PROXY` and `NO_PROXY` environment variables. With the systemd service, add them to `/etc/infranest/agent.conf`, which the service loads as its environment, and restart it: ```sh echo 'HTTPS_PROXY=http://proxy.example.com:3128' | sudo tee -a /etc/infranest/agent.conf sudo systemctl restart infranest-agent ``` A proxy that inspects TLS needs its certificate authority in the machine's trust store, which the agent reads. On Windows, and with the OpenRC and runit services, the agent does not load `agent.conf` as its environment. Set the variable for the agent's process another way, or use a proxy that needs no setting. ## When it cannot reach InfraNest Nothing is lost while the connection is down, for up to about eight hours: - Readings are still taken on time, and kept on disk in `/var/lib/infranest-agent` until InfraNest confirms it stored them. A request that merely completed, such as an error page from a proxy, does not count. - The agent tries again after 30 seconds, then waits twice as long each time, up to 15 minutes. - When the connection is back, it sends what it kept, oldest first, in batches of up to 60 readings. - It keeps at most 500 readings, about eight hours at the default interval, and drops the oldest after that so it never fills your disk. - InfraNest accepts readings up to 24 hours old. A reading more than 300 seconds ahead of InfraNest's clock is refused, so keep the server's clock in sync. If InfraNest refuses the token, because it was deleted or the server was removed, the agent tries again every 15 minutes rather than every few seconds. `sudo infranest-agent status` says what is happening without reaching the network. ## A firewall rule to copy Outbound only. With a proxy or firewall that allows by host name: ```text # InfraNest agent: outbound HTTPS only, nothing inbound allow tcp/443 to ingest.infranest.io # readings, all the time allow tcp/443 to get.infranest.io # install and upgrade only ``` Add `github.com` only if `get.infranest.io` is blocked where you install. To check the connection from the server, send a push without a token: ```sh curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://ingest.infranest.io/api/metrics/push ``` `401` means the connection works and only the token is missing. A timeout or a TLS error means something blocks it. ## Related - [What it sends](what-it-sends.md): every field, how often, and how long it is kept. - [Install the agent on many servers](install-on-many-servers.md). - The agent's source and README: . --- # What it sends Every field the InfraNest agent sends, how often, and how long InfraNest keeps it. The tables below are built from the push endpoint's own validation, so they list exactly what InfraNest accepts. The agent's [PAYLOAD.md](https://github.com/InfraNest-Infrastructure-Organized/infranest-agent/blob/main/PAYLOAD.md) has the full contract with every edge case. ## See it yourself first On the server, before or after installing: ```sh infranest-agent print ``` That takes one reading and prints the exact JSON the agent would send, and sends nothing. It needs no `sudo`: it reads the machine, not the token. ## How often - **Every interval** (60 seconds by default; `INFRANEST_INTERVAL` from 10 seconds to 5 minutes) the agent takes a reading and sends it with any it could not send before, in one `POST` to `https://ingest.infranest.io/api/metrics/push`. - **Once an hour at most**, the push also says which directories fill the fullest disk. - The agent sends at most 60 readings in one push. InfraNest accepts up to 180, and up to 120 pushes a minute from one server's token. - If your plan stores readings less often than the agent takes them, InfraNest tells the agent and it slows down to match. It never speeds up because of an answer. The token names one server. Nothing in the push says which machine it came from: InfraNest knows it from the token. ## Never sent The agent runs as its own unprivileged user and reads only what that user may read. On top of that: - **No log output.** When a service fails, the agent sends systemd's one-word reason (`oom-kill`, `timeout`, `exit-code`) and the exit status. Never a line the service printed, which can contain credentials. - **No command lines.** A process is sent by its executable's name. Its arguments are sent only if you set both `INFRANEST_PROCESSES=1` and `INFRANEST_PROCESS_ARGS=1`; the processes list itself is off by default. - **No hook commands.** For certbot it sends which kinds of renewal hook exist (`deploy`, `post`), never what they run. - **No private keys or certificate contents.** For a certificate it sends the names, the expiry and the file it read them from. - **No sizes it could not measure.** A directory the agent may not enter is named, with its permissions, but never given a size. - **Nothing from the Docker socket.** Containers are read from systemd, so they are sent by ID, without names. - **Nothing about your Ubuntu Pro account.** Only whether Pro is attached, the state of Livepatch and ESM, and when the subscription ends. Not the account, the contract or the machine ID. It reads some configuration files to answer the questions in the tables, such as certbot's renewal configs, APT and dnf-automatic settings, and the Ubuntu Pro cache. It sends the answers listed below, not the files. ## Every field A field is left out when the agent cannot tell, and that is different from zero: "no updates" is `0`, "could not check" is absent. Lists are capped, and a string over its limit makes InfraNest refuse the whole push, not only that field. ### Every reading Sent with every reading, by default every 60 seconds. | Field | Type | Limits | What it is | |---|---|---|---| | `collected_at` | time (RFC 3339) | | When the reading was taken. Defaults to when it arrived | | `cpu_percent` | number | 0 to 100 | CPU in use: user plus system time, not counting steal or iowait | | `memory_percent` | number | 0 to 100 | Memory in use | | `memory_used_bytes` | integer | 0 or more | Memory in use: total minus what the kernel calls available | | `memory_total_bytes` | integer | 0 or more | Total memory | | `swap_used_bytes` | integer | 0 or more | Swap in use | | `load_1` | number | 0 to 10,000 | Load average over 1 minute. Absent on Windows | | `load_5` | number | 0 to 10,000 | Load average over 5 minutes | | `load_15` | number | 0 to 10,000 | Load average over 15 minutes | | `uptime_seconds` | integer | 0 or more | Seconds since boot | | `network_in_bps` | integer | 0 or more | Bytes per second received since the previous reading, on interfaces backed by a device | | `network_out_bps` | integer | 0 or more | Bytes per second sent since the previous reading | | `disk_read_bps` | integer | 0 or more | Bytes per second read from whole disks since the previous reading | | `disk_write_bps` | integer | 0 or more | Bytes per second written since the previous reading | | `pressure_cpu` | number | 0 to 100 | Share of the last minute some work waited for CPU (Linux pressure stall information) | | `pressure_memory` | number | 0 to 100 | The same for memory | | `pressure_memory_full` | number | 0 to 100 | Share of the last minute all work waited for memory | | `pressure_io` | number | 0 to 100 | Share of the last minute some work waited for disk | | `pressure_io_full` | number | 0 to 100 | Share of the last minute all work waited for disk | ### Disk space, per mount Sent with every reading. Only bytes and inodes: the server works out the percentage. | Field | Type | Limits | What it is | |---|---|---|---| | `mounts` | list | up to 64 | The mounted filesystems | | `mounts[].mount_point` | string | up to 191 characters; required | Where it is mounted, such as `/var` | | `mounts[].device` | string | up to 191 characters | The device, such as `/dev/sda1` | | `mounts[].used_bytes` | integer | 0 or more; required | Bytes used | | `mounts[].total_bytes` | integer | at least 1; required | Size in bytes | | `mounts[].inodes_used` | integer | 0 or more | Inodes used. Absent where the filesystem has no fixed inode table | | `mounts[].inodes_total` | integer | at least 1 | Inodes in total | ### The machine itself A snapshot of the machine, applied from the newest reading in each push. | Field | Type | Limits | What it is | |---|---|---|---| | `system` | object | | The machine | | `system.kernel` | string | up to 128 characters | Kernel version | | `system.os` | string | up to 128 characters | Operating system and version | | `system.pending_updates` | integer | 0 to 100,000 | Updates waiting to be installed | | `system.security_updates` | integer | 0 to 100,000 | Of those, security updates | | `system.reboot_required` | boolean | | A reboot is needed to finish updates | | `system.reboot_reason` | string | up to 16 characters | What said so: `flag_file`, `newer_kernel`, … | | `system.newest_kernel` | string | up to 128 characters | The newer installed kernel | | `system.auto_update` | object | | Whether the machine installs updates by itself | | `system.auto_update.source` | string | up to 32 characters | `unattended-upgrades` or `dnf-automatic` | | `system.auto_update.enabled` | boolean | | It installs updates on its own | | `system.auto_update.reboot` | boolean | | It reboots on its own when an update needs it | | `system.auto_update.reboot_time` | string | up to 32 characters | When it reboots, as configured | | `system.auto_update.reboot_with_users` | boolean | | It reboots while someone is logged in | | `system.auto_update.utc_offset_minutes` | integer | -900 to 900 | The machine's offset from UTC | | `system.oom_kills` | integer | 0 to 1,000,000,000 | Processes the kernel killed for lack of memory since boot | | `system.boot_id` | string | up to 64 characters | An ID that changes on every boot | | `system.cpus` | integer | 1 to 4,096 | CPUs the kernel can use | | `system.raid` | list | up to 32 | Software RAID arrays, from `/proc/mdstat` | | `system.raid[].name` | string | up to 32 characters; required | The array, such as `md0` | | `system.raid[].level` | string | up to 16 characters | Its level, such as `raid1` | | `system.raid[].state` | string | up to 16 characters; required | `active` or `inactive` | | `system.raid[].devices` | integer | 0 to 1,024 | Members it is built for | | `system.raid[].active` | integer | 0 to 1,024 | Members working | | `system.raid[].failed` | integer | 0 to 1,024 | Members marked failed | | `system.raid[].sync` | string | up to 16 characters | A resync, recovery or check in progress | | `system.raid[].sync_percent` | number | 0 to 100 | How far it is | | `system.time_synced` | boolean | | The kernel considers its clock synchronised | | `system.time_error_ms` | integer | 0 to 2,147,483,647 | No longer sent by the agent; still accepted | | `system.pro` | object | | Ubuntu Pro, from its client's world-readable cache. Never the account or contract | | `system.pro.attached` | boolean | | Attached to a subscription | | `system.pro.livepatch` | string | up to 32 characters | Livepatch's state | | `system.pro.esm_infra` | string | up to 32 characters | ESM Infra's state | | `system.pro.expires` | time (RFC 3339) | | When the subscription ends | ### Services systemd units that are enabled or have failed (Windows: automatic-start services). On by default; `INFRANEST_SERVICES=0` turns it off. | Field | Type | Limits | What it is | |---|---|---|---| | `services` | list | up to 200 | The watched units | | `services[].unit` | string | up to 255 characters; required | The unit's name, such as `nginx.service` | | `services[].description` | string | up to 255 characters | Its description | | `services[].active_state` | string | up to 32 characters; required | systemd's state: `active`, `failed`, … | | `services[].sub_state` | string | up to 32 characters | systemd's sub-state: `running`, `exited`, … | | `services[].state_changed_at` | time (RFC 3339) | | When it entered that state | | `services[].result` | string | up to 32 characters | Why a failed unit failed, one word from systemd: `exit-code`, `oom-kill`, `timeout`, … Never log output | | `services[].exec_main_code` | integer | -16 to 16 | How its main process ended: exited, killed or dumped | | `services[].exec_main_status` | integer | -2,147,483,648 to 2,147,483,647 | The exit status, or the signal | | `services[].restarts` | integer | 0 to 100,000,000 | How many times systemd restarted it | | `services[].memory_bytes` | integer | 0 to 1,099,511,627,776 | Memory its cgroup uses | ### Containers Running Docker and Podman containers, read from the units systemd keeps for them — never from the Docker socket. | Field | Type | Limits | What it is | |---|---|---|---| | `containers` | list | up to 100 | The running containers | | `containers[].id` | string | up to 64 characters; required | The short container ID. No name: that is only readable as root | | `containers[].runtime` | string | up to 32 characters | `docker` or `podman` | | `containers[].active_state` | string | up to 32 characters | The state of its systemd scope | | `containers[].started_at` | time (RFC 3339) | | When it last started | | `containers[].restarts` | integer | 0 to 100,000,000 | Restarts the agent has seen | | `containers[].memory_bytes` | integer | 0 to 1,099,511,627,776 | Memory it uses | ### Certificate renewers What renews certificates here: certbot, Caddy, Traefik. Only world-readable files are read. On by default; `INFRANEST_RENEWERS=0` turns it off. | Field | Type | Limits | What it is | |---|---|---|---| | `renewers` | list | up to 50 | What renews certificates | | `renewers[].tool` | string | one of `certbot`, `caddy`, `traefik`; required | Which tool | | `renewers[].name` | string | up to 255 characters | certbot's name for the certificate, or its main name | | `renewers[].domains` | list | up to 100 | The names it renews | | `renewers[].domains[]` | string | up to 253 characters | One name | | `renewers[].domains_guessed` | boolean | | The names are guessed from the certificate's name rather than read | | `renewers[].not_after` | time (RFC 3339) | | When the certificate expires, only if its file is readable | | `renewers[].source` | string | up to 512 characters | The file this was read from | | `renewers[].authenticator` | string | up to 64 characters | certbot's authenticator: `webroot`, `nginx`, `dns-…` | | `renewers[].installer` | string | up to 64 characters | certbot's installer: `nginx` or `apache` | | `renewers[].hooks` | list | up to 8 | Which kinds of hook run after a renewal: `deploy`, `post`. Never the commands | | `renewers[].hooks[]` | string | up to 32 characters | One kind | | `renewers[].unit` | string | up to 255 characters | The systemd timer that runs it | | `renewers[].last_run_at` | time (RFC 3339) | | When the timer last fired | | `renewers[].last_result` | string | up to 32 characters | How that run finished, systemd's word | | `renewers[].next_run_at` | time (RFC 3339) | | When it fires next | | `renewers[].unreadable` | boolean | | The store exists and the agent was not allowed to read it | | `renewers[].mode` | string | up to 8 characters | Its permissions | | `renewers[].root_owned` | boolean | | Owned by root | ### Processes **Off by default.** Sent only when `INFRANEST_PROCESSES=1`. | Field | Type | Limits | What it is | |---|---|---|---| | `processes` | list | up to 32 | The largest processes by memory | | `processes[].command` | string | up to 255 characters; required | The executable's name. The full command line only if you also set `INFRANEST_PROCESS_ARGS=1` | | `processes[].cpu_percent` | number | 0 to 100 | Share of one core since the previous reading | | `processes[].memory_bytes` | integer | 0 or more | Resident memory | | `processes[].pid` | integer | 0 or more | Process ID | | `processes[].started_at` | time (RFC 3339) | | When it started | ### Once per push Beside the readings, once for the whole push. | Field | Type | Limits | What it is | |---|---|---|---| | `failed` | object | up to 32 | Collectors that could not read what they were asked for | | `failed.` | string | up to 255 characters | Why, per collector name | | `agent_version` | string | up to 32 characters | The agent's version | | `collectors` | object | | Which optional collectors are switched on, sent even when they are off | | `collectors.processes` | boolean | | Processes are collected | | `collectors.process_args` | boolean | | Full command lines are collected | | `collectors.services` | boolean | | Services are collected | | `collectors.renewers` | boolean | | Certificate renewers are collected | ### What is filling a disk At most once an hour, for the fullest mount only: the largest directories, and the ones the agent was not allowed to read. | Field | Type | Limits | What it is | |---|---|---|---| | `disk_usage` | object | | One mount's largest directories | | `disk_usage.mount_point` | string | up to 191 characters; required | Which mount was looked at | | `disk_usage.scanned_at` | time (RFC 3339) | | When | | `disk_usage.partial` | boolean | | The look ran out of time; there may be more | | `disk_usage.dirs` | list | up to 16 | The largest directories, four levels deep at most | | `disk_usage.dirs[].path` | string | up to 512 characters; required | The directory | | `disk_usage.dirs[].bytes` | integer | 0 or more; required | The size of the files in it | | `disk_usage.dirs[].kind` | string | up to 64 characters | A plain name where the agent recognises it, such as "docker images and containers" | | `disk_usage.accounted_bytes` | integer | 0 or more | Everything the look counted, listed or not | | `disk_usage.unreadable` | list | up to 16 | Directories the agent was not allowed to read. Named, never sized | | `disk_usage.unreadable[].path` | string | up to 512 characters; required | The directory | | `disk_usage.unreadable[].mode` | string | up to 8 characters | Its permissions, as `ls -l` shows them | | `disk_usage.unreadable[].root_owned` | boolean | | Owned by root | | `disk_usage.more_unreadable` | integer | 0 or more | How many more refusals were not named | ## How long it is kept InfraNest keeps every reading for 48 hours, for the detailed charts. Hourly averages are kept for as long as your plan keeps monitoring history: | Plan | Every reading | Hourly averages | |---|---|---| | Free | 48 hours | 30 days | | Pro | 48 hours | 90 days | | Business | 48 hours | 365 days | | Enterprise | 48 hours | 3,650 days | An organization on a plan agreed separately may have other numbers; **Settings → Subscription** in the app shows yours. Turning off monitoring for a server makes InfraNest refuse its pushes and store nothing, and the agent then tries again only every 15 minutes. ## Related - [Network requirements](network-requirements.md): what it connects to, and what happens offline. - [Install the agent on many servers](install-on-many-servers.md). --- # Connect Claude and ChatGPT In claude.ai and ChatGPT you connect InfraNest without a token: you add the MCP server's address, sign in to InfraNest and approve. This works on every plan, Free included. The assistant can then read the organization you picked, and never change anything. The address is: ``` https://api.infranest.io/mcp ``` ## claude.ai 1. In Claude, open **Settings → Connectors** and select **Add custom connector**. 2. Name it **InfraNest** and enter `https://api.infranest.io/mcp` as its URL. 3. Leave **Authentication** on **Sign in now**, pick **Use Claude's published identity** as the **OAuth client** if Claude has not chosen it already, and select **Add**. InfraNest then knows it is Claude and shows it as verified. **Register automatically** works too, but then Claude registers itself and InfraNest can only show it as not verified. 4. Select **Connect**. Claude sends you to InfraNest. A connector you add on claude.ai is also there in the Claude desktop and mobile apps. ## ChatGPT In ChatGPT's settings, add a custom connector (an MCP server) with the URL `https://api.infranest.io/mcp`, and connect it. ChatGPT sends you to InfraNest. Custom connectors depend on your ChatGPT plan and settings; ChatGPT's own help describes where to find them. ## Approve it in InfraNest You land on InfraNest's consent page, signed in (sign in first if you are not): 1. **Pick the organization** the assistant may read. It works only there. Your [sandbox](../sandbox.md), with sample data, is a good place to try it first. 2. **Check what it may see.** An assistant asks to read every area; your role in that organization decides what it actually gets, and a struck-out line is something it will not get. 3. Select **Allow**. You go back to the assistant, which can now answer questions about that organization. The assistant keeps access until you remove it, renewing its access by itself. One that is not used for 30 days has to be connected again. To read a second organization, connect it again and pick the other one. How this works for app builders is in [OAuth for integrations](../oauth.md). ## Next - [The MCP server](../ai-assistants.md): its tools, and things to ask. - [What an assistant can and cannot do](safety.md), and how to remove one. - [Troubleshooting](../troubleshooting.md#assistant-and-mcp-connection-problems) when it will not connect. --- # Coding agents Claude Code, Cursor, Codex, Copilot and other coding agents can write scripts against the InfraNest API, and run them. This page is how to set one up so it writes correct code and cannot do damage while it learns. Two different things are easy to mix up: - **Writing code against the API.** The agent needs the docs: point it at [`/llms.txt`](/llms.txt) or a page's Markdown. See [Give an assistant the docs](docs-for-assistants.md). - **Reading your account while it works.** The agent connects to the [MCP server](../ai-assistants.md), which only reads. ## Safe defaults - **Start in the sandbox, with a test token.** Your [sandbox](../sandbox.md) has sample zones, monitors and a server, and nothing in it reaches a provider or the internet. A test token (`in_test_…`) cannot touch a live organization even if the agent tries. - **Read-only scopes first.** Give the token only the `.read` scopes the task needs. Add a `.write` scope when the agent has shown you the change it wants to make. - **Dry-run every DNS change.** Add `?dry_run=1` and read the `changes` it returns before the real request. See [Retries and dry runs](../retries-and-dry-runs.md). - **An `Idempotency-Key` on every `POST`**, kept when retrying, so a timeout never creates something twice. - **Branch on `code`, never on `message`.** Error codes are stable; messages may be reworded. - **The token lives in an environment variable**, never in code, a commit or a prompt. Tokens expire; a script must handle `unauthenticated` by stopping, not by retrying. - **Respect `Retry-After`.** The [rate limits](../limits.md#rate-limits) are per token. ## An `AGENTS.md` snippet Put this in your repository's `AGENTS.md` (or `CLAUDE.md`, `.cursor/rules`, `.github/copilot-instructions.md`), so every agent working in it follows the same rules: ```md ## InfraNest API - Docs: https://developers.infranest.io/llms.txt (index) and https://developers.infranest.io/llms-full.txt (all of it). Any page is also Markdown at its address plus `.md`. The API description is https://developers.infranest.io/openapi.json. - Base address: https://api.infranest.io/api. Send `Authorization: Bearer $INFRANEST_TOKEN` and `Accept: application/json`. - The token comes from the INFRANEST_TOKEN environment variable. Never print it, log it or write it to a file. - Use the sandbox organization and a test token (in_test_…) until a person says otherwise. - Before any DNS change, send it with ?dry_run=1, show the result, and wait for a person to agree. - Send an Idempotency-Key header (a new UUID) with every POST; reuse it only to retry that same request. - Branch on the error's `code`, never its `message`. On 429, wait the seconds in Retry-After. - Never call anything the docs do not list. Tokens and account settings can never be changed through the API. ``` ## Prompts that work - "Read https://developers.infranest.io/recipes/dynamic-dns-from-a-script.md and write a script that keeps `home.example.com` pointing at this machine's address. Use the sandbox and dry-run first." - "Using https://developers.infranest.io/reference/monitoring.md, write a GitHub Actions step that pauses monitor `$MONITOR_ID` for the length of the deploy and resumes it after, even when the deploy fails." - "List every DNS record in the sandbox that points at `192.0.2.10`, then show me the dry run of moving them to `192.0.2.20`. Do not apply it." - "Write a Python function that calls the InfraNest API with retries: honour `Retry-After` on 429, retry 5xx with backoff, never retry other 4xx, and send the same `Idempotency-Key` on every retry of a POST." Give the agent the page that covers the task, not the whole site: it answers better from one relevant page than from everything at once. ## Connect it to your account To let the agent look things up while it works, add the MCP server with a read-only token: [Connect with a token](../ai-assistants.md#connect-with-a-token). It can never change anything through MCP; the scripts it writes are what change things, with the token you give them. --- # Give an assistant the docs To have an assistant *write code* against the API, rather than read your account, give it these docs. Everything on this site is published as plain Markdown as well, for exactly this. - [`/llms.txt`](/llms.txt) — every page of this site, with one line on what it is for. A good first thing to hand an assistant: it reads the index and fetches the pages it needs. - [`/llms-full.txt`](/llms-full.txt) — every page, the API reference included, in one file. For tools that read a whole site at once. - **Any page as Markdown** — add `.md` to its address, for example [`/getting-started.md`](/getting-started.md), or use **Copy page** at the top of the page. - **Open in Claude or ChatGPT** — the menu beside **Copy page** starts a new chat that asks about the page you are on, by its Markdown address. - [`/openapi.json`](/openapi.json) — the API description, for tools that generate clients or check requests. Also versioned in [infranest-openapi](https://github.com/InfraNest-Infrastructure-Organized/infranest-openapi). The Markdown twins of the reference pages show each call once, as `curl`, so a whole module fits in an assistant's context. The web pages show the same call in six languages. ## Tips - **One relevant page beats the whole site.** An assistant answers better from the reference page of the module it is working on than from `llms-full.txt`. - **Tell it which environment.** The docs use `https://api.infranest.io`; a sandbox uses the same address with a test token. - **Have it read the errors.** [Errors](../errors.md) lists every `code` with what to do about it, so generated code can branch on them. - For a coding agent working in a repository, add the [`AGENTS.md` snippet](coding-agents.md#an-agents-md-snippet). Search engines and AI crawlers may read and index this site; `robots.txt` names the AI crawlers explicitly so they are welcome. --- # What an assistant can and cannot do An assistant connected to InfraNest sees only what you could see yourself, narrowed to the organization and scopes you gave it, and it can never change anything. Every request it makes shows up in the audit log under the token's or the app's name. ## What it can do - **Read** what the [MCP server's tools](../ai-assistants.md#the-tools) cover: status, servers, monitors, incidents, DNS, domains, certificates, maintenance, the audit log and the uptime report — plus any other read in the [API reference](../reference.md) through `read_api`. - **Only in the organization you picked** (Connect with InfraNest), or the organizations the token reaches. - **Only with your role's permissions**, and only the scopes the token or connection holds. ## What it will not do - **Change anything.** Every tool is a read. An assistant that wants to change something has to tell you, and you, or a script you run, make the change. - **Read more than it may.** Scopes, the organization, your organization's IP allowlist and the [rate limits](../limits.md#rate-limits) apply exactly as they do to a script. An IP allowlist checks the address of the computer running the assistant — or, for a hosted assistant such as claude.ai or ChatGPT, the provider's. - **Hand out secrets.** Domain transfer codes, certificate files, agent and deploy tokens, status page subscriber lists and data exports are never returned through MCP, even when the token may read them directly. - **Follow instructions hidden in your data.** A record value or an incident note can contain text written by someone else. InfraNest tells the assistant to treat it as data; it is still worth reading what an assistant proposes before you act on it. ## Remove an assistant - **Connected with InfraNest** (claude.ai, ChatGPT): open **My account → Authorized apps** and select **Remove**. It loses access at once. Removing the connector inside Claude or ChatGPT stops it there; remove it in InfraNest too, so its access ends on our side as well. - **With a token** (Claude Code, Cursor, VS Code, Claude Desktop): revoke the token under **My account → Developer**. Every tool call with it fails from then on. An organization's owners can see each token's and each authorized app's requests in the [audit log](/reference/audit-log). --- # The MCP server Ask Claude, ChatGPT, Cursor or VS Code about your infrastructure — "which monitors were down last night?", "where does `mail.example.com` point?", "which domains expire this quarter?" — and let it look the answer up in InfraNest. The assistant connects to the InfraNest **MCP server**, which reads through the same API as your scripts. **It only reads.** Nothing an assistant does through it changes anything. The server's address is: ``` https://api.infranest.io/mcp ``` There are two ways to connect: - **In claude.ai and ChatGPT**, [Connect with InfraNest](ai/claude-and-chatgpt.md): add the address, sign in to InfraNest and approve. No token, every plan. - **In Claude Code, Cursor, VS Code and Claude Desktop**, [use an API token](#connect-with-a-token). The rest of this section: - [Connect Claude and ChatGPT](ai/claude-and-chatgpt.md) — with OAuth, step by step. - [Coding agents](ai/coding-agents.md) — an `AGENTS.md` snippet, prompts that work and safe defaults. - [Give an assistant the docs](ai/docs-for-assistants.md) — `llms.txt` and the Markdown twins. - [What an assistant can and cannot do](ai/safety.md) — and how to remove one. ## Connect with a token Claude Code, Cursor, VS Code and Claude Desktop take an API token. Create one under **My account → Developer**. Give it **read** scopes only for the areas you want the assistant to see — it cannot see more than the token can. In your [sandbox](sandbox.md), a test token works the same way, with sample data. The token goes in the header `Authorization: Bearer `. The links below add the server to your editor and read the token from the environment variable `INFRANEST_TOKEN`; neither link carries a token. ### Claude Code ```sh claude mcp add --transport http infranest https://api.infranest.io/mcp \ --header "Authorization: Bearer $INFRANEST_TOKEN" ``` ### Cursor **Add to Cursor** above, or in `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "infranest": { "url": "https://api.infranest.io/mcp", "headers": { "Authorization": "Bearer ${env:INFRANEST_TOKEN}" } } } } ``` ### VS Code **Add to VS Code** above reads `INFRANEST_TOKEN` from the environment VS Code was started from. To have VS Code ask for the token once and keep it in its secret storage instead, put this in `.vscode/mcp.json`: ```json { "inputs": [ { "type": "promptString", "id": "infranest-token", "description": "InfraNest API token", "password": true } ], "servers": { "infranest": { "type": "http", "url": "https://api.infranest.io/mcp", "headers": { "Authorization": "Bearer ${input:infranest-token}" } } } } ``` ### Claude Desktop The connector you add on claude.ai ([Connect Claude and ChatGPT](ai/claude-and-chatgpt.md)) is the simplest way. To use a token instead, use the `mcp-remote` bridge (it needs Node.js) in `claude_desktop_config.json`: ```json { "mcpServers": { "infranest": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.infranest.io/mcp", "--header", "Authorization: Bearer ${INFRANEST_TOKEN}"], "env": { "INFRANEST_TOKEN": "your token" } } } } ``` ## The tools Each tool is one read in the [API reference](reference.md), and needs that read's scope. This list is generated from the server itself. | Tool | What it answers | What it reads | Scope | |---|---|---|---| | `list_organizations` | List organizations | `GET /organizations` | `members.read` | | `status_overview` | Is everything up right now | `GET /organizations/{organization}/status` | `monitoring.read` | | `list_servers` | List servers | `GET /organizations/{organization}/cloud-servers` | `cloud.read` | | `get_server` | Get a server | `GET /organizations/{organization}/cloud-servers/{cloudServer}` | `cloud.read` | | `server_attention` | What needs attention on a server | `GET /organizations/{organization}/cloud-servers/{cloudServer}/attention` | `cloud.read` | | `server_metrics` | Servers with their latest metrics | `GET /organizations/{organization}/cloud-servers/metrics-overview` | `cloud.read` | | `list_monitors` | List monitors | `GET /organizations/{organization}/monitoring-checks` | `monitoring.read` | | `get_monitor` | Get a monitor | `GET /organizations/{organization}/monitoring-checks/{monitoringCheck}` | `monitoring.read` | | `monitor_stats` | Uptime and response times of a monitor | `GET /organizations/{organization}/monitoring-checks/{monitoringCheck}/stats` | `monitoring.read` | | `monitor_results` | Recent check results of a monitor | `GET /organizations/{organization}/monitoring-checks/{monitoringCheck}/results` | `monitoring.read` | | `list_incidents` | List incidents | `GET /organizations/{organization}/incidents` | `monitoring.read` | | `incident_timeline` | Timeline of an incident | `GET /organizations/{organization}/incidents/{incident}/updates` | `monitoring.read` | | `list_dns_zones` | List DNS zones | `GET /organizations/{organization}/dns-zones` | `dns.read` | | `list_dns_records` | List the records in a DNS zone | `GET /organizations/{organization}/dns-zones/{dnsZone}/dns-records` | `dns.read` | | `search_dns` | Search DNS records across all zones | `GET /organizations/{organization}/dns/search` | `dns.read` | | `list_domains` | List domains | `GET /organizations/{organization}/domains` | `domains.read` | | `list_certificates` | List certificates | `GET /organizations/{organization}/certificates` | `monitoring.read` | | `list_maintenance_windows` | List maintenance windows | `GET /organizations/{organization}/maintenance-windows` | `monitoring.read` | | `search_audit_log` | Search the audit log | `GET /organizations/{organization}/audit-logs` | `audit-log.read` | | `uptime_report` | Monthly uptime report | `GET /organizations/{organization}/uptime-report` | `monitoring.read` | | `read_api` | Read any public API endpoint | any read in the [API reference](/reference) | the scope of what it reads | When a token reaches more than one organization, the assistant asks `list_organizations` first and names the one it means. Connected with InfraNest, it reaches the one organization you picked. A long answer is cut off at 80,000 characters, with a note telling the assistant how to narrow it. ## Things to ask - "Is anything down right now?" - "Which monitors were down last night, and for how long?" - "Where does `mail.example.com` point, and when was that record last changed?" - "Which domains and certificates expire in the next 60 days?" - "Which servers are running out of disk space?" - "Who changed the DNS for `shop.example.com` this week?" - "What was our uptime last month, per monitor?" ## What it costs Nothing extra, on every plan — Free included. Each tool call is an API read, and counts against the same [rate limit](limits.md#rate-limits) as any other request with that token. **Connect with InfraNest** works on every plan. **Connecting with a token** needs a plan that includes API tokens (Pro or higher), or a test token in your [sandbox](sandbox.md). See [Limits](limits.md#what-each-plan-includes). When something does not work, see [Troubleshooting](troubleshooting.md#assistant-and-mcp-connection-problems). --- # Use with Postman, Bruno or Insomnia Every endpoint of the API is ready to import into the API client you already use, built from the same description as the [reference](reference.md). Each comes with an environment holding three variables: `baseUrl`, `token` and `organization_id`. None of the files contains a token. **Start with a test token from your [sandbox](sandbox.md).** Its sample data is safe to change, and nothing you send there reaches a provider or the internet. Switch to a live token once your requests do what you expect. | File | For | |---|---| | [`infranest.postman_collection.json`](/infranest.postman_collection.json) | Postman: every endpoint, one folder per reference page | | [`infranest.postman_environment.json`](/infranest.postman_environment.json) | Postman: the environment | | [`infranest.bruno.zip`](/infranest.bruno.zip) | Bruno: the collection as a folder of `.bru` files, with its environment | | [`openapi.json`](/openapi.json) | Insomnia, Bruno, Hoppscotch and anything else that imports OpenAPI 3 | The same files are kept, versioned, in [infranest-openapi](https://github.com/InfraNest-Infrastructure-Organized/infranest-openapi). ## Postman 1. Select **Import**, and drop in both `infranest.postman_collection.json` and `infranest.postman_environment.json`. 2. Pick the **InfraNest** environment in the top right. 3. Set `token` to your token (it is a secret variable, so Postman masks it) and `organization_id` to your organization's ID — the number after `/organizations/` in the app's address. 4. Open **Account → Get the signed-in user** and select **Send**: it works with any token, so it checks the token and the variables. Requests with an ID in the path, such as a zone's records, show it as a path variable (`:dnsZone_id`) under **Params**; fill it in there. ## Bruno 1. Unzip `infranest.bruno.zip`. It holds one folder, `infranest`. 2. In Bruno, select **Open collection** and pick the `infranest` folder. Keep it in your repository if you like: Bruno's files are made to be committed. 3. Pick the **InfraNest** environment and set `organization_id`. `token` is a secret: Bruno keeps its value on your machine, never in the files. 4. Send a request. Filters a request can take are listed under **Params**, switched off until you tick one. Bruno can also import `openapi.json` directly (**Import collection → OpenAPI**), if you prefer its own layout. ## Insomnia Insomnia imports the OpenAPI description itself: 1. Select **Import**, and give it `https://developers.infranest.io/openapi.json` (or the downloaded file). 2. In the environment it creates, set the base URL to `https://api.infranest.io` and add your token as a Bearer token on the collection. ## Generating a client `openapi.json` also feeds code generators. For example, with [openapi-generator](https://openapi-generator.tech): ```sh npx @openapitools/openapi-generator-cli generate \ -i https://developers.infranest.io/openapi.json -g typescript-fetch -o ./infranest-client ``` Use `-g python`, `-g php`, `-g go` or `-g csharp` for those languages. Each operation carries `x-infranest-scope`, the scope a token needs for it. --- # Changelog Every change to the public API, newest first, each dated. Follow it as RSS at [`/changelog.xml`](/changelog.xml). How changes are made and announced is in [Versioning and deprecation](versioning.md). ## Our promise - We add fields and endpoints without notice. Write clients that ignore fields they don't know. - We never remove or rename a field, or change what it means, without **six months' notice** here. - Error `code` values never change. New ones may be added. ## Webhooks have one signature; page links keep your filters - **One webhook signature, `X-InfraNest-Signature-V2`.** Every request InfraNest sends to your endpoint — a test delivery, an event an endpoint is subscribed to, and an event sent by an automation, including the **Send everything to …** automation a new endpoint starts with — now carries the same headers: `X-InfraNest-Event`, `X-InfraNest-Delivery` (when there is a delivery), `X-InfraNest-Timestamp` and `X-InfraNest-Signature-V2`, the HMAC-SHA256 of `"."`. Verify that one header and refuse a timestamp more than five minutes from your clock. Until now an event sent by an automation was signed over the body alone, with no timestamp, so a receiver built against a test delivery rejected it. - **Deprecated: `X-InfraNest-Signature`.** It keeps exactly what it carried before — the timestamped form on test and subscription deliveries, the body-only form on events sent by an automation — until **30 April 2027**, and is then no longer sent. A receiver that verifies it should switch to `X-InfraNest-Signature-V2` before then. - **A failed delivery is retried five times**, the last one eight hours after the one before it (30 seconds, 5 minutes, 30 minutes, 2 hours, 8 hours). It was four: the eight-hour retry was listed and never made. - **`next_page_url`, `prev_page_url`, `first_page_url`, `last_page_url` and `links` keep the request's filters, `sort`, `dir` and `per_page`.** They carried only `page`, so following them returned the unfiltered list from page two on. Follow them as they are. ## First public version The InfraNest API and these docs are public. - **Read** your domains, DNS zones and records, servers, monitors, incidents, maintenance windows, status pages, certificates and audit log with an API token. - **Change** DNS records, monitors, incidents, maintenance windows, status page content, tags and custom fields, in organizations where API writes are switched on. - **Tokens** are scoped per area, work in the organization they were created in, and can never manage your account or order anything. They expire after 30 to 365 days and can be renewed without changing them. - **Safe changes:** an `Idempotency-Key` header makes a retried `POST` happen once, and `?dry_run=1` shows what a DNS change would do. See [Retries and dry runs](retries-and-dry-runs.md). - **Attribution:** audit log entries made through a token carry `via_token_id` and `via_token_name`, and `GET /audit-logs` takes `token_id`. See [Seeing what a token does](tokens-and-scopes.md#seeing-what-a-token-does). - **Settings and secrets are app-only:** single sign-on, notification settings, webhook addresses, plan previews, promo codes, saved payment details and the set-up screens of integrations and automations answer a token with [`session_only`](errors.md#session_only), and are no longer in the reference. Dashboards can be read with a token, but not changed. Invitation lists no longer include the acceptance token. - **Every endpoint is described** in plain language, written for this reference. - **Every endpoint shows a real answer**: the response samples, and the request bodies in the examples, are recorded from the API itself, reads and writes, so a field in an example is a field the API sends or accepts. - **Sandbox:** a free organization with sample data where test tokens (`in_test_…`) try every write and nothing reaches a provider or the internet. See [Sandbox](sandbox.md). - **Status and history:** `GET /status` answers whether everything is up in one call, `aggregates` gives a monitor's uptime and p50/p95 response times for any period, and monitoring results and server metrics take `from`, `to`, `resolution` and pages, refusing a range too long for the resolution with [`range_too_large`](errors.md#range_too_large). See [Status, uptime and history](recipes/status-and-uptime.md). - **AI assistants:** a read-only MCP server at `https://api.infranest.io/mcp` lets Claude, ChatGPT, Cursor and VS Code read your account. In claude.ai and ChatGPT, add it as a custom connector and **Connect with InfraNest** — no token to paste; editors use an API token. See [AI assistants](ai-assistants.md). - **OAuth (Connect with InfraNest):** an app can ask people to connect it to their InfraNest account. The authorization code flow with PKCE at `/oauth/authorize` and `/oauth/token`, one organization per connection, access tokens for an hour and rotating refresh tokens for 30 days, discovery at `/.well-known/oauth-authorization-server`, and self-registration for AI assistants. People remove an app under **My account → Authorized apps**. See [OAuth for integrations](oauth.md). - **Scopes** are the same for API tokens and OAuth apps, listed in the generated [scopes table](scopes.md). - **Rate limits** per token: 300 requests and 60 changes a minute, and 1,000 changes an hour, by default, reported in `X-RateLimit-*` headers on every response. - **Errors** carry a stable `code`, a hint and a reference. See [Errors](errors.md). --- # Errors Every error from the InfraNest API has the same shape and a stable `code`. Write your scripts against the code: the message is for people and may be reworded, the code never changes. ```json { "code": "scope_missing", "message": "This API token does not have the dns.write scope.", "hint": "Create a token that includes the scope named in \"required\". Give each token only the scopes it needs.", "retryable": false, "docs_url": "https://developers.infranest.io/errors#scope_missing", "reference": "k3j9x0b2qa", "required": "dns.write" } ``` | Field | Meaning | |---|---| | `code` | What kind of error this is. Stable: new codes are added, existing ones never change. | | `message` | A sentence for people. May be reworded. | | `hint` | What to do about it. | | `retryable` | Whether sending the same request again later can work without changing anything. | | `docs_url` | This page, at the code's entry. | | `reference` | The request's ID. Quote it when you contact support. Also in the `X-Request-Id` header. | | `retry_after` | Seconds to wait, when the error says to wait. Also in the `Retry-After` header. | | `required` | The token scope that was missing (`scope_missing`). | | `required_permission` | The role permission that was missing (`permission_denied`). | | `limit_key`, `limit`, `usage` | Which plan limit was hit, and how much of it is used (`plan_limit`). | | `errors` | Each invalid field and what is wrong with it (`validation_failed`). | | `max_range_seconds`, `resolution`, `resolutions` | The longest range the asked resolution takes, and the resolutions that would fit the range you sent (`range_too_large`). | Some endpoints answer a code of their own, specific to what they do. Those are listed on the endpoint's reference page. For problems that are not one error code — a token that stopped working, an assistant that cannot connect — see [Troubleshooting](troubleshooting.md). ## Codes | Code | Status | Retry? | |---|---|---| | [`unauthenticated`](#unauthenticated) | 401 | No | | [`scope_missing`](#scope_missing) | 403 | No | | [`session_only`](#session_only) | 403 | No | | [`not_token_writable`](#not_token_writable) | 403 | No | | [`write_disabled`](#write_disabled) | 403 | No | | [`wrong_organization`](#wrong_organization) | 403 | No | | [`token_ip_not_allowed`](#token_ip_not_allowed) | 403 | No | | [`test_token_required`](#test_token_required) | 403 | No | | [`test_token_sandbox_only`](#test_token_sandbox_only) | 403 | No | | [`api_token_required`](#api_token_required) | 403 | No | | [`sandbox_personal`](#sandbox_personal) | 409 | No | | [`sandbox_needs_organization`](#sandbox_needs_organization) | 409 | No | | [`organization_offline`](#organization_offline) | 409 | No | | [`permission_denied`](#permission_denied) | 403 | No | | [`forbidden`](#forbidden) | 403 | No | | [`organization_suspended`](#organization_suspended) | 403 | No | | [`read_only_session`](#read_only_session) | 403 | No | | [`sso_required`](#sso_required) | 403 | No | | [`two_factor_setup_required`](#two_factor_setup_required) | 403 | No | | [`password_reset_required`](#password_reset_required) | 403 | No | | [`email_verification_required`](#email_verification_required) | 403 | No | | [`plan_limit`](#plan_limit) | 402 | No | | [`not_found`](#not_found) | 404 | No | | [`method_not_allowed`](#method_not_allowed) | 405 | No | | [`conflict`](#conflict) | 409 | No | | [`validation_failed`](#validation_failed) | 422 | No | | [`unprocessable`](#unprocessable) | 422 | No | | [`range_too_large`](#range_too_large) | 422 | No | | [`rate_limited`](#rate_limited) | 429 | Yes | | [`idempotency_key_reused`](#idempotency_key_reused) | 422 | No | | [`idempotency_in_progress`](#idempotency_in_progress) | 409 | Yes | | [`provider_refused`](#provider_refused) | 4xx / 502 | No | | [`provider_rate_limited`](#provider_rate_limited) | 429 | Yes | | [`provider_unavailable`](#provider_unavailable) | 502 | Yes | | [`maintenance`](#maintenance) | 503 | Yes | | [`server_error`](#server_error) | 500 | Yes | ### unauthenticated {#unauthenticated} You are not signed in, or the API token is missing, wrong, expired or revoked. **Why it happens:** No Authorization header was sent, the token was mistyped or cut off, or it has expired or been revoked. New tokens expire after 90 days unless another period was chosen. **What to do:** Send a valid token in the Authorization header, as the word Bearer followed by the token. Create a new one under My account → Developer if it expired or was revoked. **Retry the same request:** No ### scope_missing {#scope_missing} This API token does not have the scope this request needs. **Why it happens:** The token was created without the scope for this area, or with only the read scope where the request changes something. **What to do:** Create a token that includes the scope named in "required". Give each token only the scopes it needs. **Retry the same request:** No ### session_only {#session_only} This can only be done in the InfraNest app while signed in, not with an API token. **Why it happens:** The address belongs to the part of InfraNest a token may never use: account and security settings, and API tokens themselves. **What to do:** Do this in the app while signed in. Account and security settings can never be changed with a token. **Retry the same request:** No ### not_token_writable {#not_token_writable} This action is only available in the InfraNest app, not with an API token. **Why it happens:** The request changes something that only the app can change, such as provider actions that cost money or settings with no API. **What to do:** Do this in the app while signed in. The developer docs list everything a token can change. **Retry the same request:** No ### write_disabled {#write_disabled} API write access is not switched on for this organization. **Why it happens:** The token may write, but writes through the API are switched on per organization, and they are not on for this one. **What to do:** Ask an owner of the organization to switch on API write access. Reads keep working meanwhile. **Retry the same request:** No ### wrong_organization {#wrong_organization} This API token is bound to another organization. **Why it happens:** The token was created for one organization, and the address names another. **What to do:** Use a token created for this organization, or one created for all your organizations. **Retry the same request:** No ### token_ip_not_allowed {#token_ip_not_allowed} This API token cannot be used from this address. **Why it happens:** The token is limited to a list of addresses, and this request came from another one: often a CI runner, a VPN, or IPv6 where only an IPv4 address was listed. **What to do:** Call from one of the addresses the token is limited to, or change its address list under My account → Developer. **Retry the same request:** No ### test_token_required {#test_token_required} This is a sandbox organization, and only its own test tokens work here. **Why it happens:** A live token (in_live_…) was sent to a sandbox organization. **What to do:** Create a token while working in the sandbox organization; it starts with in_test_. Live tokens never work in a sandbox. **Retry the same request:** No ### test_token_sandbox_only {#test_token_sandbox_only} This is a test token, and it works only in its sandbox organization. **Why it happens:** A test token (in_test_…) was sent to a live organization. **What to do:** Use a live token for live organizations. Test tokens (in_test_…) are for the sandbox only. **Retry the same request:** No ### api_token_required {#api_token_required} The MCP server takes an API token, not a signed-in session. **Why it happens:** The MCP server was called with a signed-in session rather than an API token or an assistant's OAuth connection. **What to do:** Create an API token in My account → Developer and send it in the Authorization header, after the word Bearer. **Retry the same request:** No ### sandbox_personal {#sandbox_personal} A sandbox is personal: nobody can be invited into it, and it cannot be subscribed or handed to someone else. **Why it happens:** The request tried to invite someone into a sandbox, subscribe it to a plan or hand it to another account. **What to do:** Do this in one of your live organizations instead. **Retry the same request:** No ### sandbox_needs_organization {#sandbox_needs_organization} A sandbox belongs with an organization of your own. **Why it happens:** The account tried to create a sandbox before belonging to any organization. **What to do:** Create or join an organization first, then make your sandbox. **Retry the same request:** No ### organization_offline {#organization_offline} This organization does not reach the internet, so nothing was looked up or sent. **Why it happens:** The request needed something from outside — a provider, a DNS lookup, a message — and this organization is a sandbox, a demo or suspended, which never call out. **What to do:** A sandbox, a demo or a suspended organization never calls out. Try it in a live organization. **Retry the same request:** No ### permission_denied {#permission_denied} Your role in this organization does not allow this. **Why it happens:** A token acts as the person who created it, so their role in the organization applies, and that role does not include this. **What to do:** Ask an owner to give your role the permission named in "required_permission". **Retry the same request:** No ### forbidden {#forbidden} You are not allowed to do this. **Why it happens:** A refusal without a more specific reason, most often the wrong organization or the wrong account. **What to do:** Check that you are using the right organization and account. **Retry the same request:** No ### organization_suspended {#organization_suspended} This organization is suspended, so nothing in it can be changed. **Why it happens:** The organization has been suspended. Changes are refused; reading still works. **What to do:** Contact InfraNest support to restore the organization. Reading data still works. **Retry the same request:** No ### read_only_session {#read_only_session} This session is read-only. **Why it happens:** The session is a read-only support view of the account. **What to do:** Someone is viewing this account in read-only support mode; changes must be made by the account holder. **Retry the same request:** No ### sso_required {#sso_required} This organization requires you to sign in with its single sign-on. **Why it happens:** The organization requires single sign-on, and this session signed in another way. **What to do:** Sign in with one of the methods listed in "connections", then try again. **Retry the same request:** No ### two_factor_setup_required {#two_factor_setup_required} This organization requires two-factor authentication on your account. **Why it happens:** The organization requires two-factor authentication, and the account has not set it up yet. **What to do:** Set up two-factor authentication in the app under My account → Security. **Retry the same request:** No ### password_reset_required {#password_reset_required} Your password has expired and must be changed. **Why it happens:** The account's password has to be changed before anything else can be done. **What to do:** Change your password in the app, then try again. **Retry the same request:** No ### email_verification_required {#email_verification_required} Your email address has not been confirmed yet. **Why it happens:** The account's email address has not been confirmed. **What to do:** Open the confirmation link we emailed you, or ask for a new one in the app. **Retry the same request:** No ### plan_limit {#plan_limit} Your plan does not include this, or its limit has been reached. **Why it happens:** The organization's plan does not include this feature, or a counted allowance (API tokens, monitors, domains…) is used up. **What to do:** Upgrade the plan, or free up the limit named in "limit_key" ("usage" of "limit" is used). **Retry the same request:** No ### not_found {#not_found} Nothing exists at this address, or you cannot see it. **Why it happens:** A wrong ID in the address, something that was deleted, or something in another organization, which answers as not found on purpose. **What to do:** Check the IDs in the address. A resource in another organization answers as not found. **Retry the same request:** No ### method_not_allowed {#method_not_allowed} This address does not accept that HTTP method. **Why it happens:** The HTTP method does not match the address, for example PUT where only PATCH is accepted. **What to do:** Check the method in the API reference for this address. **Retry the same request:** No ### conflict {#conflict} This conflicts with something that already exists. **Why it happens:** Something with the same name already exists, or the resource's current state does not allow the change. **What to do:** Fetch the current state, resolve the conflict, then send the request again. **Retry the same request:** No ### validation_failed {#validation_failed} Some fields are missing or not valid. **Why it happens:** A required field is missing, or a value has the wrong type or format. **What to do:** Fix the fields listed in "errors" and send the request again. **Retry the same request:** No ### unprocessable {#unprocessable} The request could not be carried out as sent. **Why it happens:** The request is well-formed, but cannot be carried out in the current state of what it refers to. **What to do:** Read "message" for the reason; sending the same request again will fail the same way. **Retry the same request:** No ### range_too_large {#range_too_large} The time range is longer than this resolution allows. **Why it happens:** The time between "from" and "to" is longer than the asked resolution allows. **What to do:** Shorten the time between "from" and "to" to at most "max_range_seconds", or ask for a coarser "resolution"; "resolutions" lists the ones that would take the range you sent. **Retry the same request:** No ### rate_limited {#rate_limited} Too many requests. Slow down and try again shortly. **Why it happens:** The token sent more requests or changes than its plan allows (300 requests and 60 changes a minute, and 1000 changes an hour, by default), often a loop without a pause. "limit_key" says which. **What to do:** Wait the number of seconds in "retry_after" (also the Retry-After header), then retry. **Retry the same request:** Yes ### idempotency_key_reused {#idempotency_key_reused} This Idempotency-Key was already used for a different request. **Why it happens:** The same Idempotency-Key was sent again within 24 hours with a different method, address, query or body. **What to do:** Use a new key for every new request. Send the same key only to retry the exact same request. **Retry the same request:** No ### idempotency_in_progress {#idempotency_in_progress} A request with this Idempotency-Key is still being handled. **Why it happens:** A retry arrived while the first request with the same Idempotency-Key was still running. **What to do:** Wait a few seconds, then send the same request with the same key again to get its result. **Retry the same request:** Yes ### provider_refused {#provider_refused} The provider refused the request. **Why it happens:** The registrar, DNS or cloud provider turned the request down, most often because of an expired credential or a setting in your account there. **What to do:** Read "message" and "fix" for what the provider wants changed, usually in your account with that provider. **Retry the same request:** No ### provider_rate_limited {#provider_rate_limited} The provider is limiting requests right now. **Why it happens:** The provider is limiting how often your account with it may be called. **What to do:** Wait a minute and retry. If it keeps happening, spread the calls out. **Retry the same request:** Yes ### provider_unavailable {#provider_unavailable} The provider could not be reached or is having problems. **Why it happens:** The provider timed out or answered with an error of its own. **What to do:** Retry in a few minutes. Check status.infranest.io and the provider's own status page. **Retry the same request:** Yes ### maintenance {#maintenance} InfraNest is down for maintenance. **Why it happens:** InfraNest is being updated or maintained. **What to do:** Retry after the time in "retry_after". Check status.infranest.io for progress. **Retry the same request:** Yes ### server_error {#server_error} Something went wrong on our side. **Why it happens:** Something unexpected failed on our side. It is logged under the request's reference. **What to do:** Retry once. If it fails again, contact support and quote "reference". **Retry the same request:** Yes --- --- description: Quick answers about the InfraNest API — finding your organization ID, the base address, common token errors, rate limits, cost and reporting a security problem. --- # FAQ ## Where do I find my organization ID? In the app, under **Settings → Organization**, with a copy button. Or ask the API, with any token: ```sh curl https://api.infranest.io/api/organizations \ -H "Authorization: Bearer $INFRANEST_TOKEN" ``` Every organization the token can reach is listed with its `id`. A token made for one organization lists only that one. ## Why an ID and not the organization's name or slug? Names and slugs can change when someone renames the organization. An ID never changes, so a script written today keeps working. ## Which base address do I use? `https://api.infranest.io/api`. Every path in the reference starts after it. ## My token gets `wrong_organization`. Why? Tokens work in the organization they were created in. Create a token in the organization you want to reach, or one for all your organizations (see [Tokens and scopes](tokens-and-scopes.md)). ## My token gets `write_disabled`. Why? API writes are switched on per organization during the rollout. Reading works meanwhile. Ask us to switch your organization on. ## My token suddenly gets `unauthenticated`. Why? It has most likely expired, or someone revoked it. **My account → Developer** shows each token's expiry date. Renew it before it expires to keep the same token; once it has expired, create a new one. See [Expiry](tokens-and-scopes.md#expiry). ## I get `429 rate_limited`. What now? Wait the number of seconds in the `Retry-After` header, then retry. To avoid it, watch `X-RateLimit-Remaining` and slow down near zero. A script that changes many records usually only needs a short pause between changes. See [Rate limits](tokens-and-scopes.md#rate-limits). ## Does using the API cost extra? The API is part of plans that include it; requests themselves are not charged by InfraNest. What a token does can still cause costs at your providers; see [Responsibility and costs](responsibility-and-costs.md). ## How do I report a security problem? Email `security@infranest.io`. Please don't open a public issue. --- # Getting started The InfraNest API lets you automate the infrastructure you manage in InfraNest: DNS records, monitors, maintenance windows, incidents and status pages. This guide takes you from no token to your first call in about five minutes. ## Before you start - An InfraNest account on a plan that includes the API. - Your organization's ID. It's under **Settings → Organization** in the app, with a copy button. Or ask the API: `GET /api/organizations` lists every organization your token can reach. See the [FAQ](faq.md). - A terminal with `curl`. The API lives at `https://api.infranest.io/api`. Every request sends a token and asks for JSON. There are two ways in. An **API token**, used in this guide, is for your own scripts and tools and acts as you. An app that acts for other people — an integration, a hosted service — uses [OAuth](oauth.md): each person connects it to their own account, and nobody copies a token. ## Create a token 1. In the app, open **My account → Developer** and choose **New token**. 2. Give it a name that says where it will be used, for example `ci-dns`. 3. Give it only what it needs. For this guide, tick **DNS → Read**. 4. Copy the token. It's shown once. Store it where your scripts read secrets, never in code. A token works with your infrastructure, never with your account: tokens, members, billing and passwords stay in the app. Some actions are not available to a token at all; see [Tokens and scopes](tokens-and-scopes.md). What you do with a token is your responsibility, and it can cause costs at your providers; see [Responsibility and costs](responsibility-and-costs.md). ## Make your first call List the DNS zones in your organization. Replace `42` with your organization's ID. ```sh curl https://api.infranest.io/api/organizations/42/dns-zones \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` You get back the zones in that organization, each with an `id` you use to work with its records. ## When something goes wrong Every error has the same shape and a stable `code`, with a `hint` saying what to do: ```json { "code": "scope_missing", "message": "This API token does not have the dns.write scope.", "hint": "Create a token that includes the scope named in \"required\". Give each token only the scopes it needs.", "retryable": false, "required": "dns.write" } ``` Write your scripts against `code`, never the message. The full list is in [Errors](errors.md). ## What next - [Let's Encrypt with InfraNest DNS](recipes/acme-dns-01.md) - [Install the server agent on many servers](agent/install-on-many-servers.md) - [Every error code](errors.md) - [Build an app people connect with OAuth](oauth.md) --- # Limits Every limit the API applies, in one place, so a script can stay inside them instead of finding them in a `429`. The numbers on this page are generated from the API's own code at build time; they cannot drift from what it enforces. ## What each plan includes | | Free | Pro | Business | Enterprise | |---|---|---|---|---| | An assistant reads through the MCP server (Connect with InfraNest) | Yes | Yes | Yes | Yes | | API tokens: read | No | Yes | Yes | Yes | | API tokens: write | No | Yes | Yes | Yes | | API tokens per organization | 0 | 5 | 20 | Unlimited | | Requests per minute, per token | 300 | 300 | 300 | 300 | | Changes per minute, per token | 60 | 60 | 60 | 60 | | Changes per hour per hour, per token | 1000 | 1000 | 1000 | 1000 | These are the standard plans. An organization on a plan agreed separately may have other numbers; its own limits are under **Settings → Subscription** in the app. API tokens count across the organization's members. Your [sandbox](sandbox.md) takes test tokens on every plan. **Free** includes the read-only assistant: Claude or ChatGPT [connected with InfraNest](ai/claude-and-chatgpt.md) can read your account. Calling the API yourself, with a token, starts at **Pro**, for reads and writes alike. Writes also need API writes switched on for the organization ([`write_disabled`](errors.md#write_disabled)). ## Rate limits Each token has its own budget, counted per window: | Budget | Default | What counts | |---|---|---| | Requests | 300 per minute | Every request | | Changes | 60 per minute | Every `POST`, `PUT`, `PATCH` and `DELETE` (these also count as requests) | | Changes per hour | 1,000 per hour | Every `POST`, `PUT`, `PATCH` and `DELETE`, over a rolling hour | Every response to a token carries the budget that is closest to running out: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 57 X-RateLimit-Reset: 1793030460 ``` `X-RateLimit-Reset` is a Unix time. Over a budget, the API answers `429` with the code [`rate_limited`](errors.md#rate_limited), the `Retry-After` header and `retry_after` in the body (seconds), and `limit_key` naming the budget: ```json { "code": "rate_limited", "message": "This token made more than 60 changes in a minute.", "limit_key": "api_writes_per_minute", "limit": 60, "retry_after": 12, "retryable": true } ``` Wait that long and carry on. A dry run counts as a change. The app's own sessions are not counted here. ## Everything else | Limit | Value | |---|---| | Token lifetime | 90 days unless you choose another: 30, 90, 180, 365 days. Never "no expiry". | | Page size of a list | 25 by default, at most 200 (`per_page`). A larger number is lowered to 200. A few lists differ and say so on their reference entry. | | `Idempotency-Key` | 1 to 255 visible ASCII characters. A successful answer is kept for 24 hours. | | Request body | JSON. Up to 8 MB: PHP's default limit, which the API does not change. | | One MCP answer | At most 80,000 characters; a longer answer is cut off with a note on how to narrow it. | | OAuth access token | 60 minutes, renewed with the refresh token. | | OAuth refresh token | 30 days, used once: each refresh returns a new one. An assistant not used for that long has to be connected again. | | OAuth authorization code | 10 minutes, used once. | | Write spike warning | The organization's owners are told once a day when a token makes more than 10 times its usual daily writes (at least 200; 1,000 for a token less than 3 days old). Nothing is blocked. | Lists are described in [Pagination and filtering](pagination.md); keys and retries in [Retries and dry runs](retries-and-dry-runs.md); tokens in [Tokens and scopes](tokens-and-scopes.md). --- # OAuth for integrations Build an app that works in InfraNest for other people. With OAuth, the person clicks **Connect with InfraNest** in your app, approves it in InfraNest, and your app receives a token. Nobody creates or copies an API token, and they can remove your app whenever they like. InfraNest is an OAuth 2.1 authorization server: the authorization code flow with PKCE, and refresh tokens. ## OAuth or an API token? | Use | When | |---|---| | **An API token** | Your own scripts, CI and tools, acting as you. See [Getting started](getting-started.md). | | **OAuth** | An app other people connect to their own InfraNest account: an integration, a hosted service, an AI assistant. | Both reach the same API with the same [scopes](scopes.md), and the same limits apply: what a token can never do, the rate limits and your organization's IP allowlist. An OAuth token can do no more than the person who approved it could do themselves. ## How it works 1. Your app sends the person's browser to InfraNest's **authorize** address. 2. They sign in, if they are not already, and see a **consent page**: your app's name, the address it sends them back to, and what it asks to do. They pick **one organization** your app may use. A line their role in that organization cannot grant is struck out, and your app does not get it. 3. They select **Allow**. InfraNest sends the browser back to your redirect URI with a **code**. 4. Your app exchanges the code for an **access token** and a **refresh token**. 5. Your app calls the API with the access token, and refreshes it when it expires. The token works in the organization they picked, and only there. To use a second organization, they connect again and pick the other one. ## Before you start You need a **client ID** and your **redirect URI** registered with it. See [Getting a client](#getting-a-client). PKCE is required for every client. Make a fresh **code verifier** for each sign-in: a random string of 43 to 128 characters. The **code challenge** is its SHA-256 hash, base64url-encoded without padding: ```sh CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/\n' | cut -c1-64) CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n') STATE=$(openssl rand -hex 16) ``` Keep `CODE_VERIFIER` and `STATE` with the person's session in your app. You need both when they come back. ## 1. Send the person to InfraNest Open this address in their browser (shown on several lines here; it is one URL): ``` https://api.infranest.io/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fapp.example.com%2Finfranest%2Fcallback &scope=dns.read%20monitoring.read &state=STATE &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 ``` | Parameter | Value | |---|---| | `response_type` | Always `code`. | | `client_id` | Your client ID. | | `redirect_uri` | Exactly one of the redirect URIs registered for your client. | | `scope` | The [scopes](scopes.md) your app needs, separated by spaces. Ask for the fewest that work. | | `state` | A random value. InfraNest sends it back unchanged; check that it matches. | | `code_challenge` | The code challenge from above. | | `code_challenge_method` | Always `S256`. | If the client ID is unknown, or the redirect URI is not one registered for it, InfraNest shows an error and never sends the person to your app (see [Errors](#errors)). Any other problem is sent back to your redirect URI as `?error=…&error_description=…&state=…`. ## 2. The person approves InfraNest shows its consent page. You do not build anything for this step. The page tells the person: - your app's name and the host of your redirect URI, and whether InfraNest has **verified** the app; - what it asks for: **See** for a read scope, **See and change** for a write scope. An app that asks for full access (`*`) gets an extra confirmation; - which organization it may use, and what their role there lets them grant. The consent page is valid for five minutes and works once. If the person waits longer, send them to the authorize address again. They select **Allow** and come back to your redirect URI: ``` https://app.example.com/infranest/callback?code=def50200a1b2...&state=STATE ``` Or they select **Cancel**: ``` https://app.example.com/infranest/callback?error=access_denied&state=STATE ``` Check that `state` matches the one you stored before you do anything else. If it does not, stop. ## 3. Exchange the code The code is valid for ten minutes and works once. Exchange it at the token endpoint with the code verifier: ```sh curl -X POST https://api.infranest.io/oauth/token \ -H "Accept: application/json" \ -d grant_type=authorization_code \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d redirect_uri=https://app.example.com/infranest/callback \ -d code=THE_CODE \ -d code_verifier=$CODE_VERIFIER ``` Leave out `client_secret` if your client has none (a public client, such as an AI assistant or a desktop app). A client with a secret may also send it as HTTP Basic authentication instead. ```json { "token_type": "Bearer", "expires_in": 3600, "access_token": "", "refresh_token": "" } ``` Treat both tokens as opaque strings. Their format may change. ## 4. Call the API Send the access token exactly as you would an API token: ```sh curl https://api.infranest.io/api/organizations \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Accept: application/json" ``` `GET /api/organizations` answers with the organization the person picked: use its `id` in every other path, for example `/api/organizations/42/dns-zones`. Everything in the [API reference](reference.md) works, within the scopes granted. Changes your app makes appear in the organization's audit log under your app's name. ## 5. Refresh the token An access token lasts **one hour**. Before it expires, or when the API answers `401`, refresh it: ```sh curl -X POST https://api.infranest.io/oauth/token \ -H "Accept: application/json" \ -d grant_type=refresh_token \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d refresh_token=$REFRESH_TOKEN ``` The answer has the same shape as the code exchange: a new access token **and a new refresh token**. - **Refresh tokens rotate.** Each one works once. The refresh revokes the one you sent, so save the new one before you use the new access token. If two parts of your app refresh at the same time, one of them gets `invalid_grant`: let one place do the refreshing. - **A refresh token lasts 30 days.** Each refresh starts a new 30 days, so an app that is used keeps working. One unused for 30 days has to send the person through the flow again. - **The scopes and the organization stay as approved.** To ask for more, send the person through the flow again. ## When access ends The person can remove your app at any time under **My account → Authorized apps**, and an organization's owners and admins can remove it under **Settings → Authorized apps**. Leaving the organization, or being removed from it, ends it too. Your app sees this as: - the API answering `401` with the code [`unauthenticated`](errors.md#unauthenticated), and - the refresh then answering `400` with `invalid_grant`. Then stop using the stored tokens, tell the person the connection ended, and offer **Connect with InfraNest** again. ## Discovery Clients that read OAuth metadata, such as MCP clients, find everything at: ```sh curl https://api.infranest.io/.well-known/oauth-authorization-server ``` ```json { "issuer": "https://api.infranest.io", "authorization_endpoint": "https://api.infranest.io/oauth/authorize", "token_endpoint": "https://api.infranest.io/oauth/token", "registration_endpoint": "https://api.infranest.io/oauth/register", "response_types_supported": ["code"], "code_challenge_methods_supported": ["S256"], "scopes_supported": ["mcp:use"], "grant_types_supported": ["authorization_code", "refresh_token"], "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"], "client_id_metadata_document_supported": true } ``` The [MCP server](ai-assistants.md) describes itself at `https://api.infranest.io/.well-known/oauth-protected-resource/mcp`, and its `401` names that address in the `WWW-Authenticate` header. `scopes_supported` lists only `mcp:use`, the scope an assistant asks for; every scope in the [scopes table](scopes.md) can be requested. ## Scopes OAuth uses the same scopes as API tokens: see the [scopes table](scopes.md). Write includes read. Two things are particular to OAuth: - **`mcp:use`** lets a client use the MCP server and grants nothing else. A request for `mcp:use` alone is shown to the person as every read scope, because the MCP server only reads. - **What the person may grant decides.** Your app gets the scopes it asked for that the person's role in the chosen organization allows, and no others. Check what you can do from the API's answers: a scope you were not granted answers [`scope_missing`](errors.md#scope_missing). ## Lifetimes | What | Lasts | |---|---| | Consent page | 5 minutes, once | | Authorization code | 10 minutes, once | | Access token | 1 hour | | Refresh token | 30 days, once; each refresh issues a new one | ## Getting a client **AI assistants with a published identity need no registration.** If your client publishes a [Client ID Metadata Document](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00), send its URL as the `client_id` — on the authorize request and at the token endpoint. Claude does this with **Use Claude's published identity** (`https://claude.ai/oauth/mcp-oauth-client-metadata`), and ChatGPT with `https://chatgpt.com/oauth/…/client.json`. We accept documents from a fixed list of hosts, currently `claude.ai` and `chatgpt.com`; for another, email support@infranest.io. The document must: - name itself: its `client_id` is exactly the URL it is served from (https, with a path); - have a `client_name` and `redirect_uris` (https, or `http` on a loopback address), and the redirect URI you send must be one of them exactly; - be a public client: no `client_secret`, and `token_endpoint_auth_method` `none` (or left out). Prove yourself with PKCE `S256` as every client does. The consent page shows the app under its document's name as **Verified by InfraNest** — unless every redirect URI is a loopback address, which any program on the person's machine could be listening on. If we cannot accept the document, the authorize request answers `400` with `code: invalid_client` and a `reason` (`host_not_allowed`, `not_https`, `malformed_url`, `client_id_mismatch`, `invalid_document`, `unsupported_auth`, or `unreachable` when we could not fetch it). We cache a document for its `Cache-Control: max-age` (between a minute and a day, an hour if it sends none), so a change to it reaches us within that time. **Other AI assistants register themselves.** Claude, ChatGPT and other MCP clients call `POST https://api.infranest.io/oauth/register` the first time someone adds InfraNest, so there is nothing to set up. Self-registration is open to a fixed list of redirect addresses — `https://claude.ai`, `https://chatgpt.com`, `https://chat.openai.com`, and the `cursor://`, `vscode://` and `claude://` schemes — and limited to 10 registrations an hour from one address. The consent page shows such an app as **Not verified by InfraNest**. A name containing "InfraNest", or the name of a verified app, is refused (`invalid_client_metadata`), so a self-registered app cannot pass itself off as us or as an integration we verified. A self-registered client that obtains no token for 30 days is removed. A consent link belongs to the first person who opens it: anyone else who opens or answers it sees it as expired. **Integrations get a verified client from us.** Email **support@infranest.io** with: - your app's name, as people should see it on the consent page, and a logo; - your redirect URIs: `https`, or `http` only on `localhost` or `127.0.0.1` while you develop; - the scopes your app needs. We register the app and send you a client ID and a **client secret**. The secret is shown once; store it like a password. The consent page then shows your app as **Verified by InfraNest**. When you need a new secret, ask: the previous one keeps working for seven days, so you can switch without an outage. ## Errors | Where | Answer | What it means | |---|---|---| | Authorize | `400 {"code": "invalid_client"}` | The client ID is unknown, or its registration was removed. | | Authorize | `400 {"code": "invalid_redirect_uri"}` | `redirect_uri` is not one registered for this client. InfraNest never redirects to it. | | Your redirect URI | `?error=invalid_request` | A parameter is missing or wrong — most often the PKCE `code_challenge`. | | Your redirect URI | `?error=invalid_scope` | A scope that does not exist. Check the [scopes table](scopes.md). | | Your redirect URI | `?error=access_denied` | The person selected **Cancel**. | | Token | `400 {"error": "invalid_grant"}` | The code or refresh token is unknown, expired, already used or revoked, or the `code_verifier` or `redirect_uri` does not match. Send the person through the flow again. | | Token | `401 {"error": "invalid_client"}` | The client ID or secret is wrong. | | Register | `400 {"error": "invalid_redirect_uri"}` | A redirect URI outside the self-registration list. | | API | `401` [`unauthenticated`](errors.md#unauthenticated) | The access token expired or was revoked. Refresh it. | | API | `403` [`scope_missing`](errors.md#scope_missing) | The token lacks the scope named in `required`. | Every other API error is the same as for an API token: see [Errors](errors.md). ## Keeping it safe - **Always use PKCE and `state`.** A fresh code verifier and state for every sign-in, checked when the person comes back. - **Store refresh tokens like passwords**: encrypted, server-side, never in a browser's storage or a log. A refresh token is a standing key to the person's organization. - **Keep the client secret on your server.** An app that runs on people's devices cannot keep a secret; ask for a public client instead, which relies on PKCE alone. - **Ask for the fewest scopes.** People approve a short, plain list more readily, and a leaked token can do less. --- # Code you can use Ready-made code for the InfraNest API, open source on GitHub. Copy it, change it, run it — at your own risk, as each repository's licence says. ## Examples **[infranest-examples](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples)** — the [recipes](recipes/acme-dns-01.md) as scripts you can run, in shell, Python, Node.js and PowerShell: dynamic DNS, Let's Encrypt DNS-01 hooks, a maintenance window around a deploy, a monitor per deploy, watching a new server, the audit log to a SIEM, and a smoke test for your [sandbox](sandbox.md). Every request in them is checked against the API description, so they keep working as the API grows. ## GitHub Action **[InfraNest Maintenance Window](https://github.com/marketplace/actions/infranest-maintenance-window)** ([source](https://github.com/InfraNest-Infrastructure-Organized/infranest-action)) — silence alerts while a workflow deploys, and turn them back on afterwards. It also pauses and resumes a monitor and resets your sandbox. ```yaml - uses: InfraNest-Infrastructure-Organized/infranest-action@v1 id: window with: token: ${{ secrets.INFRANEST_TOKEN }} organization: 12 command: maintenance-open minutes: 30 ``` See [Maintenance from a pipeline](recipes/maintenance-window-from-a-pipeline.md) for the whole workflow. ## OpenAPI description **[infranest-openapi](https://github.com/InfraNest-Infrastructure-Organized/infranest-openapi)** — the API description as a versioned file, updated daily from [developers.infranest.io/openapi.json](/openapi.json), with a dated release whenever it changes. Point a client generator, Postman or an API gateway at it, or watch the repository to hear about new endpoints. ## Server agent **[infranest-agent](https://github.com/InfraNest-Infrastructure-Organized/infranest-agent)** — the agent that sends a server's metrics to InfraNest. It runs unprivileged, and its source is public so you can read exactly what it does on your machines. See [Install on many servers](agent/install-on-many-servers.md). ## AI assistants Ask Claude, Cursor or VS Code about your infrastructure through the hosted [MCP server](ai-assistants.md). ## Found a problem? Open an issue in the repository it is about. Report a security problem privately, as each repository's `SECURITY.md` explains — never in a public issue. --- # Pagination and filtering Lists come in two shapes. Small ones — tags, alert channels, certificates and the like — answer a plain JSON array with everything in it. Lists that can grow large — domains, DNS zones, servers, monitors, incidents, maintenance windows, contacts, watched domains, the audit log — are paginated. Each endpoint's [reference](reference.md) entry shows which shape it answers, with a recorded example. ## Pages Ask for a page with `page` (from 1) and its size with `per_page`: ```sh curl "https://api.infranest.io/api/organizations/42/domains?page=2&per_page=100" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` `per_page` is 25 by default and at most 200; a larger number is lowered to 200, not refused. A few lists have their own sizes, which their reference entry states: the audit log takes up to 100, a monitor's results up to 500. A paginated answer puts the rows in `data`, with where you are beside them: ```json { "current_page": 2, "data": [ { "id": 31, "domain": "example.com" } ], "first_page_url": "https://api.infranest.io/api/organizations/42/domains?per_page=100&page=1", "from": 101, "last_page": 3, "last_page_url": "https://api.infranest.io/api/organizations/42/domains?per_page=100&page=3", "next_page_url": "https://api.infranest.io/api/organizations/42/domains?per_page=100&page=3", "path": "https://api.infranest.io/api/organizations/42/domains", "per_page": 100, "prev_page_url": "https://api.infranest.io/api/organizations/42/domains?per_page=100&page=1", "to": 200, "total": 248, "links": [ { "url": null, "label": "« Previous", "page": null, "active": false } ], "meta": { "counts": { "total": 248, "expiring_soon": 3 } } } ``` - **Walk the pages** by following `next_page_url` until it is `null`. It and the other `*_page_url` fields and `links` keep your filters, `sort`, `dir` and `per_page`, so you can follow them as they are. Asking for `page=1`, `2`, … yourself, with the same filters each time, works too. - **`total`** counts every matching row, across all pages. - **`meta`**, where a list has it, carries extra information about the list, such as `meta.counts`: the numbers behind the app's filter tabs. There is no cursor. A row added while you walk the pages can shift the next page by one; for an exact copy, use the [data export](/reference/data) instead. ## Filtering and searching Filters are plain query parameters, named after what they filter, and each list documents its own. Most lists take: | Parameter | What it does | |---|---| | `search` | Matches the obvious text fields, such as a name, a domain or an address. | | `tag` | Only rows with this tag ID. | | `status` | Only rows in this state; the values are listed on the endpoint. | For example, domains that expire soon and will not renew by themselves: ```sh curl "https://api.infranest.io/api/organizations/42/domains?expiring_soon=1&autorenew_off=1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` An unknown parameter is ignored rather than refused, so check the spelling against the reference when a filter seems to do nothing. ## Sorting Paginated lists sort with `sort` (a column the endpoint names) and `dir` (`asc` or `desc`). An unknown `sort` value is ignored, and the list keeps its default order. ```sh curl "https://api.infranest.io/api/organizations/42/domains?sort=expires&dir=asc" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` ## Times and dates Times are ISO 8601 in UTC (`2026-01-15T09:30:00.000000Z`). Filters that take a date range, such as the audit log's `from` and `to`, say on their entry whether they take a date or a time. --- # Quickstart Five minutes, on your sandbox, with a known answer at every step: find the sandbox, read its sample DNS zones, try a change as a dry run, make it, and undo it. Everything here happens in the [sandbox](sandbox.md), with a test token, so nothing reaches a DNS provider or the internet. If a step answers something other than what is shown, the problem is on our side or in the step before — and the error's `code` says which. These exact commands run against a real sandbox in the [examples repository's CI](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/quickstart), so they stay correct. ## 1. Create your sandbox and a test token 1. In the app, open **My account → Developer** and select **Create a sandbox**. InfraNest makes it, fills it with sample data and switches you to it. 2. Still in the sandbox, choose **New token**, name it `quickstart` and tick **DNS → Read** and **DNS → Write**. 3. Copy the token (it starts `…|in_test_`) and put it in your shell: ::: code-group ```sh [cURL] export INFRANEST_TOKEN='paste the token here' ``` ```powershell [PowerShell] $env:INFRANEST_TOKEN = 'paste the token here' ``` ```python [Python] # In the shell that runs Python: export INFRANEST_TOKEN='paste the token here' import os, requests API = "https://api.infranest.io/api" HEADERS = {"Authorization": f"Bearer {os.environ['INFRANEST_TOKEN']}", "Accept": "application/json"} ``` ```js [JavaScript] // In the shell that runs Node 18+: export INFRANEST_TOKEN='paste the token here' const API = 'https://api.infranest.io/api' const HEADERS = { Authorization: `Bearer ${process.env.INFRANEST_TOKEN}`, Accept: 'application/json' } ``` ::: ## 2. Find your sandbox Ask who the token belongs to. A test token is bound to its sandbox, so you get exactly one organization, marked `is_sandbox`. ::: code-group ```sh [cURL] curl -s https://api.infranest.io/api/auth/me \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ | jq '.user.organizations[] | {id, name, is_sandbox}' ``` ```powershell [PowerShell] $headers = @{ Authorization = "Bearer $env:INFRANEST_TOKEN"; Accept = 'application/json' } $me = Invoke-RestMethod -Uri 'https://api.infranest.io/api/auth/me' -Headers $headers $me.user.organizations | Select-Object id, name, is_sandbox ``` ```python [Python] me = requests.get(f"{API}/auth/me", headers=HEADERS).json() print([(o["id"], o["name"], o["is_sandbox"]) for o in me["user"]["organizations"]]) ``` ```js [JavaScript] const me = await (await fetch(`${API}/auth/me`, { headers: HEADERS })).json() console.log(me.user.organizations.map(({ id, name, is_sandbox }) => ({ id, name, is_sandbox }))) ``` ::: You see your sandbox's ID (yours will differ from `2`): ```json { "id": 2, "name": "Sandbox", "is_sandbox": true } ``` Keep the ID: `export ORG=2` (PowerShell: `$env:ORG = 2`). ## 3. Read the sample zones ::: code-group ```sh [cURL] curl -s "https://api.infranest.io/api/organizations/$ORG/dns-zones" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ | jq '.data[] | {id, name, records_count}' ``` ```powershell [PowerShell] $zones = Invoke-RestMethod -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones" -Headers $headers $zones.data | Select-Object id, name, records_count ``` ```python [Python] org = 2 # your sandbox's ID zones = requests.get(f"{API}/organizations/{org}/dns-zones", headers=HEADERS).json()["data"] print([(z["id"], z["name"], z["records_count"]) for z in zones]) ``` ```js [JavaScript] const org = 2 // your sandbox's ID const zones = (await (await fetch(`${API}/organizations/${org}/dns-zones`, { headers: HEADERS })).json()).data console.log(zones.map(({ id, name, records_count }) => ({ id, name, records_count }))) ``` ::: Two zones, named after your sandbox's ID: ```json { "id": 1, "name": "sandbox-2.example", "records_count": 7 } { "id": 2, "name": "shop-2.example", "records_count": 2 } ``` Keep the ID of `sandbox-….example`: `export ZONE=1` (PowerShell: `$env:ZONE = 1`). ## 4. Try a change as a dry run Add an `A` record named `quickstart`, with `?dry_run=1`: the API answers with the change it would make, and makes none. ::: code-group ```sh [cURL] curl -s -X POST "https://api.infranest.io/api/organizations/$ORG/dns-zones/$ZONE/dns-records?dry_run=1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600}' ``` ```powershell [PowerShell] $record = @{ type = 'A'; name = 'quickstart'; value = '192.0.2.50'; ttl = 3600 } | ConvertTo-Json Invoke-RestMethod -Method Post -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones/$env:ZONE/dns-records?dry_run=1" ` -Headers $headers -ContentType 'application/json' -Body $record ``` ```python [Python] zone = 1 # the ID of sandbox-….example record = {"type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600} dry = requests.post(f"{API}/organizations/{org}/dns-zones/{zone}/dns-records", params={"dry_run": 1}, headers=HEADERS, json=record) print(dry.json()) ``` ```js [JavaScript] const zone = 1 // the ID of sandbox-….example const record = { type: 'A', name: 'quickstart', value: '192.0.2.50', ttl: 3600 } const post = (path, body, extra = {}) => fetch(`${API}${path}`, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/json', ...extra }, body: JSON.stringify(body), }) console.log(await (await post(`/organizations/${org}/dns-zones/${zone}/dns-records?dry_run=1`, record)).json()) ``` ::: ```json { "dry_run": true, "zone": "sandbox-2.example", "provider_push": "none", "changes": [ { "action": "create", "before": null, "after": { "type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600 } } ], "unchanged": 0, "skipped": [] } ``` `provider_push: none` because a sandbox zone is kept in InfraNest only. In a live organization it says how the change would reach your DNS provider. ## 5. Make the change The same request without `?dry_run=1`. The `Idempotency-Key` makes a retry of this request safe: send it again with the same key, and you get the same answer instead of a second record. ::: code-group ```sh [cURL] curl -s -X POST "https://api.infranest.io/api/organizations/$ORG/dns-zones/$ZONE/dns-records" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600}' ``` ```powershell [PowerShell] Invoke-RestMethod -Method Post -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones/$env:ZONE/dns-records" ` -Headers ($headers + @{ 'Idempotency-Key' = [guid]::NewGuid().ToString() }) -ContentType 'application/json' -Body $record ``` ```python [Python] import uuid created = requests.post(f"{API}/organizations/{org}/dns-zones/{zone}/dns-records", headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())}, json=record) print(created.status_code, created.json()) ``` ```js [JavaScript] const created = await post(`/organizations/${org}/dns-zones/${zone}/dns-records`, record, { 'Idempotency-Key': crypto.randomUUID() }) console.log(created.status, await created.json()) ``` ::: The API answers `201 Created` with the new record (shortened here; IDs and times will differ): ```json { "id": 10, "dns_zone_id": 1, "type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600, "priority": null, "created_at": "2026-10-25T19:01:40.000000Z" } ``` ## 6. Undo it Every DNS change is recorded in the zone's history, in batches. Take the newest batch, and undo it. ::: code-group ```sh [cURL] BATCH=$(curl -s "https://api.infranest.io/api/organizations/$ORG/dns-zones/$ZONE/changes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" | jq -r '.batches[0].batch_id') curl -s -X POST "https://api.infranest.io/api/organizations/$ORG/dns-changes/undo" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ -H "Content-Type: application/json" -d "{\"batch_id\": \"$BATCH\"}" ``` ```powershell [PowerShell] $changes = Invoke-RestMethod -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones/$env:ZONE/changes" -Headers $headers $undo = @{ batch_id = $changes.batches[0].batch_id } | ConvertTo-Json Invoke-RestMethod -Method Post -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-changes/undo" ` -Headers $headers -ContentType 'application/json' -Body $undo ``` ```python [Python] batch = requests.get(f"{API}/organizations/{org}/dns-zones/{zone}/changes", headers=HEADERS).json()["batches"][0]["batch_id"] print(requests.post(f"{API}/organizations/{org}/dns-changes/undo", headers=HEADERS, json={"batch_id": batch}).json()) ``` ```js [JavaScript] const history = await (await fetch(`${API}/organizations/${org}/dns-zones/${zone}/changes`, { headers: HEADERS })).json() console.log(await (await post(`/organizations/${org}/dns-changes/undo`, { batch_id: history.batches[0].batch_id })).json()) ``` ::: ```json { "batch_id": "935a6fe3-c635-47ad-a2bd-3985a68e79a4", "results": [ { "change_id": 10, "record": { "type": "A", "name": "quickstart", "value": "192.0.2.50" }, "outcome": "undone" } ], "undone": 1, "skipped": 0, "failed": 0 } ``` The `quickstart` record is gone, and the undo is itself a batch in the history, so it can be undone too. To start the sandbox over completely, [reset it](sandbox.md#start-again). ## Where to go next - **[Limits](limits.md)** — the rate limits and other numbers a script should stay inside. - **[Recipes](recipes/acme-dns-01.md)** — complete scripts: certificates through DNS, dynamic DNS, maintenance from a pipeline, a monitor per deploy. - **[AI assistants](ai-assistants.md)** — let Claude, ChatGPT, Cursor or VS Code read your account, or have a coding agent write against the API. - **[Errors](errors.md)** and **[Troubleshooting](troubleshooting.md)** — every `code`, and what to do about it. - **[Tokens and scopes](tokens-and-scopes.md)** — before you make a live token. --- # Let's Encrypt with InfraNest DNS Get certificates for any domain whose DNS is in InfraNest, including wildcards, without opening a port. Your ACME client asks InfraNest to add one TXT record, waits for it to be checked, then removes it. **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/acme-dns-01`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/acme-dns-01). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **DNS → Write** only. It can change records and nothing else: it can't delete the zone, move it, or touch your servers. ## Add the challenge record Find the zone's ID with `GET /organizations/{organization}/dns-zones`, then: ```sh curl -X POST https://api.infranest.io/api/organizations/42/dns-zones/318/dns-records \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: acme-$CERTBOT_DOMAIN-$CERTBOT_VALIDATION" \ -d '{"type":"TXT","name":"_acme-challenge","value":"'"$CERTBOT_VALIDATION"'","ttl":60}' ``` Keep the `id` from the response. You need it to remove the record. The `Idempotency-Key` means a hook that retries after a timeout adds the record once, not twice. See [Retries and dry runs](../retries-and-dry-runs.md). ## Remove it afterwards ```sh curl -X DELETE https://api.infranest.io/api/organizations/42/dns-zones/318/dns-records/9021 \ -H "Authorization: Bearer $INFRANEST_TOKEN" ``` Every change appears in the zone's history in InfraNest and can be undone from there. ## Coming - **Ready-made providers** for lego and acme.sh, so you won't need a hook script at all. --- # Copy the audit log into your SIEM Send every change made in your organization, in the app or with a token, to your log platform once a day. The script reads yesterday's entries page by page and writes one JSON object per line, the format most log shippers read. **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/audit-log-to-siem`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/audit-log-to-siem). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **Audit log → Read** only. It can read the log and nothing else. ## The script ```sh #!/bin/sh # Writes yesterday's audit log as JSON lines to audit-YYYY-MM-DD.jsonl. set -eu DAY=$(date -u -d yesterday +%Y-%m-%d) # macOS: date -u -v-1d +%Y-%m-%d URL="https://api.infranest.io/api/organizations/42/audit-logs?from=$DAY&to=$DAY&per_page=100" : > "audit-$DAY.jsonl" while [ -n "$URL" ] && [ "$URL" != "null" ]; do page=$(curl -fsS "$URL" -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json") echo "$page" | jq -c '.data[]' >> "audit-$DAY.jsonl" URL=$(echo "$page" | jq -r .next_page_url) done ``` Run it once a day, shortly after midnight UTC, and point your log shipper (Filebeat, Vector, the Splunk forwarder, …) at the files. - **`from` and `to`** are dates (`YYYY-MM-DD`). Both are included. - **`per_page`** goes up to 100. The answer is a page with `data` and a `next_page_url`, which is `null` on the last page. - **Each entry** has the `action` (for example `dns_record.updated`), who did it (`user`), what it was about (`resource_type`, `resource_id`), the details in `meta`, and `created_at`. See the [audit log reference](/reference/audit-log). - **Filter** with `action` (a part of the action name, such as `dns_record`) or `resource_type`. --- # Update DNS from a script when your IP changes Keep a record such as `home.example.com` pointing at a connection whose address changes. A small script finds your current address and changes the record only when it differs. ::: tip Using a router? Most routers can update DNS themselves. InfraNest has a built-in **Dynamic IP** feature for that, with a link your router calls. See [Dynamic IP on your router](https://infranest.io/docs/dynamic-ip-on-your-router). Use this recipe when you want a script, for example on a Raspberry Pi or in a container. ::: **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/dynamic-dns`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/dynamic-dns). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **DNS → Write** only, in the organization that holds the zone. ## Find the zone and the record ```sh curl https://api.infranest.io/api/organizations/42/dns-zones \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` Note the zone's `id`, then list its A records: ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/318/dns-records?type=A" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` Note the `id` of the record you want to keep up to date. ## The script ```sh #!/bin/sh # Updates one A record to this machine's public IPv4 address, only when it changed. set -eu API=https://api.infranest.io/api/organizations/42/dns-zones/318/dns-records/9021 current=$(curl -fsS https://api.ipify.org) stored=$(curl -fsS "$API" -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" | jq -r .value) if [ "$current" != "$stored" ]; then curl -fsS -X PUT "$API" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"value\":\"$current\",\"ttl\":300}" > /dev/null echo "Updated to $current" fi ``` Run it every five minutes, for example with cron: `*/5 * * * * /usr/local/bin/update-dns.sh`. - **A short TTL** (300 seconds here) makes the new address take effect quickly. The minimum is 60. - **Only changed addresses are sent**, so the script stays well inside the [rate limits](../tokens-and-scopes.md#rate-limits) and the zone's history shows real changes only. - **To see what would change first**, add `?dry_run=1` to the `PUT`. See [Retries and dry runs](../retries-and-dry-runs.md). --- # Open a maintenance window from your deploy pipeline Silence alerts while a deploy runs, show the work on your status page, and close the window when the deploy is done. Your pipeline opens the window first and completes it at the end, even if the deploy fails. On GitHub Actions, the [InfraNest Maintenance Window](../open-source.md#github-action) action does all of this for you. The steps below work in any CI. **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/maintenance-window`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/maintenance-window). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **Monitoring → Write**. Store it as a secret in your CI, for example `INFRANEST_TOKEN`. ## Open the window ```sh WINDOW_ID=$(curl -fsS -X POST https://api.infranest.io/api/organizations/42/maintenance-windows \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: deploy-$CI_PIPELINE_ID" \ -d '{ "name": "Deploy of the web app", "starts_at": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'", "ends_at": "'"$(date -u -d '+30 minutes' +%Y-%m-%dT%H:%M:%SZ)"'", "suppress_alerts": true, "check_ids": [1201, 1202] }' | jq -r .id) ``` - **`check_ids`** are the monitors to silence. Find them with `GET /organizations/42/monitoring-checks`. Use `server_ids` for servers. - **`ends_at`** is a safety net: if the pipeline never completes the window, it still closes then. - **The `Idempotency-Key`** uses the pipeline's id, so a retried job step opens one window, not two. See [Retries and dry runs](../retries-and-dry-runs.md). On macOS, `date -d` is not available. Use `date -u -v+30M +%Y-%m-%dT%H:%M:%SZ` instead. ## Close it when the deploy is done ```sh curl -fsS -X PATCH "https://api.infranest.io/api/organizations/42/maintenance-windows/$WINDOW_ID/complete" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` Run this step whether the deploy succeeded or failed (in GitHub Actions, `if: always()`; in GitLab CI, an `after_script`). Alerts resume as soon as the window is complete. --- # Create a monitor when you deploy a new service When a new service goes live, add a monitor for it in the same pipeline, so nothing reaches production without being watched. Running the step again does not create a second monitor. **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/monitor-per-deploy`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/monitor-per-deploy). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **Monitoring → Write**. ## Create the monitor ```sh curl -fsS -X POST https://api.infranest.io/api/organizations/42/monitoring-checks \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: monitor-api.example.com" \ -d '{ "name": "API health", "type": "http", "target": "https://api.example.com/health", "interval_seconds": 60, "ssl_check_enabled": true, "ssl_expiry_days": 14 }' ``` - **`type`** is one of `http`, `port`, `ssl`, `dns`, `keyword`, `redirect`, `ping` or `api`. - **`interval_seconds`** is one of 30, 60, 120, 300, 600, 900, 1800 or 3600. Your plan sets the shortest you can use. - **The `Idempotency-Key`** is tied to the target, so a pipeline that runs twice within 24 hours creates the monitor once. For a check that should exist forever, first look for it: `GET /organizations/42/monitoring-checks`, and only create it when it is not there. The response is the new monitor, including its `id`. Keep it if you want to pause the monitor around later deploys. ## Pause it around a deploy ```sh curl -fsS -X POST https://api.infranest.io/api/organizations/42/monitoring-checks/1201/pause \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" # … deploy … curl -fsS -X POST https://api.infranest.io/api/organizations/42/monitoring-checks/1201/resume \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` For a deploy that also touches servers or several monitors, a [maintenance window](maintenance-window-from-a-pipeline.md) is usually the better fit. --- # Status, uptime and history Three questions a dashboard, a chat bot or a weekly report asks, each answered in one call: is everything up right now, what was a monitor's uptime last month, and what happened hour by hour. **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/status-and-uptime`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/status-and-uptime). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **Monitoring → Read**. Add **Servers → Read** if you want your servers in the status answer too; without it, `servers` is `null`. ## Is everything up right now? ```sh curl -fsS "https://api.infranest.io/api/organizations/42/status" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` The answer lists every monitor and server with its current state, the open incidents and the maintenance running now, and counts them in `summary`: ```json { "summary": { "monitors": { "total": 13, "up": 11, "down": 1, "degraded": 0, "paused": 1, "unknown": 0 }, "servers": { "total": 14, "running": 12, "agent_not_reporting": 1, "breaching": 0 }, "open_incidents": 1, "active_maintenance": 0 }, "monitors": [{ "id": 3, "name": "API health", "status": "down", "last_checked_at": "2026-01-15T11:58:00Z", "uptime_24h": 99.4 }], "servers": [{ "id": 77, "name": "web-03", "status": "running", "agent_reporting": true, "breaching_alerts": 0 }], "incidents": [{ "id": 51, "title": "API health is down", "monitoring_check_id": 3, "started_at": "2026-01-15T11:20:00Z" }], "maintenance": [] } ``` - **One customer or one team:** add `?tag=7` to see only the monitors and servers with that tag, their incidents, and maintenance that carries the tag or covers them. - **`agent_reporting`** is `true` while the server agent sends readings, `false` when it has gone quiet, and `null` when no agent is installed. - **A traffic light** is `summary.monitors.down + summary.monitors.degraded == 0 && summary.open_incidents == 0`. ## Uptime and response times for a period ```sh curl -fsS "https://api.infranest.io/api/organizations/42/monitoring-checks/3/aggregates?period=30d" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` ```json { "period": "30d", "from": "2025-12-16T12:00:00Z", "to": "2026-01-15T12:30:00Z", "uptime_percent": 99.95, "up_percent": 99.81, "checks": { "total": 8640, "up": 8624, "degraded": 4, "down": 12 }, "response_time_ms": { "p50": 182, "p95": 410 }, "incidents": { "count": 2, "down_minutes": 21 } } ``` - **`period`** is `24h`, `7d`, `30d` or `90d`. For a calendar month, send `from` and `to` instead: `?from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z` (up to 366 days). - **`uptime_percent`** is the figure the app and your status page show: the share of the time no confirmed outage covered. **`up_percent`** is the share of individual checks that came back up, which a single probe's blip lowers. - **Response times** over more than an hour are averaged from hourly figures, weighted by the number of checks, so they are close rather than exact. For every monitor at once, by calendar month, the [monthly uptime report](/reference/monitoring) is one call: `GET /organizations/42/uptime-report?month=2026-01`. ## Results hour by hour ```sh curl -fsS "https://api.infranest.io/api/organizations/42/monitoring-checks/3/results?resolution=hour&from=2026-01-14T00:00:00Z&to=2026-01-15T00:00:00Z" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` Each row sums one hour across all probes: `checks_total`, `checks_up`, `checks_degraded` (slow), `checks_down` (failed), `up_percent` and the p50 and p95 response times. Newest first, 50 rows a page; ask for up to 500 with `per_page` and follow `next_page_url`. | `resolution` | Each row is | Longest range per request | Kept for | |---|---|---|---| | `raw` (default) | one check by one probe | 3 days | 3 days | | `hour` | one hour, all probes | 31 days | your plan's history | | `day` | one day, all probes | 366 days | your plan's history | A longer range is refused with [`range_too_large`](../errors.md#range_too_large). The error says the longest range the resolution takes in `max_range_seconds`, and which resolutions would take your range in `resolutions` — ask for one of those, or split the range. ## Server metrics over a range The same `from`, `to` and `resolution` work on a server's stored metrics: ```sh curl -fsS "https://api.infranest.io/api/organizations/42/cloud-servers/77/metric-series?metrics=cpu_percent,memory_percent&from=2026-01-01T00:00:00Z&to=2026-01-15T00:00:00Z" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` Raw samples are kept for 48 hours; past that, or for a range over 48 hours, the answer is hourly averages, and `resolution` says which you got. Hourly averages go back up to 92 days per request. Each metric is a list of `[unix seconds, value]` pairs, oldest first; `pagination` says how many points the range holds, 10,000 to a page. --- # Start watching a new server After you create a server at your cloud provider, InfraNest finds it at its next sync. This recipe turns on monitoring for it and adds a check on its website, so a new machine is watched from its first hour. **Runnable code:** the code on this page, ready to clone and run, is in [`infranest-examples/examples/watch-new-server`](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/watch-new-server). CI runs it against the [sandbox](../sandbox.md). ## The token Make a token with **Servers → Write** and **Monitoring → Write**. ## Find the server ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers?search=web-03" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` The answer is a page of servers; take the `id` and `ipv4` of the one in `data`. InfraNest syncs with your provider on a schedule, so a server created a moment ago may not be listed yet. To fetch it at once, select **Sync now** on the provider under **Integrations** in the app. ## Turn on monitoring ```sh curl -fsS -X POST https://api.infranest.io/api/organizations/42/cloud-servers/77/monitoring \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" ``` The answer says how many monitored servers your plan allows and how many you use: ```json { "monitoring_enabled": true, "monitoring_enabled_at": "2026-01-15T12:00:00+00:00", "usage": 4, "limit": 10 } ``` At the limit, the API answers `402` with the code [`plan_limit`](../errors.md#plan_limit). Turning it on again when it is already on changes nothing. For CPU, memory and disk metrics, install the server agent as well. See [Install the agent on many servers](../agent/install-on-many-servers.md). ## Watch its website ```sh curl -fsS -X POST https://api.infranest.io/api/organizations/42/monitoring-checks \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: server-77-http" \ -d '{"name": "web-03 website", "type": "http", "target": "https://web-03.example.com", "interval_seconds": 60}' ``` --- # API reference Every endpoint a token can use, generated from the API itself and grouped like the app's sidebar. Each one shows the scope it needs, whether a token may call it when it changes something, its parameters, the call in cURL, PowerShell, PHP, Python, JavaScript and Go, the recorded answer, and an example of each error it can answer. ## Downloads - [openapi.json](https://developers.infranest.io/openapi.json) — the same description as OpenAPI 3, for generating clients or loading into an API gateway. Also versioned on GitHub in [infranest-openapi](https://github.com/InfraNest-Infrastructure-Organized/infranest-openapi). - [Postman collection](https://developers.infranest.io/infranest.postman_collection.json) and [environment](https://developers.infranest.io/infranest.postman_environment.json) — every endpoint, one folder per page below. - [Bruno collection](https://developers.infranest.io/infranest.bruno.zip) — the same, as a folder of `.bru` files with its environment. How to import each, and Insomnia: [Use with Postman, Bruno or Insomnia](api-clients.md). ## Pages | Section | Page | What is in it | |---|---|---| | Domains | [Domains](/reference/domains) | Domains and contacts as InfraNest knows them. Registrar actions (register, renew, transfer) stay in the app. | | Domains | [DNS](/reference/dns) | Zones and records at your DNS providers, templates and dynamic IP sources. Record changes are recorded in the zone history and can be undone. | | Domains | [Certificates](/reference/certificates) | TLS certificates InfraNest tracks or issues, and when they expire. | | Infrastructure | [Servers and cloud](/reference/cloud) | Servers, volumes, networks, firewalls and load balancers. Provider actions stay in the app; tokens edit InfraNest's own records and alerting. | | Monitoring | [Monitoring](/reference/monitoring) | Monitors, their results and uptime, and deploy markers. | | Monitoring | [Incidents](/reference/incidents) | Incidents: open, update and resolve them. | | Monitoring | [Maintenance](/reference/maintenance) | Planned maintenance windows, which pause alerts and show on status pages. | | Monitoring | [Status pages](/reference/status-pages) | Public status pages and what they show. | | Monitoring | [Alerts and notifications](/reference/alerts) | Alert channels, alert rules and notification settings. Read only: changing who gets alerted stays in the app. | | Automations | [Automations](/reference/automations) | Automations and the connections they use. Read only. | | Automations | [Integrations](/reference/integrations) | Connected providers and messaging apps. Read only: credentials are managed in the app. | | Drop catch | [Drop catch](/reference/drop-catch) | Watched domains. | | Organization | [Organization and members](/reference/organization) | The organizations a token can reach, their members, roles, single sign-on and dashboards. Read only. | | Organization | [Tags and custom fields](/reference/tags) | Tags and custom fields, which work across every module. | | Organization | [Costs](/reference/costs) | Cost items, cost changes and provider price changes. | | Organization | [Billing](/reference/billing) | Subscription, invoices and usage. Read only. | | Organization | [Audit log](/reference/audit-log) | Every change in the organization, including those made with tokens. Read only. | | Organization | [Data export](/reference/data) | Export your data as CSV or JSON. | | Account | [Account](/reference/account) | Who the token belongs to, and revoking it. | Errors use one shared format, described in [Errors](errors.md). --- # Responsibility and costs An API token acts as you. What it does is done in your name, in your organization, at the providers you have connected to InfraNest. This page says what that means in practice. ## You are responsible for what your tokens do - A change made with a token is your change. It appears in the organization's audit log next to changes made in the app. - Keep tokens secret and give each one only the scopes it needs (see [Tokens and scopes](tokens-and-scopes.md)). Anyone who holds a token can do what it allows until you revoke it. - Test scripts and automations before you point them at production infrastructure. ## Actions can cause costs InfraNest refuses some actions to tokens altogether, such as ordering domains or servers. Everything a token *can* do still reaches your providers, and that can cost money: - **Your providers decide their own prices.** Some charge for API calls, DNS queries or changes, or for resources that a change keeps running. InfraNest does not control or refund those costs. - **Your plan's limits apply.** Monitors, checks and other resources a token creates count against your plan, exactly as when you create them in the app. - **Mistakes cost the same as intentional changes.** A script that loops, or a token that leaks, can make many changes quickly. Rate limits slow that down, but do not prevent it. ## Before you automate - Check the pricing of the providers your automation will touch. - Start with a read-only token and a dry run where one is available. - Watch the audit log after your first runs. The terms that apply to using InfraNest, including through the API, are the [Terms of Service](https://infranest.io/terms). --- # Retries and dry runs Two tools make changes through the API safe to automate. Use them in scripts, and always in AI agents. - **`Idempotency-Key`** lets you retry a `POST` without creating the same thing twice. - **`?dry_run=1`** shows what a DNS change would do, and changes nothing. ## Retry safely with `Idempotency-Key` A request can time out after it has already been carried out. If your code then retries it, a second DNS record or monitor can be created. Send an `Idempotency-Key` header with every `POST` to prevent that: ```sh curl -X POST https://api.infranest.io/api/organizations/42/tags \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Idempotency-Key: 6f1c2a7e-3b0d-4f6e-9a51-0c2d8e7b4a19" \ -H "Content-Type: application/json" \ -d '{"name": "web", "color": "#336699"}' ``` - **Make a new key for every new request.** A UUID works well. Keys can be up to 255 visible ASCII characters. - **Send the same key only when you retry that exact request.** You get the first answer back, with the header `Idempotent-Replayed: true`, and nothing is done again. - **Same key, different request:** the API answers `422` [`idempotency_key_reused`](errors.md#idempotency_key_reused). - **Same key while the first request is still running:** the API answers `409` [`idempotency_in_progress`](errors.md#idempotency_in_progress). Wait a few seconds and send it again. - **Only successful answers are kept, for 24 hours.** If the first attempt was refused or failed, nothing was changed, so a retry with the same key is handled as new. - **Keys belong to one token.** Another token using the same key never sees your answers. - **A replay is only returned while your access is unchanged.** If your role, the token's scopes, the plan or the organization's status changed since the first request, the request runs again and gets today's answer. `PUT`, `PATCH` and `DELETE` don't need a key: doing them twice has the same result as doing them once. ## Look before you change: `?dry_run=1` Add `?dry_run=1` to a DNS write to see the change it would make. Nothing is saved, nothing is sent to the DNS provider, and nothing appears in the audit log. It works on: | Request | Path | |---|---| | Create a record | `POST /organizations/{organization}/dns-zones/{zone}/dns-records` | | Change a record | `PUT /organizations/{organization}/dns-zones/{zone}/dns-records/{record}` | | Delete a record | `DELETE /organizations/{organization}/dns-zones/{zone}/dns-records/{record}` | | Apply a template | `POST /organizations/{organization}/dns-templates/{template}/apply/{zone}` | ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/dns-zones/7/dns-records/311?dry_run=1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "192.0.2.20"}' ``` ```json { "dry_run": true, "zone": "example.com", "provider_push": "per_record", "changes": [ { "action": "update", "before": { "type": "A", "name": "www", "value": "192.0.2.10", "ttl": 300 }, "after": { "type": "A", "name": "www", "value": "192.0.2.20", "ttl": 300 } } ], "unchanged": 0, "skipped": [] } ``` - **`changes`** lists each record that would be created, updated or deleted. `before` is `null` for a create, and `after` is `null` for a delete. An empty list means there is nothing to do. - **`unchanged`** counts records that are already as requested. - **`skipped`** says why a template record would be left out, for example a value that does not fit its type. - **`provider_push`** says how the change would reach the DNS provider: - `per_record`: just these records; - `whole_zone`: the provider takes the whole zone at once; - `none`: the zone is only kept in InfraNest. - **Refusals are the same as for the real request.** A dry run that would be refused answers with that refusal, such as `409` for a record that already exists, or a missing scope. - **Sending the same request without `?dry_run=1` makes exactly that change**, as long as the zone has not changed in between. A dry run counts towards your [rate limit](tokens-and-scopes.md#rate-limits) as a change. ## For AI agents Dry-run every DNS change, show the result to the person you work for, and send the real request only after they agree. Send an `Idempotency-Key` with every `POST`, and keep the key when you retry. --- # Sandbox Try the API's writes without touching anything real. Your sandbox is a free organization with sample DNS zones, records, monitors, a server, domains, certificates and an incident. Tokens you make there are **test tokens**, and nothing in it ever reaches a DNS provider, a cloud provider or the internet. ## Create your sandbox In the app, open **My account → Developer** and select **Create a sandbox**. InfraNest makes the organization, fills it with sample data and switches you to it. You have one sandbox; it is marked **Sandbox** in the organization switcher. A sandbox belongs with an organization of your own: you need to be in at least one live organization to make one ([`sandbox_needs_organization`](errors.md#sandbox_needs_organization)). It is **yours alone** — nobody can be invited into it, and it cannot be subscribed or handed to someone else ([`sandbox_personal`](errors.md#sandbox_personal)). Its ID is in the address bar and under **Settings → Organization**, like any organization's. Your scripts use it in the same paths, for example `/organizations/{sandbox id}/dns-zones`. ## Make a test token While you work in the sandbox, create a token under **My account → Developer** as usual. A token made there is a test token: - **It starts with `in_test_`** after the ID (`123|in_test_…`), so you can tell it from a live token anywhere you paste it. - **It works only in your sandbox.** A live organization answers it with [`test_token_sandbox_only`](errors.md#test_token_sandbox_only). - **A live token never works in the sandbox**, not even one made for all your organizations. It gets [`test_token_required`](errors.md#test_token_required). ## Try an AI assistant there first Connect [Claude or ChatGPT](ai-assistants.md) and pick your sandbox on InfraNest's consent page, or give Claude Code, Cursor or VS Code a test token. The assistant reads the sample data, so you can see what it answers before it reads anything real. ## What happens there - **Writes work**, even before API writes are switched on for your own organizations. The same routes are writable as in a live organization, so what works in the sandbox works live, too. - **Nothing leaves.** DNS changes are saved in InfraNest only, monitors are never actually checked, and webhooks and automation calls are not sent. **No alerts go out either** — no email, chat, SMS or push, and no status page notices. An alert that would have gone out is recorded as *not sent* in the destination's delivery log, so you can still see what would have happened. Lookups on the internet (certificate scans, DNS propagation) answer [`organization_offline`](errors.md#organization_offline). - **Limits are small**, and the sandbox is free and does not count towards your organizations. - **Changes are audited**, like everywhere else. ## Start again Select **Reset sandbox** on the same page, or call: ```sh curl -X POST https://api.infranest.io/api/organizations/42/sandbox/reset \ -H "Authorization: Bearer $INFRANEST_TEST_TOKEN" \ -H "Accept: application/json" ``` Everything you made in the sandbox is removed — domains, contacts, DNS zones and records, servers, monitors, maintenance windows, status pages, alert destinations, certificates, endpoints, dynamic IPs, watched domains, automations, webhooks, deploy links and tokens, templates, dashboards, report schedules, cost items, custom fields and tags — and the sample data comes back. What stays: - **Your test tokens**, so a test suite keeps using the token it reset with. - **The audit log**, including the reset itself. - **The sandbox's settings** — its members and roles, notification preferences and any integrations you connected. Nothing in a sandbox is ever sent through them. - **History** — activity, cost and domain events — which ages out like any organization's. A test suite can reset the sandbox before each run — up to 30 times an hour ([`rate_limited`](errors.md#rate_limited) after that). One reset runs at a time: a second one sent while the first is still running waits for it, and answers [`conflict`](errors.md#conflict) if it waited too long. Any organization that is not a sandbox answers this with `404`. ## When a sandbox is removed - **Unused for 90 days** — no API call with its test tokens and no visit in the app. We email you 14 days before, and using it once keeps it. - **When you leave your last live organization**, your sandbox goes too. Making a new one is one click. ## The sample data | What | Sample | |---|---| | DNS zones | `sandbox-{id}.example` (A, AAAA, CNAME, MX, SPF and DMARC records) and `shop-{id}.example` | | Server | `web-01`, at `192.0.2.10` | | Monitors | A website, a certificate and a ping monitor, all up | | Domains | `sandbox-{id}.example`, `shop-{id}.example` (expires in three weeks) and `old-brand-{id}.example` (expires in nine days, auto-renew off) | | Certificates | One for `sandbox-{id}.example`, and one for `shop-{id}.example` that expires soon | | Incident | A resolved outage of the website three days ago, with its timeline | | Maintenance | An upgrade of `web-01` scheduled for next week | | Tags | `production`, `staging` | Names use the reserved `.example` domain and documentation addresses, so nothing in the sandbox points at a real machine. --- # Scopes A scope says what a token may do. API tokens and [OAuth apps](oauth.md) use the same scopes: you tick them when you create a token, and an app asks for them when it sends someone to InfraNest to connect. Each area has a **read** scope and, where the API takes changes, a **write** scope. Write includes read. Whatever the scopes, a token never does what only a person signed in to the app may do. See [What a token can never do](tokens-and-scopes.md#what-a-token-can-never-do). | Scope | Area | What it allows | |---|---|---| | `domains.read` | Domains & Contacts | Read domains, contacts and registrar details. | | `domains.write` | Domains & Contacts | Track a domain, edit its notes and labels, archive it, and manage the contact book. Includes read. | | `dns.read` | DNS | Read DNS zones and records. | | `dns.write` | DNS | Create, change and delete records, undo a change, apply a template and sync a zone. Includes read. | | `cloud.read` | Cloud Infrastructure | Read servers, volumes, networks, firewalls and load balancers. | | `cloud.write` | Cloud Infrastructure | Change InfraNest's own server records: labels, pricing, snooze and alert rules. Includes read. | | `monitoring.read` | Monitoring | Read monitors and their results, incidents, maintenance windows, status pages and certificates. | | `monitoring.write` | Monitoring | Change monitors, incidents, maintenance windows, status page content, tags, custom fields and cost items. Includes read. | | `drop-catch.read` | Drop Catch | Read watched domains. | | `drop-catch.write` | Drop Catch | Nothing beyond read: watched domains are changed in the app. | | `members.read` | Members & Teams | Read members and roles. | | `members.write` | Members & Teams | Nothing beyond read: members and roles are changed in the app. | | `data.read` | Data export & import | Export your data. | | `data.write` | Data export & import | Nothing beyond export: importing is done in the app. | | `billing.read` | Billing | Read the subscription and invoices. There is no write scope: billing changes are made in the app. | | `audit-log.read` | Audit Log | Read the organization's audit log. There is no write scope. | | `*` | Everything | Every scope above, read and write. Still never what only a person signed in to the app may do. | | `mcp:use` | AI assistants | Use the InfraNest [MCP server](ai-assistants.md). Grants nothing by itself: the other scopes decide what the assistant can read. OAuth only. | An app that asks for `mcp:use` and nothing else is asked for every read scope, because the MCP server only reads. The person connecting it still sees each scope, and gets only what their role allows. Every endpoint in the [API reference](reference.md) shows the scope it needs. A request without it answers [`scope_missing`](errors.md#scope_missing), naming the scope in `required`. --- # Tokens and scopes An API token acts as you, within the scopes you give it. This page says how scopes work, what no token can ever do, and how to keep tokens safe. ## Token or OAuth? An API token is for your own scripts and tools, acting as you. An app that other people connect to their InfraNest account uses [OAuth](oauth.md) instead: same scopes, but nobody copies a token. ## One organization, or all of them A token works in **the organization it was created in**. A token made while you work in Acme cannot touch Globex, even if you belong to both: it gets `wrong_organization` there, and Globex is left out of every list it reads. When a tool really needs every organization you belong to, tick **Works in all my organizations** when you create the token. Tokens made before this choice existed work in all your organizations; the token list shows which is which. ## Scopes Each area has a **read** scope and, where the API allows changes, a **write** scope. Write includes read. Every scope, and what it allows, is in the [scopes table](scopes.md), generated from the list the token dialog uses. Scopes follow the API, not the app's menu, so one scope can reach several reference pages. `monitoring` is the broad one: besides monitors it covers incidents, maintenance, status pages, certificates, alerts, automations, tags and costs. Every endpoint in the reference shows the scope it needs. | Scope | Reference pages it reaches | |---|---| | `audit-log` | [Audit log](/reference/audit-log) | | `billing` | [Billing](/reference/billing) | | `cloud` | [Servers and cloud](/reference/cloud), [Integrations](/reference/integrations) | | `data` | [Data export](/reference/data) | | `dns` | [DNS](/reference/dns) | | `domains` | [Domains](/reference/domains), [Certificates](/reference/certificates) | | `drop-catch` | [Drop catch](/reference/drop-catch) | | `members` | [Organization and members](/reference/organization) | | `monitoring` | [DNS](/reference/dns), [Certificates](/reference/certificates), [Servers and cloud](/reference/cloud), [Monitoring](/reference/monitoring), [Incidents](/reference/incidents), [Maintenance](/reference/maintenance), [Status pages](/reference/status-pages), [Alerts and notifications](/reference/alerts), [Automations](/reference/automations), [Integrations](/reference/integrations), [Tags and custom fields](/reference/tags), [Costs](/reference/costs) | API writes are switched on per organization. Until they are for yours, write requests answer `write_disabled`. Some reads hand back a secret, and need the **write** scope even though they only read: a certificate's private key needs `monitoring.write`, a domain's transfer code needs `domains.write`. ## What a token can never do Whatever its scopes, these stay with a person signed in to the app. A token gets `not_token_writable` or `session_only` if it tries: - **Ordering and buying:** registering, transferring or renewing domains, new servers, volumes or load balancers, backorders, billing. - **Anything that destroys or overwrites:** deleting servers, zones or volumes, rebuilding, restoring backups, changing nameservers. - **Anything that changes who gets in or where data goes:** members, roles, single sign-on, provider credentials, webhooks, alert channels, automations. - **Your account:** password, email, two-factor, passkeys, sessions and tokens themselves. - **Settings that belong to people, and anything that is itself a key:** single sign-on, notification settings, dashboards, saved payment details, plan changes and promo codes, the set-up screens of integrations and automations, and the addresses of incoming webhooks and the PagerDuty webhook, which would let whoever has them trigger automations or file events. These limits are a safeguard, not a promise that a token causes no costs. What a token does still reaches your providers, and some providers charge for API use or for the resources you change. See [Responsibility and costs](responsibility-and-costs.md). ## Expiry A token expires after the lifetime you pick when you create it: 30, 90, 180 or 365 days. The default is 90. A week before it expires, you get an email. To keep it working, select **Renew for 90 days** in the token's menu under **My account → Developer**. The token itself does not change, so nothing that uses it needs updating. An expired token answers `unauthenticated`. Tokens made before expiry existed never expire. They show **Never** in the list. Once such a token is 180 days old, you get one email suggesting you renew it, which gives it an expiry date. ## Limiting a token to addresses A token can be limited to the addresses it is used from: one IP address or CIDR range per line, such as `203.0.113.10` or `203.0.113.0/24`, up to 50 entries. From anywhere else it answers `403` with the code [`token_ip_not_allowed`](errors.md#token_ip_not_allowed), so a copy of the token taken from a build log or a laptop is useless. Set the list when you create the token, or change it later with **Limit addresses** in the token's menu under **My account → Developer**. An empty list means any address, which is what every token has unless you set one. Changes to the list are in the audit log. The address is the one your request reaches InfraNest from: for a CI runner, its egress address; behind a NAT or proxy, the address of the NAT or proxy. OAuth apps are not limited this way; their access is decided by the grant. ## Rate limits Each token has its own budgets: | Budget | Default | What counts | |---|---|---| | Requests | 300 per minute | Every request | | Changes | 60 per minute | Every `POST`, `PUT`, `PATCH` and `DELETE` (these also count as requests) | | Changes per hour | 1,000 per hour | Every `POST`, `PUT`, `PATCH` and `DELETE`, over a rolling hour | The hourly budget is for long runs: a script that paces itself just under the per-minute line still stops after 1,000 changes in an hour. If a job really makes more than that, split it across hours, or ask us about a plan with a higher limit. Your plan can set different numbers; [Limits](limits.md) has every limit in one place. Every response says where you stand, for whichever budget is closer to running out: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 57 X-RateLimit-Reset: 1793030460 ``` Over a budget, the API answers `429` with the code [`rate_limited`](errors.md#rate_limited) and a `Retry-After` header in seconds. Wait that long, then retry. The body's `limit_key` says which budget ran out (`api_requests_per_minute`, `api_writes_per_minute` or `api_writes_per_hour`) and `limit` its number. After the hourly budget, the wait can be up to an hour. Running out of changes does not stop reads. Two tokens never share a budget. ## Keeping tokens safe - **One token per purpose**, named for where it is used, so revoking one breaks one thing. - **The fewest scopes that work.** A certificate script needs `dns.write`, nothing else. - **One organization per token** unless a tool truly needs all of them. - **Limit it to fixed addresses** when it runs somewhere that has them, such as a CI runner or a server. - **Store tokens as secrets**: your CI's secret store, a vault, an environment variable. Never in a repository. - **Revoke a token you no longer use** under **My account → Developer**. The list shows when each was last used. - **If a token leaks**, revoke it there at once. It stops working immediately. ## Seeing what a token does - **Every change names its token.** In the organization's **audit log**, a change made through a token shows **via token** and the token's name, and the name stays after the token is revoked. Filter the log to one token from the token's menu (**View activity**), or with `token_id` on [`GET /audit-logs`](/reference/audit-log). - **A new token is announced.** Creating one sends a security email to the account it belongs to, as adding a passkey does. If you did not create it, revoke it and change your password. - **A sudden flood of changes is flagged.** When a token makes far more changes in a day than it usually does — ten times its average over the past week, and at least 200 — the organization's owners get a notification, and the token appears under **Needs you** on Home for the rest of the day. A token less than three days old is flagged at 1,000 changes. --- # Troubleshooting Start with the error's `code`. Every error from the API has one, and its `docs_url` links to the entry on [Errors](errors.md) that says what it means, why it usually happens and what to do. The problems below are the ones that are not a single code: a token that stopped working, a script that slows down, an assistant that will not connect. ## Every error code | Code | Status | What it means | |---|---|---| | [`unauthenticated`](/errors#unauthenticated) | 401 | You are not signed in, or the API token is missing, wrong, expired or revoked. | | [`scope_missing`](/errors#scope_missing) | 403 | This API token does not have the scope this request needs. | | [`session_only`](/errors#session_only) | 403 | This can only be done in the InfraNest app while signed in, not with an API token. | | [`not_token_writable`](/errors#not_token_writable) | 403 | This action is only available in the InfraNest app, not with an API token. | | [`write_disabled`](/errors#write_disabled) | 403 | API write access is not switched on for this organization. | | [`wrong_organization`](/errors#wrong_organization) | 403 | This API token is bound to another organization. | | [`token_ip_not_allowed`](/errors#token_ip_not_allowed) | 403 | This API token cannot be used from this address. | | [`test_token_required`](/errors#test_token_required) | 403 | This is a sandbox organization, and only its own test tokens work here. | | [`test_token_sandbox_only`](/errors#test_token_sandbox_only) | 403 | This is a test token, and it works only in its sandbox organization. | | [`api_token_required`](/errors#api_token_required) | 403 | The MCP server takes an API token, not a signed-in session. | | [`sandbox_personal`](/errors#sandbox_personal) | 409 | A sandbox is personal: nobody can be invited into it, and it cannot be subscribed or handed to someone else. | | [`sandbox_needs_organization`](/errors#sandbox_needs_organization) | 409 | A sandbox belongs with an organization of your own. | | [`organization_offline`](/errors#organization_offline) | 409 | This organization does not reach the internet, so nothing was looked up or sent. | | [`permission_denied`](/errors#permission_denied) | 403 | Your role in this organization does not allow this. | | [`forbidden`](/errors#forbidden) | 403 | You are not allowed to do this. | | [`organization_suspended`](/errors#organization_suspended) | 403 | This organization is suspended, so nothing in it can be changed. | | [`read_only_session`](/errors#read_only_session) | 403 | This session is read-only. | | [`sso_required`](/errors#sso_required) | 403 | This organization requires you to sign in with its single sign-on. | | [`two_factor_setup_required`](/errors#two_factor_setup_required) | 403 | This organization requires two-factor authentication on your account. | | [`password_reset_required`](/errors#password_reset_required) | 403 | Your password has expired and must be changed. | | [`email_verification_required`](/errors#email_verification_required) | 403 | Your email address has not been confirmed yet. | | [`plan_limit`](/errors#plan_limit) | 402 | Your plan does not include this, or its limit has been reached. | | [`not_found`](/errors#not_found) | 404 | Nothing exists at this address, or you cannot see it. | | [`method_not_allowed`](/errors#method_not_allowed) | 405 | This address does not accept that HTTP method. | | [`conflict`](/errors#conflict) | 409 | This conflicts with something that already exists. | | [`validation_failed`](/errors#validation_failed) | 422 | Some fields are missing or not valid. | | [`unprocessable`](/errors#unprocessable) | 422 | The request could not be carried out as sent. | | [`range_too_large`](/errors#range_too_large) | 422 | The time range is longer than this resolution allows. | | [`rate_limited`](/errors#rate_limited) | 429 | Too many requests. Slow down and try again shortly. | | [`idempotency_key_reused`](/errors#idempotency_key_reused) | 422 | This Idempotency-Key was already used for a different request. | | [`idempotency_in_progress`](/errors#idempotency_in_progress) | 409 | A request with this Idempotency-Key is still being handled. | | [`provider_refused`](/errors#provider_refused) | 4xx / 502 | The provider refused the request. | | [`provider_rate_limited`](/errors#provider_rate_limited) | 429 | The provider is limiting requests right now. | | [`provider_unavailable`](/errors#provider_unavailable) | 502 | The provider could not be reached or is having problems. | | [`maintenance`](/errors#maintenance) | 503 | InfraNest is down for maintenance. | | [`server_error`](/errors#server_error) | 500 | Something went wrong on our side. | Quote the error's `reference` (also the `X-Request-Id` header) when you contact support. It finds your request in our logs. ## Token problems ### It worked yesterday and answers `unauthenticated` today The token has probably expired. New tokens expire after 90 days unless you chose another period, and you get an email a week before. Open **My account → Developer**, find the token and select **Renew**: it keeps its value and scopes, so nothing needs updating. A revoked token cannot be renewed; create a new one. See [Expiry](tokens-and-scopes.md#expiry). Also check the header itself: `Authorization: Bearer `, with the whole token including the part before the `|`. A shell variable that is empty in CI sends `Bearer ` and nothing else. ### `wrong_organization` The token was made for one organization and the address names another. A token is bound to the organization it was created in unless you chose **all my organizations**. Check the number after `/organizations/` in your script, or create a token in the organization you mean. [`wrong_organization`](errors.md#wrong_organization) ### `token_ip_not_allowed` The token has an address list, and the request came from somewhere else. Common causes: - **A CI runner.** Hosted runners change address on every run; an address list does not suit them. - **IPv6.** Your machine may reach the API over IPv6 while you listed its IPv4 address. Add both, or a range. - **A VPN or a proxy**, which changes the address the API sees. Change the list under **My account → Developer**, on the token. [`token_ip_not_allowed`](errors.md#token_ip_not_allowed) ### `plan_limit` when creating a token, or `api.read` / `api.write` in the error API tokens come with the Pro plan and up; Free includes the read-only assistant only. See which plan includes what on [Limits](limits.md#what-each-plan-includes). In your [sandbox](sandbox.md), test tokens work on every plan. [`plan_limit`](errors.md#plan_limit) ### `scope_missing` The error's `required` field names the scope that is missing. Tokens cannot gain scopes later: create a new token with it, and give each token only what it needs. [`scope_missing`](errors.md#scope_missing) ### `write_disabled` Writes through the API are switched on per organization. Reads keep working while they are off. Ask an owner of the organization. In the [sandbox](sandbox.md), writes always work. [`write_disabled`](errors.md#write_disabled) ### `test_token_required` or `test_token_sandbox_only` Test tokens (`in_test_…`) work only in your sandbox, and live tokens never work there. Use a token made in the organization you are calling. [`test_token_required`](errors.md#test_token_required) ## Slowing down: `429 rate_limited` Each token has budgets: 300 requests and 60 changes a minute, and 1,000 changes an hour, by default (see [Limits](limits.md)). The error's `limit_key` says which one ran out. Every response carries `X-RateLimit-Remaining`; when it reaches zero, wait the seconds in `Retry-After` before the next request. Do not retry in a tight loop: the waiting time does not shorten. A script that has to change many records can spread them out, or use an endpoint that takes many at once. [`rate_limited`](errors.md#rate_limited) ## A retry created something twice Send an `Idempotency-Key` header with every `POST` and keep the same key when you retry. The API then returns the first answer instead of doing it again. See [Retries and dry runs](retries-and-dry-runs.md). ## Assistant and MCP connection problems ### Claude or ChatGPT will not connect - **The address is wrong.** The connector's URL is `https://api.infranest.io/mcp`, nothing more. See [Connect Claude and ChatGPT](ai/claude-and-chatgpt.md). - **You did not allow it.** The assistant gets access only after you pick an organization and select **Allow** on InfraNest's consent page. Connect again. - **It worked and then stopped.** An assistant that has not been used for 30 days has to be connected again, and one removed under **My account → Authorized apps** loses access at once. Connect it again. ### It answers about the wrong organization Connected with InfraNest, an assistant reads only the organization you picked on the consent page. Remove it under **My account → Authorized apps**, connect again and pick the other one. With a token, ask it to run `list_organizations` and name the one you mean, or create the token in the right organization. ### It says a scope is missing The token or the connection does not include that area. Give the token the read scope it names, or reconnect and pick an organization where your role allows it. [`scope_missing`](errors.md#scope_missing) ### Cursor, VS Code or Claude Code answers `401` The editor sent no token, or an empty one. The token goes in the header `Authorization: Bearer `. When the configuration reads it from `INFRANEST_TOKEN`, that variable must be set where the editor starts, not only in your terminal. See [Connect with a token](ai-assistants.md#connect-with-a-token). ### `api_token_required` The MCP server takes an API token or an assistant's connection, never a browser session. [`api_token_required`](errors.md#api_token_required) ### An answer is cut off One answer is at most 80,000 characters. Ask the assistant to use the tool's filters, or a later page. ## Still stuck Use **Report a problem** at the bottom of any page, or email support with the error's `reference`. Check [status.infranest.io](https://status.infranest.io) first if many requests fail at once. --- # Versioning and deprecation The InfraNest API has one version, and it changes by adding. There is no `/v1` in the address and no version header to send: a script written today keeps working, and anything that would break it is announced months ahead. ## What changes without notice - **New endpoints, new fields in a response, new optional parameters.** Write clients that ignore fields they do not know, and do not fail on an unknown key. - **New error codes.** A new refusal gets a new `code`; existing codes never change. Treat a code you do not know by its HTTP status. - **New values in a list of options** (a monitor type, an event name). Handle an unknown value without failing. - **Wording.** `message` and `hint` in an error, and descriptions in the reference, may be reworded. Branch on `code`, never on text. ## What gets six months' notice - **Removing or renaming** an endpoint, a field or a parameter. - **Changing what a field means**, including its type. Each is announced in the [changelog](changelog.md) at least six months before it takes effect, with the date it takes effect and what to use instead. Until that date both the old and the new way work. ## How changes are announced - **The [changelog](changelog.md)**, every change, each dated, newest first. - **The RSS feed** at [`/changelog.xml`](/changelog.xml), for a feed reader or a chat channel. - **[infranest-openapi](https://github.com/InfraNest-Infrastructure-Organized/infranest-openapi)**, where every published version of `openapi.json` is a dated release with what was added, removed and changed. Watch its releases to be told. ## `Deprecation` and `Sunset` headers The API does not send these headers yet. When it does, a response from something on its way out will carry `Deprecation` (when it was announced), `Sunset` (the date it stops working) and a `Link` to its changelog entry, so a client can log a warning without anyone reading the changelog. Until then, the changelog and its feed are where a deprecation is announced. ## Stable things to build on - **Error codes** — see [Errors](errors.md). - **Scopes** — see [Scopes](scopes.md). - **Webhook envelopes** — `event`, `created_at` and `data`, see [Webhooks](webhooks.md). --- # Webhooks InfraNest can tell your own system when something happens — a domain about to expire, a monitor going down, DNS records changing — by posting JSON to a URL you own. This page is for building the receiver: the events, what they carry, and how to check that a request really came from InfraNest. Endpoints are set up in the app, under **Integrations → Custom webhook**, not through the API: a URL and its signing secret are settings a token may never change. The help center's [Connect a custom webhook](https://infranest.io/docs/connect-custom-webhook) walks through it. Copy the **signing secret** (`whsec_…`) when you save the endpoint; it is shown once. ## What arrives A `POST` with a JSON body in one envelope, whatever the event: ```json { "event": "monitor.alert", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "check_name": "Website", "status": "down", "monitor_type": "http" } } ``` - **`event`** — which event it is. Branch on this. - **`created_at`** — when InfraNest sent it, ISO 8601 in UTC. - **`data`** — the event's own fields, listed below. Headers: | Header | What it carries | |---|---| | `X-InfraNest-Signature-V2` | `sha256=` and the HMAC-SHA256 of `"."`, in hex. See [Verify the signature](#verify-the-signature). | | `X-InfraNest-Timestamp` | Unix time, in seconds, that the signature covers. | | `X-InfraNest-Event` | The event name, the same as `event` in the body. | | `X-InfraNest-Delivery` | The delivery's ID, unique per delivery. Use it to ignore a delivery you already handled. | | `X-InfraNest-Signature` | **Deprecated**, sent until 2027-04-30. See [The old signature header](#the-old-signature-header). | | `Content-Type` | `application/json` | Every request carries these headers, whether it is a test delivery, an event your endpoint is subscribed to, or an event sent by an automation, including the **Send everything to …** automation an endpoint starts with. The only exception is an automation that calls a URL directly instead of a saved endpoint: there is no delivery, so there is no `X-InfraNest-Delivery`. Your endpoint has 10 seconds to answer, and only a `2xx` counts as received. A delivery is tried up to 6 times, waiting 30 seconds, then 5 minutes, then 30 minutes, then 2 hours, then 8 hours between tries. An event sent by an automation is retried by the automation, and each try is in its run history. ## Events Each sample is what **Send test event** delivers for that event, generated from the same list of fields the automation builder filters on. Values are examples; `"sample": true` is only ever in a test delivery. A real delivery may carry more fields than the sample: read the ones you need and ignore the rest.
domain.expiring ```json { "event": "domain.expiring", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "days_left": 14, "expiry_may_be_stale": true, "unused": true, "sample": true } } ```
domain.status_changed ```json { "event": "domain.status_changed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "change": "nameservers_changed", "old_nameservers": [ "ns1.old.example", "ns2.old.example" ], "new_nameservers": [ "ns1.new.example", "ns2.new.example" ], "sample": true } } ```
domain.dnssec_broken ```json { "event": "domain.dnssec_broken", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "sample": true } } ```
server.status_changed ```json { "event": "server.status_changed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "server_name": "web-01", "action": "cloud_server.powered_on", "sample": true } } ```
monitor.alert ```json { "event": "monitor.alert", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "check_name": "example", "status": "up", "monitor_type": "http", "sample": true } } ```
dns.records_changed ```json { "event": "dns.records_changed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "zone": "example", "provider": "aws", "sample": true } } ```
dns.live_mismatch ```json { "event": "dns.live_mismatch", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "zone": "example", "records": 1, "record_name": "example", "record_type": "A", "sample": true } } ```
watched_domain.dropping_soon ```json { "event": "watched_domain.dropping_soon", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "sample": true } } ```
watched_domain.available ```json { "event": "watched_domain.available", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "sample": true } } ```
watched_domain.registered ```json { "event": "watched_domain.registered", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "sample": true } } ```
watched_domain.failed ```json { "event": "watched_domain.failed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "sample": true } } ```
watched_domain.in_auction ```json { "event": "watched_domain.in_auction", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "auction": { "provider": "Example Auctions", "url": "https://auctions.example/example.com", "source": "http" }, "sample": true } } ```
watched_domain.released ```json { "event": "watched_domain.released", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "drop_eta": "2026-01-01T12:00:00+00:00", "sample": true } } ```
watched_domain.renewed ```json { "event": "watched_domain.renewed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "expires_at": "2026-01-01T12:00:00+00:00", "sample": true } } ```
watched_domain.funding_short ```json { "event": "watched_domain.funding_short", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "domain": "example.com", "tld": "com", "drop_eta": "2026-01-01T12:00:00+00:00", "sample": true } } ```
certificate.expiring ```json { "event": "certificate.expiring", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "subject": "example", "days_left": 14, "owner_name": "example", "owner_email": "example", "sample": true } } ```
certificate.expired ```json { "event": "certificate.expired", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "subject": "example", "sample": true } } ```
certificate.changed ```json { "event": "certificate.changed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "subject": "example", "sample": true } } ```
certificate.weak ```json { "event": "certificate.weak", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "subject": "example", "issues": [ "example" ], "sample": true } } ```
certificate.unexpected ```json { "event": "certificate.unexpected", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "subject": "example", "sample": true } } ```
certificate.revoked ```json { "event": "certificate.revoked", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "subject": "example", "issuer": "example", "host": "example", "sample": true } } ```
dynamic_ip.changed ```json { "event": "dynamic_ip.changed", "created_at": "2026-01-15T09:30:00.000000Z", "data": { "label": "example", "ip": "example", "ip_version": "v4", "current_ipv4": "example", "current_ipv6": "example", "country_code": "example", "asn_org": "example", "is_hosting": true, "sample": true } } ```
## Verify the signature Check every request before acting on it. Compute the HMAC-SHA256 of the timestamp, a full stop and the **raw request body, exactly as received**, `"."`, with your endpoint's signing secret. Do it before any JSON parsing, which changes spacing. Compare the result with `X-InfraNest-Signature-V2` in constant time, and refuse a timestamp more than 300 seconds from your clock, so a captured request cannot be replayed later. ::: code-group ```js [JavaScript] import crypto from 'node:crypto' // rawBody: the request body as a string or Buffer, before JSON.parse. export function verifyInfraNest(rawBody, headers, secret, toleranceSeconds = 300) { const signature = headers['x-infranest-signature-v2'] ?? '' const timestamp = headers['x-infranest-timestamp'] ?? '' if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false const signed = Buffer.concat([Buffer.from(`${timestamp}.`), Buffer.from(rawBody)]) const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(signed).digest('hex') const a = Buffer.from(expected) const b = Buffer.from(signature) return a.length === b.length && crypto.timingSafeEqual(a, b) } ``` ```python [Python] import hashlib import hmac import time def verify_infranest(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> bool: """raw_body: the request body as bytes, before json.loads.""" signature = headers.get("X-InfraNest-Signature-V2", "") timestamp = headers.get("X-InfraNest-Timestamp", "") if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance: return False signed = timestamp.encode() + b"." + raw_body expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` ```php [PHP] $tolerance) { return false; } return hash_equals('sha256='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret), (string) $signature); } // verifyInfraNest($rawBody, $_SERVER['HTTP_X_INFRANEST_TIMESTAMP'] ?? null, $_SERVER['HTTP_X_INFRANEST_SIGNATURE_V2'] ?? null, getenv('INFRANEST_WEBHOOK_SECRET')); ``` ```go [Go] package webhook import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" "time" ) // VerifyInfraNest checks a delivery: timestamp and signature are the X-InfraNest-Timestamp and // X-InfraNest-Signature-V2 headers, rawBody the body as received. func VerifyInfraNest(rawBody []byte, timestamp, signature, secret string) bool { ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } if age := time.Now().Unix() - ts; age > 300 || age < -300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) } ``` ```powershell [PowerShell] function Test-InfraNestSignature { param([byte[]]$RawBody, [string]$Timestamp, [string]$Signature, [string]$Secret, [int]$Tolerance = 300) if ($Timestamp -notmatch '^\d+$') { return $false } if ([Math]::Abs([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - [long]$Timestamp) -gt $Tolerance) { return $false } $signed = [Text.Encoding]::UTF8.GetBytes("$Timestamp.") + $RawBody $hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($Secret)) $expected = 'sha256=' + -join ($hmac.ComputeHash($signed) | ForEach-Object { $_.ToString('x2') }) return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals( [Text.Encoding]::ASCII.GetBytes($expected), [Text.Encoding]::ASCII.GetBytes([string]$Signature)) } ``` ```sh [Shell] # BODY holds the raw body; TIMESTAMP and SIGNATURE the X-InfraNest-Timestamp and X-InfraNest-Signature-V2 headers. expected="sha256=$(printf '%s' "$TIMESTAMP.$BODY" | openssl dgst -sha256 -hmac "$INFRANEST_WEBHOOK_SECRET" | awk '{print $NF}')" [ "$expected" = "$SIGNATURE" ] && echo "valid" || echo "invalid" ``` ::: Each of these, with a test against a known signature, is in [infranest-examples](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/verify-webhook). ### The old signature header `X-InfraNest-Signature` is deprecated and is sent until 2027-04-30. It carries what it always did, which depends on how the request was sent. On a test delivery or a subscribed event it is the same value as `X-InfraNest-Signature-V2`. On an event sent by an automation it is the HMAC of the body alone, with no timestamp, so it gives no protection against replay. A receiver that checks it should switch to `X-InfraNest-Signature-V2`. See the [changelog](changelog.md). ## Good practice - **Answer `2xx` quickly**, then do the work. Ten seconds is the limit; a slow job belongs in your own queue. - **Expect a delivery twice.** A retry after a timeout can repeat one your system already handled. Keep the `X-InfraNest-Delivery` IDs you have seen, or make handling idempotent. - **Use HTTPS**, and keep the secret where your other secrets are. Rotating it in the app stops the old one at once. - **Do not rely on order.** Events can arrive out of order; use `created_at` when order matters. Webhooks are the push side. To ask instead, poll the API — for example [`GET /status`](/reference/monitoring#get-organizations-organization-status) for everything that is up or down. --- # Domains Domains and contacts as InfraNest knows them. Registrar actions (register, renew, transfer) stay in the app. **Common tasks** - [List your domains](#get-organizations-organization-domains) - [Start tracking a domain by name](#post-organizations-organization-domains-track) - [Keep the contact book up to date](#post-organizations-organization-contacts) **Only in the app:** Registering, renewing and transferring domains, nameservers and registrar settings. **How it works in the app:** [help article](https://infranest.io/docs/domains). **Scopes:** Read with `domains.read`, change with `domains.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Contacts](#contacts) 5 · [Domain findings](#domain-findings) 1 · [Domains](#domains) 26
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Contacts ### List contacts {#get-organizations-organization-contacts}
`GET` `/organizations/{organization}/contacts` **Scope:** `domains.read` · **Changes data:** no Your organization’s contacts, paginated with `page` and `per_page` (up to 200), with their tags. Filter with `search` or `tag`, and sort with `sort` (`name`, `company`, `email`, `city`, `country`) and `dir`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/contacts" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 1, "organization_id": 1, "first_name": "Alex", "last_name": "Rivera", "company": "Northwind Cloud", "email": "domains@northwind.example", "phone": "+31 20 555 0123", "address": "Keizersgracht", "city": "Amsterdam", "state": null, "zip": "1015 CJ", "country": "NL", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "house_number": "123", "vat_number": null, "company_number": null, "missing_for_registration": [], "tags": [] }, { "id": 3, "organization_id": 1, "first_name": "Priya", "last_name": "Nair", "company": "Northwind Cloud", "email": "legal@northwind.example", "phone": "+44 20 7946 0321", "address": "Shoreditch High Street", "city": "London", "state": "Greater London", "zip": "E1 6JJ", "country": "GB", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "house_number": "42", "vat_number": null, "company_number": null, "missing_for_registration": [], "tags": [] } ], "first_page_url": "http://localhost/api/organizations/1/contacts?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/contacts?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/contacts?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/contacts", "per_page": 25, "prev_page_url": null, "to": 4, "total": 4, "meta": { "tags": [] } } ```
--- ### Create a contact {#post-organizations-organization-contacts}
`POST` `/organizations/{organization}/contacts` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates a contact you can assign to domains as registrant, admin, tech or billing contact, optionally tagged with `tag_ids`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `first_name`
body · required | string | Must not be greater than 100 characters. | | `last_name`
body · required | string | Must not be greater than 100 characters. | | `company`
body | string, nullable | Must not be greater than 255 characters. | | `vat_number`
body | string, nullable | Must not be greater than 50 characters. | | `company_number`
body | string, nullable | Must not be greater than 50 characters. | | `email`
body · required | string | Must be a valid email address. Must not be greater than 255 characters. | | `phone`
body | string, nullable | Must not be greater than 50 characters. | | `address`
body | string, nullable | Must not be greater than 255 characters. | | `house_number`
body | string, nullable | Must not be greater than 20 characters. | | `city`
body | string, nullable | Must not be greater than 100 characters. | | `state`
body | string, nullable | Must not be greater than 100 characters. | | `zip`
body | string, nullable | Must not be greater than 20 characters. | | `country`
body · required | string | Must be 2 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/contacts" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"first_name":"Ada","last_name":"Brandt","email":"ada@northwind.example","country":"DE"}' ``` **Response** · 201 — Created ```json { "first_name": "Ada", "last_name": "Brandt", "email": "ada@northwind.example", "country": "DE", "organization_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 5, "missing_for_registration": [ "address", "city" ], "tags": [] } ```
--- ### Get a contact {#get-organizations-organization-contacts-id}
`GET` `/organizations/{organization}/contacts/{id}` **Scope:** `domains.read` · **Changes data:** no One contact with the domains it is assigned to. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `id`
path · required | integer | The id's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/contacts/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "first_name": "Alex", "last_name": "Rivera", "company": "Northwind Cloud", "email": "domains@northwind.example", "phone": "+31 20 555 0123", "address": "Keizersgracht", "city": "Amsterdam", "state": null, "zip": "1015 CJ", "country": "NL", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "house_number": "123", "vat_number": null, "company_number": null, "missing_for_registration": [], "domains": [] } ```
--- ### Update a contact {#put-organizations-organization-contacts-id}
`PUT` `/organizations/{organization}/contacts/{id}` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Updates the contact and, when `tag_ids` is sent, its tags. Recorded in InfraNest only; nothing is sent to a registrar. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `id`
path · required | integer | The id's ID. | | `first_name`
body | string | Must not be greater than 100 characters. | | `last_name`
body | string | Must not be greater than 100 characters. | | `company`
body | string, nullable | Must not be greater than 255 characters. | | `vat_number`
body | string, nullable | Must not be greater than 50 characters. | | `company_number`
body | string, nullable | Must not be greater than 50 characters. | | `email`
body | string | Must be a valid email address. Must not be greater than 255 characters. | | `phone`
body | string, nullable | Must not be greater than 50 characters. | | `address`
body | string, nullable | Must not be greater than 255 characters. | | `house_number`
body | string, nullable | Must not be greater than 20 characters. | | `city`
body | string, nullable | Must not be greater than 100 characters. | | `state`
body | string, nullable | Must not be greater than 100 characters. | | `zip`
body | string, nullable | Must not be greater than 20 characters. | | `country`
body | string | Must be 2 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/contacts/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"company":"Northwind Cloud GmbH","city":"Köln"}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "first_name": "Alex", "last_name": "Rivera", "company": "Northwind Cloud GmbH", "email": "domains@northwind.example", "phone": "+31 20 555 0123", "address": "Keizersgracht", "city": "Köln", "state": null, "zip": "1015 CJ", "country": "NL", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "house_number": "123", "vat_number": null, "company_number": null, "missing_for_registration": [], "tags": [] } ```
--- ### Delete a contact {#delete-organizations-organization-contacts-id}
`DELETE` `/organizations/{organization}/contacts/{id}` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Deletes the contact from your organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `id`
path · required | integer | The id's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/contacts/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
## Domain findings ### List domain findings {#get-organizations-organization-domain-findings}
`GET` `/organizations/{organization}/domain-findings` **Scope:** `domains.read` · **Changes data:** no Advisor findings across all your domains, most urgent first, with one finding per cause naming every domain it affects. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domain-findings" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "key": "expiry", "check": "expiry", "status": "error", "variant": "lapsed", "params": { "days": -6, "expires_at": "2026-01-09" }, "action": "renew", "cause": "domain:18:expiry", "rank": 390, "home": { "source": "domain_expiring", "key": "domain:18", "fingerprint": "2026-01-09" }, "snoozed": false, "domains": [ { "id": 18, "name": "nwcloud.app" } ] }, { "key": "dnssec", "check": "dnssec", "status": "error", "variant": "notValidating", "params": { "since": "2026-01-13T12:00:00.000000Z" }, "action": null, "cause": "domain:9:dnssec", "rank": 350, "home": { "source": "dnssec_broken", "key": "dnssec_broken:9", "fingerprint": "" }, "snoozed": false, "domains": [ { "id": 9, "name": "northwind.de" } ] } ] } ```
## Domains ### List domains {#get-organizations-organization-domains}
`GET` `/organizations/{organization}/domains` **Scope:** `domains.read` · **Changes data:** no Your domains, paginated with `page` and `per_page` (up to 200), each with its tags, advisor issue count and who hosts its website and mail. Filter with `search`, `credential`, `tag`, `active`, `expiring_soon`, `autorenew_off`, `website`, `mail` or `archived` (archived domains are left out unless asked for). Registration orders that failed are not domains you hold and are left out too; pass `include_failed=1` to list them, and `meta.counts.failed` says how many there are. Sort with `sort` (`domain`, `registrar`, `expires`) and `dir`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 11, "organization_id": 1, "name": "northwind", "tld": "cloud", "registrar": "Porkbun", "registered_at": "2024-01-14T00:00:00.000000Z", "expires_at": "2026-11-12T00:00:00.000000Z", "auto_renew": true, "status": "active", "notes": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": [ "hydrogen.ns.hetzner.com", "oxygen.ns.hetzner.com" ], "transfer_lock": true, "whois_privacy": true, "registrar_contacts": null, "dnssec_entries": null, "has_dnssec": null, "renewal_price": "22.00", "renewal_price_currency": "EUR", "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "manual", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": null, "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": null, "dnssec_checked_at": null, "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "custom_fields": { "1": "Marketing" }, "advisor_issues": 0, "uses": { "website": { "kind": "server", "label": "web-01", "url": "/servers/1", "flagged": false, "since": null, "provider": "Hetzner", "note": null, "ours": false }, "site": { "key": "host:hetzner", "label": "Hetzner" }, "mail": { "key": "none", "label": null }, "no_zone": null }, "full_name": "northwind.cloud", "archivable": true, "dns_host": "hetzner", "management": "tracked", "sync_halt": null, "provider_credential": null, "tags": [ { "id": 4, "name": "Internal", "color": "#8b5cf6", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 11, "tag_id": 4 } } ] }, { "id": 1, "organization_id": 1, "name": "northwind", "tld": "com", "registrar": "Namecheap", "registered_at": "2023-12-28T00:00:00.000000Z", "expires_at": "2026-02-02T00:00:00.000000Z", "auto_renew": true, "status": "active", "notes": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "transfer_lock": true, "whois_privacy": true, "registrar_contacts": null, "dnssec_entries": [ { "key_tag": 62949, "algorithm": 13, "digest_type": 2, "digest": "FBF66B8D019C1EEC1E1A0B5A9D3C2E7F4A8B60D5C93E71F2A6B8D04C7E19A35B" } ], "has_dnssec": true, "renewal_price": "12.50", "renewal_price_currency": "EUR", "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "manual", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": null, "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": "signed", "dnssec_checked_at": "2026-01-15T11:25:00.000000Z", "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "custom_fields": { "1": "Marketing" }, "advisor_issues": 0, "uses": { "website": { "kind": "server", "label": "web-01", "url": "/servers/1", "flagged": false, "since": null, "provider": "Hetzner", "note": { "key": "proxied", "params": { "provider": "Cloudflare" } }, "ours": false }, "site": { "key": "host:hetzner", "label": "Hetzner" }, "mail": { "key": "other", "label": "mx.northwind.com" }, "no_zone": null }, "full_name": "northwind.com", "archivable": true, "dns_host": "cloudflare", "management": "tracked", "sync_halt": null, "provider_credential": null, "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 1, "tag_id": 5 } } ] } ], "first_page_url": "http://localhost/api/organizations/1/domains?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/domains?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/domains?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/domains", "per_page": 25, "prev_page_url": null, "to": 20, "total": 20, "meta": { "locked_ids": [], "expiry_window_days": 60, "counts": { "total": 20, "active": 19, "expiring_soon": 3, "autorenew_off": 5, "failed": 0, "archived": 0 }, "integrations": [], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6" }, { "id": 4, "name": "Internal", "color": "#8b5cf6" } ], "uses": { "sites": [ { "key": "host:hetzner", "label": "Hetzner", "count": 12 }, { "key": "none", "label": null, "count": 1 } ], "mail": [ { "key": "other", "label": null, "count": 7 }, { "key": "none", "label": null, "count": 6 } ] }, "custom_fields": [ { "id": 1, "key": "cost_centre", "label": "Cost centre", "type": "select", "options": [ "Platform", "Marketing" ] } ] } } ```
--- ### List a domain’s running registrar actions {#get-organizations-organization-domains-domain-actions}
`GET` `/organizations/{organization}/domains/{domain}/actions` **Scope:** `domains.read` · **Changes data:** no The action currently running at the registrar for this domain, where the registrar reports one; `can_retry` says whether it can be retried. An empty list when no registrar is linked or the registrar does not offer this. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/actions" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "actions": [] } ```
--- ### Get a domain’s advisor findings {#get-organizations-organization-domains-domain-advisor}
`GET` `/organizations/{organization}/domains/{domain}/advisor` **Scope:** `domains.read` · **Changes data:** no The current advisor findings for one domain, taking into account what its registrar supports. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/advisor" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "expiry_window_days": 60, "findings": [ { "key": "expiry", "check": "expiry", "status": "info", "variant": "renewalUnknown", "params": { "days": 18, "expires_at": "2026-02-02", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "action": null, "cause": "domain:1:expiry", "rank": 222, "home": { "source": "domain_finding", "key": "domain_finding:domain:1:expiry", "fingerprint": "" }, "snoozed": false }, { "key": "auto_renew", "check": "auto_renew", "status": "info", "variant": "unknown", "params": { "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "action": null, "cause": "portfolio:auto_renew", "rank": 150, "home": { "source": "domain_finding", "key": "domain_finding:portfolio:auto_renew", "fingerprint": "" }, "snoozed": false } ] } ```
--- ### Archive a domain {#post-organizations-organization-domains-domain-archive}
`POST` `/organizations/{organization}/domains/{domain}/archive` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Takes the domain out of your working list while keeping it, its costs and its links; it keeps syncing and stops counting toward your plan’s domain limit. Refused with 422 while the domain is still active at a connected registrar. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/domains/1/archive" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "northwind", "tld": "com", "registrar": "Namecheap", "registered_at": "2023-12-28T00:00:00.000000Z", "expires_at": "2026-02-02T00:00:00.000000Z", "auto_renew": true, "status": "active", "notes": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "transfer_lock": true, "whois_privacy": true, "registrar_contacts": null, "dnssec_entries": [ { "key_tag": 62949, "algorithm": 13, "digest_type": 2, "digest": "FBF66B8D019C1EEC1E1A0B5A9D3C2E7F4A8B60D5C93E71F2A6B8D04C7E19A35B" } ], "has_dnssec": true, "renewal_price": "12.50", "renewal_price_currency": "EUR", "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "manual", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": "2026-01-15T12:00:00.000000Z", "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": "signed", "dnssec_checked_at": "2026-01-15T11:25:00.000000Z", "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "full_name": "northwind.com", "archivable": true, "dns_host": "cloudflare", "management": "tracked", "sync_halt": null } ```
--- ### Get a domain’s registrar capabilities {#get-organizations-organization-domains-domain-capabilities}
`GET` `/organizations/{organization}/domains/{domain}/capabilities` **Scope:** `domains.read` · **Changes data:** no Which registrar operations are available for this domain, given what its registrar and extension support, with a `reason` when something is unavailable. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/capabilities" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "capabilities": { "nameservers": false, "contacts": false, "dnssec": false, "transfer_lock": false, "whois_privacy": false, "auto_renew": false, "auto_renew_reenable": false, "complete_domain_list": false, "epp_code": false, "register": false, "transfer": false, "renew": false }, "reason": "no_registrar_credential", "registry_withheld": [], "contact_roles": [ "registrant", "admin" ], "dnssec_fields": [ "key_tag", "flags" ] } ```
--- ### List a domain’s contacts {#get-organizations-organization-domains-domain-contacts}
`GET` `/organizations/{organization}/domains/{domain}/contacts` **Scope:** `domains.read` · **Changes data:** no The contacts assigned to the domain, each with its role in `pivot.type`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/contacts" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
--- ### Assign a contact to a domain {#post-organizations-organization-domains-domain-contacts}
`POST` `/organizations/{organization}/domains/{domain}/contacts` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Assigns one of your contacts (`contact_id`) to the domain as `registrant`, `admin`, `tech` or `billing`, replacing whoever held that role. Recorded in InfraNest only; nothing is sent to the registrar. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | | `contact_id`
body · required | integer | | | `type`
body · required | string | One of: `registrant`, `admin`, `tech`, `billing`. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/domains/1/contacts" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"contact_id":1,"type":"admin"}' ``` **Response** · 201 — Created ```json { "id": 1, "organization_id": 1, "first_name": "Alex", "last_name": "Rivera", "company": "Northwind Cloud", "email": "domains@northwind.example", "phone": "+31 20 555 0123", "address": "Keizersgracht", "city": "Amsterdam", "state": null, "zip": "1015 CJ", "country": "NL", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "house_number": "123", "vat_number": null, "company_number": null, "missing_for_registration": [], "pivot": { "type": "admin" } } ```
--- ### Remove a contact from a domain {#delete-organizations-organization-domains-domain-contacts-contact}
`DELETE` `/organizations/{organization}/domains/{domain}/contacts/{contact}` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Unassigns the contact from the domain in every role it held. The contact itself is kept. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | | `contact_id`
path · required | integer | The contact's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/domains/1/contacts/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
--- ### Get a domain’s transfer code {#get-organizations-organization-domains-domain-epp-code}
`GET` `/organizations/{organization}/domains/{domain}/epp-code` **Scope:** `domains.read` · **Changes data:** no Fetches the domain’s EPP (auth) code live from its registrar; InfraNest never stores it. Needs write access; answers 404 when no registrar is linked or the registrar cannot provide the code. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/epp-code" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Get a domain’s audit history {#get-organizations-organization-domains-domain-history}
`GET` `/organizations/{organization}/domains/{domain}/history` **Scope:** `domains.read` · **Changes data:** no The 50 most recent audit-log entries for the domain and its manual cost items, newest first. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/history" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
--- ### Check a domain’s nameservers {#get-organizations-organization-domains-domain-ns-health}
`GET` `/organizations/{organization}/domains/{domain}/ns-health` **Scope:** `domains.read` · **Changes data:** no Checks each of the domain’s nameservers now: whether it answers on port 53 and is authoritative, whether the nameservers agree with each other, and whether DNSSEC validates. Empty when the domain has no nameservers; the result is kept for the advisor. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/ns-health" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "checks": [ { "ns": "nina.ns.cloudflare.com", "authoritative": true, "reachable": true }, { "ns": "rick.ns.cloudflare.com", "authoritative": true, "reachable": true } ], "ns_consistency": { "consistent": true, "resolvers": { "cloudflare": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "google": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "dnssb": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "adguard": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "nextdns": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ] } }, "dnssec_validates": true, "zone_delegation": { "zone_id": 1, "zone_name": "northwind.com", "provider": "cloudflare", "delegated": true, "delegated_to": null } } ```
--- ### Get a domain’s registrar pricing {#get-organizations-organization-domains-domain-pricing}
`GET` `/organizations/{organization}/domains/{domain}/pricing` **Scope:** `domains.read` · **Changes data:** no The registrar’s current prices for the domain’s extension, asked live. Answers 404 when no registrar connection is linked; `pricing` is null when the registrar does not allow price lookups with your key. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/pricing" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Get a domain’s registry record {#get-organizations-organization-domains-domain-registry-record}
`GET` `/organizations/{organization}/domains/{domain}/registry-record` **Scope:** `domains.read` · **Changes data:** no What the registry itself says about the domain: its status codes, created and updated dates, the source (RDAP or WHOIS) and an excerpt of the raw answer. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/registry-record" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "statuses": [], "created_at": null, "updated_at": null, "source": null, "raw": null } ```
--- ### Get how a domain is set up {#get-organizations-organization-domains-domain-setup}
`GET` `/organizations/{organization}/domains/{domain}/setup` **Scope:** `domains.read` · **Changes data:** no Who hosts the domain’s DNS and whether its nameservers answer, its mail provider, and the servers, certificates, monitors and status pages behind its addresses. Based on the DNS zone InfraNest holds; a domain whose DNS is hosted elsewhere gets no website or email details. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/setup" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "zone": { "id": 1, "name": "northwind.com" }, "registrar": { "label": "Namecheap", "managed": false, "credential_id": null, "provider": null }, "dns": { "provider": "cloudflare", "provider_label": "Cloudflare", "connected": true, "zone_here": true, "nameservers": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "answering": null, "checked_at": null }, "email": { "provider": null, "hosts": [ "mx.northwind.com" ] }, "website": { "records": [ { "type": "A", "value": "198.51.100.10", "url": "/dns/1?record=1" } ], "server": { "type": "server", "id": 1, "label": "web-01", "sublabel": "198.51.100.10", "url": "/servers/1", "meta": [] } }, "runs_on": [ { "record_id": 1, "zone_id": 1, "host": "northwind.com", "type": "A", "value": "198.51.100.10", "url": "/dns/1?record=1", "server": { "type": "server", "id": 1, "label": "web-01", "sublabel": "198.51.100.10", "url": "/servers/1", "meta": [] }, "certificate": { "type": "certificate", "id": 1, "label": "northwind.com", "sublabel": "2026-03-30", "url": "/certificates/1", "meta": [] }, "monitors": [ { "type": "monitor", "id": 1, "label": "Marketing site", "sublabel": "https://northwind.com", "url": "/monitoring/1", "meta": { "type": "http", "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z" } }, { "type": "monitor", "id": 4, "label": "Checkout flow", "sublabel": "https://northwind.com/checkout", "url": "/monitoring/4", "meta": { "type": "http", "last_status": "down", "last_checked_at": "2026-01-15T11:58:00.000000Z" } } ], "status_pages": [] }, { "record_id": 3, "zone_id": 1, "host": "api.northwind.com", "type": "CNAME", "value": "northwind.com", "url": "/dns/1?record=3", "server": null, "certificate": { "type": "certificate", "id": 3, "label": "api.northwind.com", "sublabel": "2026-04-13", "url": "/certificates/3", "meta": [] }, "monitors": [ { "type": "monitor", "id": 3, "label": "API health", "sublabel": "https://api.northwind.com/health", "url": "/monitoring/3", "meta": { "type": "http", "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z" } }, { "type": "monitor", "id": 6, "label": "Search API", "sublabel": "https://api.northwind.com/v1/search", "url": "/monitoring/6", "meta": { "type": "api", "last_status": "degraded", "last_checked_at": "2026-01-15T11:58:00.000000Z" } } ], "status_pages": [] } ], "mail": [ { "record_id": 4, "value": "mx.northwind.com", "url": "/dns/1?record=4" } ], "unmonitored": [ "archive.northwind.com", "cdn.northwind.com" ] } ```
--- ### Unarchive a domain {#post-organizations-organization-domains-domain-unarchive}
`POST` `/organizations/{organization}/domains/{domain}/unarchive` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Brings an archived domain back into your working list, where it counts toward your plan’s domain limit again. A domain that is not archived is returned unchanged. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/domains/1/unarchive" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "northwind", "tld": "com", "registrar": "Namecheap", "registered_at": "2023-12-28T00:00:00.000000Z", "expires_at": "2026-02-02T00:00:00.000000Z", "auto_renew": true, "status": "active", "notes": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "transfer_lock": true, "whois_privacy": true, "registrar_contacts": null, "dnssec_entries": [ { "key_tag": 62949, "algorithm": 13, "digest_type": 2, "digest": "FBF66B8D019C1EEC1E1A0B5A9D3C2E7F4A8B60D5C93E71F2A6B8D04C7E19A35B" } ], "has_dnssec": true, "renewal_price": "12.50", "renewal_price_currency": "EUR", "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "manual", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": null, "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": "signed", "dnssec_checked_at": "2026-01-15T11:25:00.000000Z", "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "full_name": "northwind.com", "archivable": true, "dns_host": "cloudflare", "management": "tracked", "sync_halt": null } ```
--- ### Look up a domain’s WHOIS {#get-organizations-organization-domains-domain-whois}
`GET` `/organizations/{organization}/domains/{domain}/whois` **Scope:** `domains.read` · **Changes data:** no A live WHOIS lookup for the domain. Answers 422 with a message when the lookup fails. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `domain_id`
path · required | integer | The domain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1/whois" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "server": "whois.verisign-grs.com", "output": " Domain Name: NORTHWIND.COM\r\n Registry Domain ID: 4516866_DOMAIN_COM-VRSN\r\n Registrar WHOIS Server: whois.godaddy.…" } ```
--- ### Get a domain {#get-organizations-organization-domains-id}
`GET` `/organizations/{organization}/domains/{id}` **Scope:** `domains.read` · **Changes data:** no One domain with its full details. A domain over your plan’s domain limit is locked and cannot be opened; stale renewal pricing is refreshed in the background. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `id`
path · required | integer | The id's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "northwind", "tld": "com", "registrar": "Namecheap", "registered_at": "2023-12-28T00:00:00.000000Z", "expires_at": "2026-02-02T00:00:00.000000Z", "auto_renew": true, "status": "active", "notes": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "transfer_lock": true, "whois_privacy": true, "registrar_contacts": null, "dnssec_entries": [ { "key_tag": 62949, "algorithm": 13, "digest_type": 2, "digest": "FBF66B8D019C1EEC1E1A0B5A9D3C2E7F4A8B60D5C93E71F2A6B8D04C7E19A35B" } ], "has_dnssec": true, "renewal_price": "12.50", "renewal_price_currency": "EUR", "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "manual", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": null, "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": "signed", "dnssec_checked_at": "2026-01-15T11:25:00.000000Z", "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "full_name": "northwind.com", "archivable": true, "dns_host": "cloudflare", "management": "tracked", "sync_halt": null, "contacts": [], "provider_credential": null, "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 1, "tag_id": 5 } } ], "dns_zone": { "id": 1, "name": "northwind.com", "provider": "cloudflare", "provider_label": "Cloudflare", "points_here": true, "can_sign": true }, "tracked": { "registrar": "Namecheap", "provider": { "key": "namecheap", "label": "Namecheap", "integrable": true }, "connected": true, "checked_at": null }, "capabilities": { "nameservers": false, "contacts": false, "dnssec": false, "transfer_lock": false, "whois_privacy": false, "auto_renew": false, "auto_renew_reenable": false, "complete_domain_list": false, "epp_code": false, "register": false, "transfer": false, "renew": false }, "capabilities_reason": "no_registrar_credential", "registry_withheld": [], "contact_roles": [ "registrant", "admin" ], "settings": { "transfer_lock": { "state": "on", "source": "registry", "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "auto_renew": { "state": "unknown", "source": null, "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "whois_privacy": { "state": "on", "source": "registrar", "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "dnssec": { "state": "unknown", "source": null, "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "caa": { "state": "off", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "spf": { "state": "on", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "dmarc": { "state": "on", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "email": { "state": "on", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "dkim": { "state": "off", "source": "zone", "route": "elsewhere", "where": null }, "no_mail": { "state": "none", "source": null, "route": null, "where": null } }, "advisor": { "expiry_window_days": 60, "findings": [ { "key": "expiry", "check": "expiry", "status": "info", "variant": "renewalUnknown", "params": { "days": 18, "expires_at": "2026-02-02", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "action": null, "cause": "domain:1:expiry", "rank": 222, "home": { "source": "domain_finding", "key": "domain_finding:domain:1:expiry", "fingerprint": "" }, "snoozed": false }, { "key": "auto_renew", "check": "auto_renew", "status": "info", "variant": "unknown", "params": { "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "action": null, "cause": "portfolio:auto_renew", "rank": 150, "home": { "source": "domain_finding", "key": "domain_finding:portfolio:auto_renew", "fingerprint": "" }, "snoozed": false } ] } } ```
--- ### Update a domain {#put-organizations-organization-domains-id}
`PUT` `/organizations/{organization}/domains/{id}` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Updates what InfraNest keeps for the domain: `notes`, `serves_website`, `expected_issuers`, `nameservers`, `registrar_contacts`, pricing and `tag_ids`. Registration data such as the expiry date and auto-renew is not accepted here; returns the domain’s full details. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `id`
path · required | integer | The id's ID. | | `notes`
body | string, nullable | Must not be greater than 10000 characters. | | `tag_ids`
body | array | | | `serves_website`
body | boolean, nullable | | | `expected_issuers`
body | array | Must not be greater than 100 characters. | | `nameservers`
body | array | Must not be greater than 253 characters. | | `registrar_contacts`
body | object, nullable | | | `renewal_price`
body | number, nullable | Must be at least 0. Must not be greater than 9999999.99. | | `renewal_price_currency`
body | string, nullable | Must be 3 characters. | | `billing_interval`
body | string | One of: `hourly`, `daily`, `weekly`, `monthly`, `quarterly`, `yearly`. | | `interval_count`
body | integer | Must be at least 1. Must not be greater than 120. | | `purchase_price`
body | number, nullable | Must be at least 0. Must not be greater than 9999999.99. | | `purchase_price_currency`
body | string, nullable | Must be 3 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/domains/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"notes":"Main brand domain. Renew three years ahead."}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "northwind", "tld": "com", "registrar": "Namecheap", "registered_at": "2023-12-28T00:00:00.000000Z", "expires_at": "2026-02-02T00:00:00.000000Z", "auto_renew": true, "status": "active", "notes": "Main brand domain. Renew three years ahead.", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "transfer_lock": true, "whois_privacy": true, "registrar_contacts": null, "dnssec_entries": [ { "key_tag": 62949, "algorithm": 13, "digest_type": 2, "digest": "FBF66B8D019C1EEC1E1A0B5A9D3C2E7F4A8B60D5C93E71F2A6B8D04C7E19A35B" } ], "has_dnssec": true, "renewal_price": "12.50", "renewal_price_currency": "EUR", "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "manual", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": null, "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": "signed", "dnssec_checked_at": "2026-01-15T11:25:00.000000Z", "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "full_name": "northwind.com", "archivable": true, "dns_host": "cloudflare", "management": "tracked", "sync_halt": null, "contacts": [ { "id": 1, "organization_id": 1, "first_name": "Alex", "last_name": "Rivera", "company": "Northwind Cloud", "email": "domains@northwind.example", "phone": "+31 20 555 0123", "address": "Keizersgracht", "city": "Amsterdam", "state": null, "zip": "1015 CJ", "country": "NL", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "house_number": "123", "vat_number": null, "company_number": null, "missing_for_registration": [], "pivot": { "domain_id": 1, "contact_id": 1, "type": "tech", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } } ], "provider_credential": null, "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\Domain", "taggable_id": 1, "tag_id": 5 } } ], "dns_zone": { "id": 1, "name": "northwind.com", "provider": "cloudflare", "provider_label": "Cloudflare", "points_here": true, "can_sign": true }, "tracked": { "registrar": "Namecheap", "provider": { "key": "namecheap", "label": "Namecheap", "integrable": true }, "connected": true, "checked_at": null }, "capabilities": { "nameservers": false, "contacts": false, "dnssec": false, "transfer_lock": false, "whois_privacy": false, "auto_renew": false, "auto_renew_reenable": false, "complete_domain_list": false, "epp_code": false, "register": false, "transfer": false, "renew": false }, "capabilities_reason": "no_registrar_credential", "registry_withheld": [], "contact_roles": [ "registrant", "admin" ], "settings": { "transfer_lock": { "state": "on", "source": "registry", "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "auto_renew": { "state": "unknown", "source": null, "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "whois_privacy": { "state": "on", "source": "registrar", "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "dnssec": { "state": "unknown", "source": null, "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "caa": { "state": "off", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "spf": { "state": "on", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "dmarc": { "state": "on", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "email": { "state": "on", "source": "zone", "route": "here", "where": { "label": "Cloudflare", "url": null } }, "dkim": { "state": "off", "source": "zone", "route": "elsewhere", "where": null }, "no_mail": { "state": "none", "source": null, "route": null, "where": null } }, "advisor": { "expiry_window_days": 60, "findings": [ { "key": "expiry", "check": "expiry", "status": "info", "variant": "renewalUnknown", "params": { "days": 18, "expires_at": "2026-02-02", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "action": null, "cause": "domain:1:expiry", "rank": 222, "home": { "source": "domain_finding", "key": "domain_finding:domain:1:expiry", "fingerprint": "" }, "snoozed": false }, { "key": "auto_renew", "check": "auto_renew", "status": "info", "variant": "unknown", "params": { "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" } }, "action": null, "cause": "portfolio:auto_renew", "rank": 150, "home": { "source": "domain_finding", "key": "domain_finding:portfolio:auto_renew", "fingerprint": "" }, "snoozed": false } ] } } ```
--- ### Plan CAA protection across domains {#get-organizations-organization-domains-caa-protection-plan}
`GET` `/organizations/{organization}/domains/caa-protection-plan` **Scope:** `domains.read` · **Changes data:** no For each domain in `ids` (up to 200), the CAA plan and its `route`, as for the email protection plan. Changes nothing; apply it through each zone’s CAA protection endpoint, which checks the zone’s certificates first. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `ids`
body | array | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/caa-protection-plan" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "domains": [ { "id": 1, "name": "northwind.com", "zone_id": 1, "route": "here", "plan": { "zone_id": 1, "zone": "northwind.com", "route": "here", "where": { "label": "Cloudflare", "url": null }, "records": [ { "key": "kept:7", "action": "none", "type": "CAA", "name": "@", "value": "0 issue \"letsencrypt.org\"", "current": null, "record_id": null, "reason": "caa_kept", "params": [] }, { "key": "issue:globalsign.com", "action": "create", "type": "CAA", "name": "@", "value": "0 issue \"globalsign.com\"", "current": null, "record_id": null, "reason": "caa_cert", "params": { "subject": "shop.northwind.com" } } ], "refusals": [], "certificates": [ { "id": 1, "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "ca": "letsencrypt.org", "wildcard": false, "state": "stays_renewable" }, { "id": 13, "subject": "cdn.northwind.com", "issuer": "Let's Encrypt (R3)", "ca": "letsencrypt.org", "wildcard": false, "state": "stays_renewable" } ], "fronts": [ { "label": "Cloudflare", "ids": [ "letsencrypt.org", "pki.goog" ], "publishes_own": true } ] } } ] } ```
--- ### Plan email protection across domains {#get-organizations-organization-domains-email-protection-plan}
`GET` `/organizations/{organization}/domains/email-protection-plan` **Scope:** `domains.read` · **Changes data:** no For each domain in `ids` (up to 200), the email protection plan for `mode` (`protect` or `no_mail`) and its `route`: `here`, `elsewhere` (records to copy to your DNS host), `reconnect`, `unchanged`, `refused` or `none` (no zone held). Changes nothing; apply it through each zone’s email protection endpoint. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `mode`
body · required | string | | | `ids`
body | array | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/email-protection-plan?mode=%3Cmode%3E" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "mode": "no_mail", "domains": [ { "id": 1, "name": "northwind.com", "zone_id": 1, "route": "refused", "plan": { "zone_id": 1, "zone": "northwind.com", "mode": "no_mail", "route": "here", "where": { "label": "Cloudflare", "url": null }, "provider": null, "providers": [], "records": [], "refusals": [ { "key": "mode", "reason": "receives_mail", "params": [] } ], "lookups": null, "dkim": null } } ] } ```
--- ### Get the domains overview {#get-organizations-organization-domains-overview}
`GET` `/organizations/{organization}/domains/overview` **Scope:** `domains.read` · **Changes data:** no A summary of your domains: what needs your attention, what the next year of renewals costs in your base currency, and how the portfolio is protected. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/overview" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "summary": { "total": 20, "fine": 16, "need_you": 4, "this_week": 2, "registrars": 7, "renewal_cost": 327.5, "currency": "EUR", "auto_renewing": 0, "synced_at": null, "next_renewal": { "domain_id": 14, "name": "northwind-labs.com", "expires_at": "2026-01-24", "auto_renew": false } }, "needs_you": [ { "key": "expiry", "check": "expiry", "status": "error", "variant": "lapsed", "params": { "days": -6, "expires_at": "2026-01-09" }, "action": "renew", "cause": "domain:18:expiry", "rank": 390, "home": { "source": "domain_expiring", "key": "domain:18", "fingerprint": "2026-01-09" }, "snoozed": false, "domains": [ { "id": 18, "name": "nwcloud.app" } ], "attention_keys": [ "domain:18" ], "due_in_days": 0, "renewal": null, "evidence": [ { "source": "registrar", "state": "auto_renew_off", "tone": "bad" } ], "prevention": { "template": "auto_renew_expiring_domains", "active": false } }, { "key": "dnssec", "check": "dnssec", "status": "error", "variant": "notValidating", "params": { "since": "2026-01-13T12:00:00.000000Z" }, "action": null, "cause": "domain:9:dnssec", "rank": 350, "home": { "source": "dnssec_broken", "key": "dnssec_broken:9", "fingerprint": "" }, "snoozed": false, "domains": [ { "id": 9, "name": "northwind.de" } ], "attention_keys": [ "dnssec_broken:9" ], "due_in_days": 0, "renewal": null, "evidence": [], "prevention": null } ], "decided": [], "renewals": { "currency": "EUR", "priced": true, "total": 327.5, "items": 18, "unpriced": 0, "months": [ { "month": "2026-01", "cost": 44.5, "items": 2, "unpriced": 0, "decide_cost": 44.5, "decide_items": 2, "domains": [ { "id": 10, "name": "northwind.shop", "cost": 32, "decide": true }, { "id": 14, "name": "northwind-labs.com", "cost": 12.5, "decide": true } ], "more": 0 }, { "month": "2026-02", "cost": 12.5, "items": 1, "unpriced": 0, "decide_cost": 12.5, "decide_items": 1, "domains": [ { "id": 1, "name": "northwind.com", "cost": 12.5, "decide": true } ], "more": 0 } ], "savings": { "unused": [], "total": 0 } }, "protection": [ { "setting": "transfer_lock", "on": 14, "applicable": 16, "off": [ 14, 18 ], "applies": true, "here": [], "reconnect": [], "elsewhere": [ { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/", "ids": [ 14, 18 ] } ], "unknown": 0, "none": 3 }, { "setting": "auto_renew", "on": 0, "applicable": 5, "off": [ 4, 10 ], "applies": true, "here": [], "reconnect": [], "elsewhere": [ { "label": "Cloudflare", "url": "https://dash.cloudflare.com/?to=/:account/domains", "ids": [ 4 ] }, { "label": "Porkbun", "url": "https://porkbun.com/account/domainsSpeedy", "ids": [ 10 ] } ], "unknown": 14, "none": 0 } ], "stale": [], "found_elsewhere": [], "cheaper_registrar": { "groups": [], "set_aside": [] }, "setup": null } ```
--- ### List registrar moves {#get-organizations-organization-domains-registrar-moves}
`GET` `/organizations/{organization}/domains/registrar-moves` **Scope:** `domains.read` · **Changes data:** no The 200 most recent moves of domains to another registrar you have connected, newest first, each with its domain, source and target connection, and current state. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/registrar-moves" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [] } ```
--- ### Get registration requirements for an extension {#get-organizations-organization-domains-registration-fields}
`GET` `/organizations/{organization}/domains/registration-fields` **Scope:** `domains.read` · **Changes data:** no The extra fields a registrar needs to register a domain under `tld` (for example the nexus details some country extensions ask for) and the registration periods it offers. `credential_id` and `tld` are required; `unknown` is true when the registrar could not be asked, in which case it may still refuse. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `credential_id`
body · required | integer | Must match an existing stored value. | | `tld`
body | string | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/registration-fields?credential_id=16" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "fields": [], "unknown": true, "periods": null } ```
--- ### Plan a settings change across domains {#get-organizations-organization-domains-settings-plan}
`GET` `/organizations/{organization}/domains/settings-plan` **Scope:** `domains.read` · **Changes data:** no For each domain in `ids` (up to 500), who would carry out turning `setting` on or off (`value`, default true). Changes nothing; each setting is changed through its own endpoint. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `setting`
body · required | string | | | `value`
body | boolean | | | `ids`
body | array | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/settings-plan?setting=%3Csetting%3E" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "setting": "auto_renew", "value": true, "applies": true, "domains": [ { "id": 1, "name": "northwind.com", "state": "unknown", "route": "elsewhere", "where": { "label": "Namecheap", "url": "https://ap.www.namecheap.com/domains/list/" }, "dnssec": null, "zone_id": null } ] } ```
--- ### Track a domain by name {#post-organizations-organization-domains-track}
`POST` `/organizations/{organization}/domains/track` **Scope:** `domains.write` · **Changes data:** yes — only where API writes are switched on for the organization Adds a domain held at a registrar InfraNest is not connected to; its registrar, dates, status and nameservers are read from the public registry, now and daily. Only `name` is accepted; it counts toward your plan’s domain limit, and a domain already in your organization is refused with 422. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `name`
body · required | string | Must not be greater than 253 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/domains/track" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"northwind-studio.com"}' ``` **Response** · 201 — Created ```json { "id": 21, "organization_id": 1, "name": "northwind-studio", "tld": "com", "registrar": null, "registered_at": null, "expires_at": null, "auto_renew": true, "status": "active", "notes": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": null, "nameservers": null, "transfer_lock": null, "whois_privacy": null, "registrar_contacts": null, "dnssec_entries": null, "has_dnssec": null, "renewal_price": null, "renewal_price_currency": null, "pricing_cached_at": null, "last_details_synced_at": null, "status_message": null, "expected_issuers": null, "renewal_price_source": "api", "purchase_price": null, "purchase_price_currency": null, "registrar_price": null, "registrar_price_currency": null, "registrar_flags": null, "requested_whois_privacy": null, "requested_auto_renew": null, "archived_at": null, "requested_nameservers": null, "missing_since": null, "serves_website": null, "billing_interval": "yearly", "interval_count": 1, "dnssec_state": null, "dnssec_checked_at": null, "dnssec_broken_since": null, "requested_dns_credential_id": null, "requested_dns_template": null, "status_detail": null, "ordered_by_user_id": null, "ns_health": null, "ns_health_checked_at": null, "unused_since": null, "registry_statuses": null, "registry_checked_at": null, "registry_dnssec": null, "full_name": "northwind-studio.com", "archivable": true, "dns_host": null, "management": "tracked", "sync_halt": null } ```
--- ### Check a domain before transferring it in {#get-organizations-organization-domains-transfer-preflight}
`GET` `/organizations/{organization}/domains/transfer-preflight` **Scope:** `domains.read` · **Changes data:** no Checks the usual reasons an inbound transfer fails for the domain given by `name` and `tld`, before you submit it. Optionally pass `credential_id` for the registrar that would receive it; counts toward your plan’s hourly domain-check limit. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `name`
body · required | string | Must not be greater than 100 characters. | | `tld`
body · required | string | Must not be greater than 30 characters. | | `credential_id`
body | integer, nullable | Which of the org's registrars the transfer would go to. Optional: the registry checks below are about the losing side and run without it. Scoped to the org rather than taken on trust — a credential id from another tenant would otherwise make one API call on their key. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/domains/transfer-preflight?name=www&tld=%3Ctld%3E" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "domain": "northwind.com", "registrar": "Namecheap", "expires_at": "2026-02-02", "source": "fixture", "checks": [ { "key": "not_locked", "status": "warn", "detail": [] }, { "key": "sixty_day_rule", "status": "pass", "detail": { "days": 749, "free_on": "2024-02-26", "since": "registration" } } ] } ```
--- # DNS Zones and records at your DNS providers, templates and dynamic IP sources. Record changes are recorded in the zone history and can be undone. **Common tasks** - [List the records in a zone](#get-organizations-organization-dns-zones-dnszone-dns-records) - [Create a record](#post-organizations-organization-dns-zones-dnszone-dns-records) - [Undo a change](#post-organizations-organization-dns-changes-undo) **Only in the app:** Adding or deleting zones and moving a zone to another provider. **How it works in the app:** [help article](https://infranest.io/docs/dns-records). **Scopes:** Read with `dns.read` or `monitoring.read`, change with `dns.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[DNS](#dns) 4 · [DNS changes](#dns-changes) 1 · [DNS templates](#dns-templates) 3 · [DNS zones](#dns-zones) 26 · [Dynamic IPs](#dynamic-ips) 4
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## DNS ### Plan moving records to another server {#get-organizations-organization-dns-address-move}
`GET` `/organizations/{organization}/dns/address-move` **Scope:** `dns.read` · **Changes data:** no Shows what moving A/AAAA records from one server to another would change: each record with its new value, the records left out and why, and the monitors that would follow. Send `from_server_id` or `record_ids`, and optionally `to_server_id`; changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `from_server_id`
query | integer | The server being left. Send this or `record_ids`. | | `record_ids`
query | string | Comma-separated A/AAAA record ids, as the cross-zone search returns them. | | `to_server_id`
query | integer | The server to move to; without it the records are listed with no new values. | | `from_server_id`
body | integer, nullable | This field is required when record_ids is not present. | | `record_ids`
body | string, nullable | This field is required when from_server_id is not present. Must match the regex /^\d+(,\d+)*$/. Must not be greater than 5000 characters. | | `to_server_id`
body | integer, nullable | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns/address-move" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "from": { "server": { "id": 1, "name": "web-01" }, "addresses": [ "198.51.100.10", "2001:db8:1c1e:5a10::/64" ] }, "to": null, "targets": [ { "id": 13, "name": "analytics-01", "ipv4": "192.0.2.80", "ipv6": null, "ipv6_basis": null }, { "id": 3, "name": "api-01", "ipv4": "198.51.100.20", "ipv6": "2001:db8:1c1e:5a20::1", "ipv6_basis": "default" } ], "records": [ { "record_id": 26, "zone_id": 5, "zone_name": "northwind-docs.com", "zone_provider": "cloudflare", "name": "@", "fqdn": "northwind-docs.com", "type": "A", "value": "198.51.100.10", "ttl": 300, "proxied": true, "family": 4, "new_value": null, "excluded": "read_only", "aliases": [ { "zone_id": 5, "record_id": 28, "fqdn": "api.northwind-docs.com" } ] }, { "record_id": 27, "zone_id": 5, "zone_name": "northwind-docs.com", "zone_provider": "cloudflare", "name": "www", "fqdn": "www.northwind-docs.com", "type": "A", "value": "198.51.100.10", "ttl": 300, "proxied": true, "family": 4, "new_value": null, "excluded": "read_only", "aliases": [] } ], "monitors": { "follow": [], "by_name": [] }, "ttl": { "long": 0, "longest": null, "threshold": 3600, "lowered": 300 } } ```
--- ### List DNS host moves {#get-organizations-organization-dns-moves}
`GET` `/organizations/{organization}/dns/moves` **Scope:** `dns.read` · **Changes data:** no The 200 most recent moves of a domain’s DNS to another DNS host, newest first, each with its domain, target connection, current `state` and timestamps. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns/moves" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [] } ```
--- ### Get the DNS overview {#get-organizations-organization-dns-overview}
`GET` `/organizations/{organization}/dns/overview` **Scope:** `dns.read` · **Changes data:** no A summary across all your DNS zones: what needs your attention, grouped by cause, the recent changes, where your DNS is hosted and what is worth tidying up. Changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns/overview" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "summary": { "state": "needs_you", "critical": false, "zones": 13, "needing_you": 2, "fine": 11, "live_match": 12, "live_checked": 12, "records": 88, "hosts": 6, "last_synced_at": "2026-01-15T11:53:00+00:00" }, "needs": [ { "key": "advisor:6:dnssec", "cause": "advisor", "critical": false, "subject": "northwind.de", "zones": [ { "id": 6, "name": "northwind.de" } ], "params": [], "since": null, "due_at": null, "route": "/dns/6", "finding": { "id": "dnssec", "status": "error", "variant": "error" }, "halt": null, "attention_keys": [ "dnssec_broken:9" ] }, { "key": "advisor:3:cf_ssl_mode", "cause": "advisor", "critical": false, "subject": "northwind-app.dev", "zones": [ { "id": 3, "name": "northwind-app.dev" } ], "params": [], "since": null, "due_at": null, "route": "/dns/3", "finding": { "id": "cf_ssl_mode", "status": "error", "value": "flexible" }, "halt": null, "attention_keys": [ "dns_zone_finding:3:cf_ssl_mode" ] } ], "decided": [], "changes": { "from": "2026-01-08T12:00:00+00:00", "entries": [ { "key": "audit:2", "kind": "audit", "action": "dns_record.created", "at": "2026-01-15T07:00:00+00:00", "zones": [ { "id": null, "name": "" } ], "actor": { "type": "user", "name": "Sam Okoye" }, "outside": false, "count": 1, "params": { "name": "@", "type": "" }, "batch": null, "writable": false, "provider": null }, { "key": "audit:8", "kind": "audit", "action": "dns_record.updated", "at": "2026-01-12T11:00:00+00:00", "zones": [ { "id": null, "name": "" } ], "actor": { "type": "user", "name": "Tom Bakker" }, "outside": false, "count": 1, "params": { "name": "@", "type": "" }, "batch": null, "writable": false, "provider": null } ], "truncated": false }, "hosts": [ { "key": "cred:2", "name": "Cloudflare DNS", "provider": "cloudflare", "credential_id": 2, "zones": 5, "connected": true, "halt": null }, { "key": "cred:1", "name": "Hetzner — Production", "provider": "hetzner", "credential_id": 1, "zones": 2, "connected": true, "halt": null } ], "domains_without_zone": 5, "recommendations": [] } ```
--- ### Search records across all zones {#get-organizations-organization-dns-search}
`GET` `/organizations/{organization}/dns/search` **Scope:** `dns.read` · **Changes data:** no Finds DNS records in any of your zones that match the `q` query parameter, by name, value or address, grouped by zone with the reason each one matched. A query shorter than two characters returns nothing with `too_short` set; at most 100 records are returned, with `truncated` set when there were more. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns/search" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "query": "", "too_short": true, "min_length": 2, "total": 0, "truncated": false, "zones": [], "address_record_ids": [] } ```
## DNS changes ### Undo a DNS change {#post-organizations-organization-dns-changes-undo}
`POST` `/organizations/{organization}/dns-changes/undo` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Undoes one recorded change (`change_id`) or every change in a batch (`batch_id`), even across zones, pushing each one to the DNS provider. A change whose record has changed since, or that a dynamic IP keeps up to date, is skipped with its reason; answers 404 when nothing matches. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `change_id`
body | integer | The change to undo. Send this or `batch_id`. | | `batch_id`
body | string | Every change in this batch, undone as one. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/dns-changes/undo" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"change_id":42,"batch_id":"9b2c6a1e-3f7d-4a8e-9c1b-2d3e4f5a6b7c"}' ```
## DNS templates ### List DNS templates {#get-organizations-organization-dns-templates}
`GET` `/organizations/{organization}/dns-templates` **Scope:** `dns.read` · **Changes data:** no The built-in DNS templates followed by your organization’s custom templates, each with its records, plus the TTL values a template can use. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-templates" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "builtin": [ { "id": null, "slug": "google-workspace", "name": "Google Workspace", "description": "MX records, SPF, and DMARC starter for Google Workspace email.", "is_builtin": true, "records": [ { "type": "MX", "name": "@", "value": "aspmx.l.google.com.", "ttl": 3600, "priority": 1 }, { "type": "MX", "name": "@", "value": "alt1.aspmx.l.google.com.", "ttl": 3600, "priority": 5 } ] }, { "id": null, "slug": "microsoft-365", "name": "Microsoft 365", "description": "MX record, SPF, Autodiscover CNAME, and DMARC starter for Microsoft 365. The MX is filled in from the domain.", "is_builtin": true, "records": [ { "type": "MX", "name": "@", "value": "{zone_dashed}.mail.protection.outlook.com.", "ttl": 3600, "priority": 0 }, { "type": "TXT", "name": "@", "value": "v=spf1 include:spf.protection.outlook.com -all", "ttl": 3600 } ] } ], "custom": [ { "id": 1, "organization_id": 1, "name": "Google Workspace mail", "description": "MX, SPF and DKIM for a domain that sends mail through Google Workspace.", "slug": "google-workspace-mail", "is_builtin": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "records": [ { "id": 1, "dns_template_id": 1, "type": "MX", "name": "@", "value": "aspmx.l.google.com", "ttl": 3600, "priority": 1, "proxied": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" }, { "id": 2, "dns_template_id": 1, "type": "MX", "name": "@", "value": "alt1.aspmx.l.google.com", "ttl": 3600, "priority": 5, "proxied": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } ] } ], "ttl_options": [ 1, 30 ] } ```
--- ### Apply a template to a zone {#post-organizations-organization-dns-templates-templatekey-apply-zone}
`POST` `/organizations/{organization}/dns-templates/{templateKey}/apply/{zone}` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates or updates the template’s records in the zone and pushes each change to the DNS provider; with `overwrite=false`, records that already exist with the same type and name are left alone. `dry_run=1` returns the changes without making any. A CAA record that would block a certificate’s renewal is refused with 409 unless `confirm_caa_impact` is sent. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `templateKey`
path · required | string | The templateKey. | | `zone_id`
path · required | integer | The zone's ID. | | `dry_run`
query | boolean | Answer with the changes this would make, and make none. | | `overwrite`
body | boolean, nullable | | | `confirm_caa_impact`
body | boolean, nullable | Apply even though a template CAA record blocks a certificate's renewing CA — after a 409. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/dns-templates/1/apply/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"overwrite":true,"confirm_caa_impact":true}' ``` **Response** · 200 ```json { "added": 1, "overwritten": 0, "failed": 0, "error": null, "skipped": 2, "skipped_reason": "The A record \"@\" needs a value the template cannot know ({server_ipv4}). Add it yourself. The AAAA record \"@\" needs a value the template cannot know ({server_ipv6}). Add it yourself.", "message": "Applied Static Site: 1 records added, 0 updated. 2 skipped: The A record \"@\" needs a value the template cannot know ({server_ipv4}). Add it yourself. The AAAA record \"@\" needs a value the template cannot know ({server_ipv6}). Add it yourself." } ```
--- ### Preview applying a template {#get-organizations-organization-dns-templates-templatekey-preview-zone}
`GET` `/organizations/{organization}/dns-templates/{templateKey}/preview/{zone}` **Scope:** `dns.read` · **Changes data:** no Shows each of the template’s records for the zone with its status (such as `add`, `overwrite` or `unchanged`), the existing value it would replace and the TTL the zone will actually get. `templateKey` is a built-in template’s slug or a custom template’s id; changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `templateKey`
path · required | string | The templateKey. | | `zone_id`
path · required | integer | The zone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-templates/1/preview/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "type": "A", "name": "@", "value": "{server_ipv4}", "ttl": 3600, "priority": null, "proxied": false, "unfilled": "{server_ipv4}", "status": "skipped", "existing_value": null, "reason": "The A record \"@\" needs a value the template cannot know ({server_ipv4}). Add it yourself." }, { "type": "AAAA", "name": "@", "value": "{server_ipv6}", "ttl": 3600, "priority": null, "proxied": false, "unfilled": "{server_ipv6}", "status": "skipped", "existing_value": null, "reason": "The AAAA record \"@\" needs a value the template cannot know ({server_ipv6}). Add it yourself." } ] ```
## DNS zones ### List DNS zones {#get-organizations-organization-dns-zones}
`GET` `/organizations/{organization}/dns-zones` **Scope:** `dns.read` · **Changes data:** no Your DNS zones, paginated with `page` and `per_page` (up to 200), each with its provider, record count, tags and delegation state. Filter with `search`, `credential`, `tag` or `status` (`active`, `stale`, `drift` for records changed at the provider, or `security`) and sort with `sort` (`zone`, `provider`, `records`, `synced`) and `dir`; `meta.counts` holds the totals. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 11, "organization_id": 1, "domain_id": 20, "name": "northwind-api.dev", "provider": "openprovider", "provider_zone_id": "zone-210", "status": "pending", "last_synced_at": "2026-01-15T10:24:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 7, "ns_live": [ "ns1.openprovider.nl", "ns2.openprovider.be" ], "ns_checked_at": "2026-01-15T11:53:00.000000Z", "security_status": "info", "ns_mismatch_since": null, "provider_modified_at": null, "price_monthly": "0.00", "price_source": "manual", "price_currency": "EUR", "price_amount": "0.0000", "billing_interval": "monthly", "interval_count": 1, "live_mismatch_count": 0, "live_checked_at": "2026-01-15T11:53:00.000000Z", "live_mismatch_since": null, "dnssec_pending": null, "dnssec_pending_since": null, "provider_settings": null, "provider_settings_checked_at": null, "default_ttl": null, "registrar_name": null, "registrar_checked_at": null, "records_count": 7, "custom_fields": [], "delegation": { "state": "matches", "reason": null, "live": [ "ns1.openprovider.nl", "ns2.openprovider.be" ], "dnssec": null, "belongs_to": "openprovider", "checked_at": "2026-01-15T11:53:00.000000Z" }, "can_sign": false, "sync_halt": null, "provider_credential": { "id": 7, "organization_id": 1, "provider": "openprovider", "label": "Openprovider — Reseller", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] }, { "id": 3, "organization_id": 1, "domain_id": 3, "name": "northwind-app.dev", "provider": "cloudflare", "provider_zone_id": "zone-202", "status": "active", "last_synced_at": "2026-01-15T11:48:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 2, "ns_live": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "ns_checked_at": "2026-01-15T11:53:00.000000Z", "security_status": "error", "ns_mismatch_since": null, "provider_modified_at": null, "price_monthly": "4.00", "price_source": "manual", "price_currency": "EUR", "price_amount": "4.0000", "billing_interval": "monthly", "interval_count": 1, "live_mismatch_count": 0, "live_checked_at": "2026-01-15T11:53:00.000000Z", "live_mismatch_since": null, "dnssec_pending": null, "dnssec_pending_since": null, "provider_settings": { "ssl": "flexible", "min_tls_version": "1.0", "security_header": { "strict_transport_security": { "enabled": false } }, "always_use_https": "off", "development_mode": "on", "development_mode_expires_at": "2026-01-15T14:00:00.000000Z" }, "provider_settings_checked_at": "2026-01-15T11:40:00.000000Z", "default_ttl": null, "registrar_name": null, "registrar_checked_at": null, "records_count": 8, "custom_fields": [], "delegation": { "state": "matches", "reason": null, "live": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "dnssec": null, "belongs_to": "cloudflare", "checked_at": "2026-01-15T11:53:00.000000Z" }, "can_sign": true, "sync_halt": null, "provider_credential": { "id": 2, "organization_id": 1, "provider": "cloudflare", "label": "Cloudflare DNS", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] } ], "first_page_url": "http://localhost/api/organizations/1/dns-zones?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/dns-zones?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/dns-zones?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/dns-zones", "per_page": 25, "prev_page_url": null, "to": 13, "total": 13, "meta": { "locked_ids": [], "counts": { "total": 13, "active": 12, "stale": 0, "drift": 0, "security": 4 }, "integrations": [ { "id": 2, "provider": "cloudflare", "label": "Cloudflare DNS" }, { "id": 4, "provider": "dynadot", "label": "Dynadot Registrar" } ], "tags": [], "custom_fields": [] } } ```
--- ### Get a DNS zone {#get-organizations-organization-dns-zones-dnszone}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}` **Scope:** `dns.read` · **Changes data:** no One zone with all its records, its provider connection, whether record changes can be pushed to the provider, its delegation and DNSSEC details, and findings such as records pointing at released addresses. Opening it may queue fresh nameserver and live-DNS checks in the background. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "domain_id": 1, "name": "northwind.com", "provider": "cloudflare", "provider_zone_id": "zone-200", "status": "active", "last_synced_at": "2026-01-15T11:48:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 2, "ns_live": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "ns_checked_at": "2026-01-15T11:53:00.000000Z", "security_status": "warn", "ns_mismatch_since": null, "provider_modified_at": null, "price_monthly": "4.00", "price_source": "manual", "price_currency": "EUR", "price_amount": "4.0000", "billing_interval": "monthly", "interval_count": 1, "live_mismatch_count": 0, "live_checked_at": "2026-01-15T11:53:00.000000Z", "live_mismatch_since": null, "dnssec_pending": null, "dnssec_pending_since": null, "provider_settings": { "ssl": "strict", "min_tls_version": "1.2", "security_header": { "strict_transport_security": { "enabled": true } }, "always_use_https": "on", "development_mode": "off" }, "provider_settings_checked_at": "2026-01-15T11:40:00.000000Z", "default_ttl": null, "registrar_name": null, "registrar_checked_at": null, "records_count": 10, "sync_halt": null, "records": [ { "id": 1, "dns_zone_id": 1, "type": "A", "name": "@", "value": "198.51.100.10", "ttl": 300, "priority": null, "provider_record_id": "rec-200-0", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": true, "live_values": null, "live_checked_at": null, "value_is_placeholder": false, "live_mismatch": null }, { "id": 2, "dns_zone_id": 1, "type": "A", "name": "www", "value": "198.51.100.10", "ttl": 300, "priority": null, "provider_record_id": "rec-200-1", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": true, "live_values": null, "live_checked_at": null, "value_is_placeholder": false, "live_mismatch": null } ], "provider_credential": { "id": 2, "organization_id": 1, "provider": "cloudflare", "label": "Cloudflare DNS", "verified_at": "2025-10-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "enabled": true, "enabled_capabilities": null, "disabled_capabilities": null, "error_email_enabled": true, "disabled_operations": null, "alerts_muted_until": null, "spending_capacity": null, "spending_checked_at": null, "scope_check": null, "scope_checked_at": null, "removing_at": null, "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "account_identity": null, "refused_at": null, "refusal_checked_at": null }, "tags": [], "record_types": [ "A", "AAAA" ], "ttl_options": [ 1, 120 ], "can_push_records": false, "delegation": { "state": "matches", "reason": null, "live": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "dnssec": false, "belongs_to": "cloudflare", "checked_at": "2026-01-15T11:53:00.000000Z" }, "proxiable_record_types": [], "can_sign": true, "signing_reason": null, "dnssec_plan": { "shape": "sign_then_paste", "dns": { "provider": "cloudflare", "can_sign": true, "exposes_key": true, "reason": null }, "registrar": { "provider": "Namecheap", "known": true, "can_publish": false, "help_url": null, "fields": [ "key_tag", "flags" ] } }, "managed_hostnames": [], "posture": [ { "id": "cf_ssl_mode", "status": "pass", "value": "strict" }, { "id": "cf_min_tls", "status": "pass", "value": "1.2" } ], "dangling_addresses": [], "unclaimed_cnames": [], "live": { "mismatches": 0, "checked_at": "2026-01-15T11:53:00.000000Z", "mismatch_since": null } } ```
--- ### Get a zone’s advisor findings {#get-organizations-organization-dns-zones-dnszone-advisor}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/advisor` **Scope:** `dns.read` · **Changes data:** no Findings about the zone’s stored records, such as missing or weak email protection, with the snapshot those findings were judged from. Changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/advisor" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "snapshot": { "spf": [ "v=spf1 include:_spf.northwind.com ~all" ], "dmarc": [ "v=DMARC1; p=quarantine; rua=mailto:dmarc@northwind.example" ], "dkim_selectors": [], "mx": [ "mx.northwind.com" ], "null_mx": false, "mail_provider": null, "mx_cname_targets": [], "apex_a": [ "198.51.100.10" ], "apex_aaaa": [], "apex_cname": [], "caa": [ "0 issue \"letsencrypt.org\"" ], "caa_required": { "certificates": [ { "id": 1, "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "ids": [ "letsencrypt.org" ], "wildcard": false, "renews_itself": true, "provider": false }, { "id": 13, "subject": "cdn.northwind.com", "issuer": "Let's Encrypt (R3)", "ids": [ "letsencrypt.org" ], "wildcard": false, "renews_itself": true, "provider": false } ], "expected": [], "fronts": [ { "key": "cloudflare", "label": "Cloudflare", "ids": [ "letsencrypt.org", "pki.goog" ], "publishes_own": true } ] }, "wildcard_a": [], "wildcard_aaaa": [], "cf_exposed_ips": [ "198.51.100.10" ], "mta_sts": [], "bimi": [], "apex_ns": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "ns_matches_provider": true, "mail_subdomains_missing_spf": [] }, "findings": [ { "id": "ns", "status": "pass", "variant": "pass", "values": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ] }, { "id": "dnssec", "status": "pass", "variant": "pass" } ] } ```
--- ### Preview CAA protection for a zone {#get-organizations-organization-dns-zones-dnszone-caa-protection}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/caa-protection` **Scope:** `dns.read` · **Changes data:** no Shows the CAA records that would be added, each of the zone’s certificates and whether it stays renewable, or why a change is refused. `live=1` also checks each record against public DNS. Changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `live`
query | boolean | Also check each record to add against public DNS. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/caa-protection" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "zone_id": 1, "zone": "northwind.com", "route": "here", "where": { "label": "Cloudflare", "url": null }, "records": [ { "key": "kept:7", "action": "none", "type": "CAA", "name": "@", "value": "0 issue \"letsencrypt.org\"", "current": null, "record_id": null, "reason": "caa_kept", "params": [] }, { "key": "issue:globalsign.com", "action": "create", "type": "CAA", "name": "@", "value": "0 issue \"globalsign.com\"", "current": null, "record_id": null, "reason": "caa_cert", "params": { "subject": "shop.northwind.com" } } ], "refusals": [], "certificates": [ { "id": 1, "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "ca": "letsencrypt.org", "wildcard": false, "state": "stays_renewable" }, { "id": 13, "subject": "cdn.northwind.com", "issuer": "Let's Encrypt (R3)", "ca": "letsencrypt.org", "wildcard": false, "state": "stays_renewable" } ], "fronts": [ { "label": "Cloudflare", "ids": [ "letsencrypt.org", "pki.goog" ], "publishes_own": true } ] } ```
--- ### List a zone’s recorded changes {#get-organizations-organization-dns-zones-dnszone-changes}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/changes` **Scope:** `dns.read` · **Changes data:** no The zone’s record changes from its last 50 operations, newest first, grouped by the operation that made each batch, each with whether it can still be undone or restored to. Also returns a merged timeline and how many days of changes your plan keeps. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/changes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "batches": [], "timeline": [ { "kind": "audit", "entry": { "id": 21, "action": "dns_zone.exported", "meta": { "zone": "northwind.com", "format": "bind", "records": 10 }, "user_id": 1, "created_at": "2026-01-15T12:00:00.000000Z" } } ], "tracked_since": null, "writable": false, "retention_days": 90, "dynamic_ip_retention_days": 7 } ```
--- ### List records in a zone {#get-organizations-organization-dns-zones-dnszone-dns-records}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records` **Scope:** `dns.read` · **Changes data:** no Every record in the zone, ordered by type and name, with the dynamic IP source that keeps it up to date, if any. Filter by record type with `type`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "dns_zone_id": 1, "type": "A", "name": "@", "value": "198.51.100.10", "ttl": 300, "priority": null, "provider_record_id": "rec-200-0", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": true, "live_values": null, "live_checked_at": null, "value_is_placeholder": false, "live_mismatch": null, "dynamic_ip_target": null }, { "id": 88, "dns_zone_id": 1, "type": "A", "name": "archive", "value": "198.51.100.11", "ttl": 300, "priority": null, "provider_record_id": "demo-certificate-archive", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": false, "live_values": [ "198.51.100.11" ], "live_checked_at": "2026-01-15T11:53:00.000000Z", "value_is_placeholder": false, "live_mismatch": false, "dynamic_ip_target": null } ] ```
--- ### Create a record {#post-organizations-organization-dns-zones-dnszone-dns-records}
`POST` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates a record in the zone and pushes it to the DNS provider; `dry_run=1` returns the change without making it. Refused with 422 when the zone is read-only or an A/AAAA value is an address your fleet released, and with 409 when an apex CAA record would block a certificate’s renewal, unless `confirm_caa_impact` is sent. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `dry_run`
query | boolean | Answer with the change this would make, and make none. | | `type`
body · required | string | One of: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SOA`, `SRV`, `CAA`, `PTR`, `ALIAS`, `CERT`, …. | | `name`
body | string, nullable | Must not be greater than 255 characters. | | `value`
body · required | string | Must not be greater than 65535 characters. | | `ttl`
body | integer | Must be at least 60. Must not be greater than 86400. | | `priority`
body | integer, nullable | Must be at least 0. Must not be greater than 65535. | | `proxied`
body | boolean | | | `comment`
body | string, nullable | Must not be greater than 500 characters. | | `confirm_caa_impact`
body | boolean | Save an apex CAA record even though it blocks the CA a certificate renews from — send after a 409 `caa_blocks_renewal`. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"type":"A","name":"api","value":"192.0.2.20","ttl":3600}' ``` **Response** · 201 — Created ```json { "id": 91, "dns_zone_id": 14, "type": "A", "name": "api", "value": "192.0.2.20", "ttl": 3600, "priority": null, "provider_record_id": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": false, "live_values": null, "live_checked_at": null, "value_is_placeholder": false, "live_mismatch": null } ``` **409** — The CAA change would block a certificate's renewing CA ```json { "message": "This CAA change would block the certificate authority that renews status.example.com. Their next renewal would fail.", "error_type": "caa_blocks_renewal", "certificates": [ { "id": 12, "subject": "status.example.com", "ca": "letsencrypt.org" } ] } ```
--- ### Get a record {#get-organizations-organization-dns-zones-dnszone-dns-records-record}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records/{record}` **Scope:** `dns.read` · **Changes data:** no One record in the zone. Answers 404 when the record belongs to another zone. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `record_id`
path · required | integer | The record's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "dns_zone_id": 1, "type": "A", "name": "@", "value": "198.51.100.10", "ttl": 300, "priority": null, "provider_record_id": "rec-200-0", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": true, "live_values": null, "live_checked_at": null, "value_is_placeholder": false, "live_mismatch": null } ```
--- ### Update a record {#put-organizations-organization-dns-zones-dnszone-dns-records-record}
`PUT` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records/{record}` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes the record and pushes the change to the DNS provider; `dry_run=1` returns the change without making it. Refused with 422 when the zone is read-only or a new A/AAAA value is an address your fleet released, and with 409 when an apex CAA change would block a certificate’s renewal, unless `confirm_caa_impact` is sent. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `record_id`
path · required | integer | The record's ID. | | `dry_run`
query | boolean | Answer with the change this would make, and make none. | | `type`
body | string | One of: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SOA`, `SRV`, `CAA`, `PTR`, `ALIAS`, `CERT`, …. | | `name`
body | string, nullable | Must not be greater than 255 characters. | | `value`
body | string | Must not be greater than 65535 characters. | | `ttl`
body | integer | Must be at least 60. Must not be greater than 86400. | | `priority`
body | integer, nullable | Must be at least 0. Must not be greater than 65535. | | `proxied`
body | boolean | | | `comment`
body | string, nullable | Must not be greater than 500 characters. | | `confirm_caa_impact`
body | boolean | Save an apex CAA change even though it blocks the CA a certificate renews from — send after a 409 `caa_blocks_renewal`. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"type":"A","name":"www","value":"192.0.2.11","ttl":3600}' ``` **Response** · 200 ```json { "id": 89, "dns_zone_id": 14, "type": "A", "name": "www", "value": "192.0.2.11", "ttl": 3600, "priority": null, "provider_record_id": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "comment": null, "proxied": false, "live_values": null, "live_checked_at": null, "value_is_placeholder": false, "live_mismatch": null } ``` **409** — The CAA change would block a certificate's renewing CA ```json { "message": "This CAA change would block the certificate authority that renews status.example.com. Their next renewal would fail.", "error_type": "caa_blocks_renewal", "certificates": [ { "id": 12, "subject": "status.example.com", "ca": "letsencrypt.org" } ] } ```
--- ### Delete a record {#delete-organizations-organization-dns-zones-dnszone-dns-records-record}
`DELETE` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records/{record}` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Deletes the record at the DNS provider first, then in InfraNest; if the provider refuses, the record is kept and 502 is returned. Supports `dry_run=1`, and `confirm_caa_impact=1` after a 409 for an apex CAA record whose removal would block a certificate’s renewal. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `record_id`
path · required | integer | The record's ID. | | `confirm_caa_impact`
query | boolean | Delete an apex CAA record even though that blocks the CA a certificate renews from — send after a 409 `caa_blocks_renewal`. | | `dry_run`
query | boolean | Answer with the change this would make, and make none. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content **409** — The CAA change would block a certificate's renewing CA ```json { "message": "This CAA change would block the certificate authority that renews status.example.com. Their next renewal would fail.", "error_type": "caa_blocks_renewal", "certificates": [ { "id": 12, "subject": "status.example.com", "ca": "letsencrypt.org" } ] } ```
--- ### Set a record’s label {#put-organizations-organization-dns-zones-dnszone-dns-records-record-label}
`PUT` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records/{record}/label` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets or clears a note on what the record is for (`label`, up to 120 characters; empty or null clears it). Kept in InfraNest only and never written to the DNS provider; a labelled record is never proposed as a leftover. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `record_id`
path · required | integer | The record's ID. | | `label`
body | string, nullable | What the record is for, up to 120 characters. Empty or null clears it. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records/1/label" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"label":"Main website"}' ``` **Response** · 200 ```json { "label": "Main website", "points_at": { "kind": "custom", "label": "Main website", "url": null, "flagged": false, "since": null, "provider": null, "note": null, "ours": false } } ```
--- ### Check a record’s propagation {#get-organizations-organization-dns-zones-dnszone-dns-records-record-propagation}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/dns-records/{record}/propagation` **Scope:** `dns.read` · **Changes data:** no Asks public DNS resolvers for the record and reports, per resolver, whether its stored value is live yet; `tier=fast` uses only the quicker resolvers. With `landing=1` it instead reports whether the record’s latest change (or the one in `change_id`) has taken effect, and `landing` is null when nothing changed recently. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `record_id`
path · required | integer | The record's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/dns-records/1/propagation" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Get a zone’s DNSSEC status {#get-organizations-organization-dns-zones-dnszone-dnssec}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/dnssec` **Scope:** `dns.read` · **Changes data:** no Reads the zone’s DNSSEC state from its DNS provider, including the DS details to add at your registrar when signing is waiting on that step. `status` is null when the provider cannot sign the zone or its state could not be read. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/dnssec" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "status": null } ```
--- ### Preview email protection for a zone {#get-organizations-organization-dns-zones-dnszone-email-protection}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/email-protection` **Scope:** `dns.read` · **Changes data:** no Shows each email record (such as SPF and DMARC) as it is and as it would be, or why a change is refused. `mode` is required: `protect` for a domain that receives mail, `no_mail` for one that sends none; `live=1` also checks against public DNS. Changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `mode`
query · required | string | `protect` (a domain that receives mail) or `no_mail`. | | `provider`
query | string | A mail provider key, when MX names nobody with an SPF include. | | `live`
query | boolean | Also check each record against public DNS, and look for the DKIM key there. | | `dkim_target`
query | string | Microsoft 365: the selector1 CNAME target as Defender shows it. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/email-protection?mode=protect" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "zone_id": 1, "zone": "northwind.com", "mode": "no_mail", "route": "here", "where": { "label": "Cloudflare", "url": null }, "provider": null, "providers": [], "records": [], "refusals": [ { "key": "mode", "reason": "receives_mail", "params": [] } ], "lookups": null, "dkim": null } ```
--- ### Get a zone’s audit history {#get-organizations-organization-dns-zones-dnszone-history}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/history` **Scope:** `dns.read` · **Changes data:** no The 50 most recent audit-log entries for the zone and its records, deleted records included, newest first. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/history" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 21, "action": "dns_zone.exported", "meta": { "zone": "northwind.com", "format": "bind", "records": 10 }, "user_id": 1, "created_at": "2026-01-15T12:00:00.000000Z" } ] ```
--- ### List leftover records in a zone {#get-organizations-organization-dns-zones-dnszone-leftovers}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/leftovers` **Scope:** `dns.read` · **Changes data:** no Records the zone appears to have outgrown, such as old verification codes, addresses that stopped answering and duplicates, each with its reason and evidence. `writable` is false when InfraNest cannot change records in this zone. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/leftovers" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "items": [], "writable": false, "unanswered_days": 90 } ```
--- ### Compare stored records with live DNS {#get-organizations-organization-dns-zones-dnszone-live-mismatch}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/live-mismatch` **Scope:** `dns.read` · **Changes data:** no Records whose stored value differs from what public resolvers last answered, each with both values, the likely cause and whether taking the live value is offered. Also returns the zone’s delegation state; changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/live-mismatch" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "zone": "northwind.com", "checked_at": "2026-01-15T11:53:00.000000Z", "delegation": { "state": "matches", "reason": null, "live": [ "nina.ns.cloudflare.com", "rick.ns.cloudflare.com" ], "dnssec": null, "belongs_to": "cloudflare", "checked_at": "2026-01-15T11:53:00.000000Z" }, "can_push": false, "records": [] } ```
--- ### List outside changes to a zone {#get-organizations-organization-dns-zones-dnszone-outside-changes}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/outside-changes` **Scope:** `dns.read` · **Changes data:** no Records changed at the DNS provider outside InfraNest that nobody has decided on yet, newest first, each with what the record was and is now. `writable` says whether your earlier value can be put back for this zone. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/outside-changes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "changes": [], "writable": false } ```
--- ### Preview what a record value would point at {#get-organizations-organization-dns-zones-dnszone-points-at}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/points-at` **Scope:** `dns.read` · **Changes data:** no Says what a record of the given `type` with the given `value` (both required) would point at, before you save it. `points_at` is null when nothing is recognised. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `type`
query · required | string | The record type. | | `value`
query · required | string | The value being typed. | | `type`
body · required | string | One of: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SOA`, `SRV`, `CAA`, `PTR`, `ALIAS`, `CERT`, …. | | `value`
body · required | string | Must not be greater than 255 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/points-at?type=A&value=203.0.113.7" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "points_at": { "kind": "server", "label": "web-01", "url": "/servers/1", "flagged": false, "since": null, "provider": "Hetzner", "note": null, "ours": false } } ```
--- ### Get what a zone’s records point at {#get-organizations-organization-dns-zones-dnszone-record-context}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/record-context` **Scope:** `dns.read` · **Changes data:** no For each record in the zone, keyed by record id: what it points at (one of your servers, a recognised service, a connection or a released address) and what uses it, such as monitors, certificates and status pages. Records with neither are left out; `long_ttl` is the TTL above which InfraNest calls a TTL long. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/record-context" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "records": { "1": { "points_at": { "kind": "server", "label": "web-01", "url": "/servers/1", "flagged": false, "since": null, "provider": "Hetzner", "note": { "key": "proxied", "params": { "provider": "Cloudflare" } }, "ours": false }, "used_by": [ { "type": "monitor", "id": 1, "label": "Marketing site", "sublabel": "https://northwind.com", "url": "/monitoring/1", "meta": { "type": "http", "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z" } }, { "type": "monitor", "id": 4, "label": "Checkout flow", "sublabel": "https://northwind.com/checkout", "url": "/monitoring/4", "meta": { "type": "http", "last_status": "down", "last_checked_at": "2026-01-15T11:58:00.000000Z" } } ] }, "2": { "points_at": { "kind": "server", "label": "web-01", "url": "/servers/1", "flagged": false, "since": null, "provider": "Hetzner", "note": { "key": "proxied", "params": { "provider": "Cloudflare" } }, "ours": false }, "used_by": [ { "type": "monitor", "id": 8, "label": "www → apex redirect", "sublabel": "https://www.northwind.com", "url": "/monitoring/8", "meta": { "type": "redirect", "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z" } }, { "type": "certificate", "id": 1, "label": "northwind.com", "sublabel": "2026-03-30", "url": "/certificates/1", "meta": [] } ] } }, "long_ttl": 3600 } ```
--- ### Check whether an address left your fleet {#get-organizations-organization-dns-zones-dnszone-released-address}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/released-address` **Scope:** `dns.read` · **Changes data:** no Says whether the `address` query parameter (required) belongs to one of your servers, or is an address one of your servers held and released. A record cannot be pointed at a released address. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `address`
query · required | string | The A/AAAA value being typed. | | `address`
body · required | string | Must not be greater than 64 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/released-address?address=203.0.113.7" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "server": { "id": 1, "name": "web-01" }, "released": null } ```
--- ### Preview restoring a zone to an earlier point {#get-organizations-organization-dns-zones-dnszone-restore-preview}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/restore-preview` **Scope:** `dns.read` · **Changes data:** no Shows what putting every record back the way it was just after the batch `batch_id` (required) would change. Changes nothing; answers 404 when that batch is not in the zone’s history. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | | `batch_id`
query · required | string | The batch to restore to. | | `batch_id`
body · required | string | Must be a valid UUID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/restore-preview?batch_id=9b2c6a1e-3f7d-4a8e-9c1b-2d3e4f5a6b7c" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Check a zone’s live DNS security {#get-organizations-organization-dns-zones-dnszone-security-check}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/security-check` **Scope:** `dns.read` · **Changes data:** no Resolves the zone’s SPF, DMARC, DKIM, MX, CAA, MTA-STS, BIMI, NS, address and DNSSEC answers from public DNS right now, with findings judged by the same rules as the stored-record advisor. Reflects what the internet answers, not what InfraNest stores. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/security-check" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "zone": "northwind.com", "checked_at": "2026-01-15T12:00:00.000000Z", "dnssec": { "state": "unknown", "ds": [], "validates": null }, "spf": [], "dmarc": [], "dkim": [], "mx": [], "a": [], "aaaa": [], "caa": [], "wildcard_a": [], "wildcard_aaaa": [], "mta_sts": [], "bimi": [], "ns": [], "ns_matches_provider": null, "snapshot": { "spf": [], "dmarc": [], "dkim_selectors": [], "mx": [], "null_mx": false, "mail_provider": null, "mx_cname_targets": [], "apex_a": [], "apex_aaaa": [], "apex_cname": [], "caa": [], "wildcard_a": [], "wildcard_aaaa": [], "cf_exposed_ips": [], "mta_sts": [], "bimi": [], "apex_ns": [], "ns_matches_provider": null, "mail_subdomains_missing_spf": [] }, "findings": [ { "id": "dnssec", "status": "pass", "variant": "pass" }, { "id": "no_mail", "status": "info", "variant": "info", "fix": { "type": "no_mail" } } ] } ```
--- ### List server addresses for records {#get-organizations-organization-dns-zones-dnszone-server-addresses}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/server-addresses` **Scope:** `dns.read` · **Changes data:** no Your servers that have an address, each with the IPv4 and IPv6 address an A or AAAA record should hold to reach it. Where a server holds a whole IPv6 block, `basis` says how the address was chosen. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/server-addresses" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": 13, "name": "analytics-01", "status": "stopped", "ipv4": { "address": "192.0.2.80", "basis": "exact" }, "ipv6": null }, { "id": 3, "name": "api-01", "status": "running", "ipv4": { "address": "198.51.100.20", "basis": "exact" }, "ipv6": { "address": "2001:db8:1c1e:5a20::1", "basis": "default", "block": "2001:db8:1c1e:5a20::/64" } } ] } ```
--- ### Sync a zone from its provider {#post-organizations-organization-dns-zones-dnszone-sync}
`POST` `/organizations/{organization}/dns-zones/{dnsZone}/sync` **Scope:** `dns.write` · **Changes data:** yes — only where API writes are switched on for the organization Queues a fresh import of the zone’s records from its DNS provider and answers with `queued: true`. Refused with 422 for a zone with no linked provider connection; manual syncs are limited per hour by your plan. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/dns-zones/1/sync" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Download a zone as a BIND file {#get-organizations-organization-dns-zones-dnszone-zone-file}
`GET` `/organizations/{organization}/dns-zones/{dnsZone}/zone-file` **Scope:** `dns.read` · **Changes data:** no Returns the zone’s records as a BIND master file, in plain text, as an attachment named after the zone. The export is recorded in your audit log. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dnsZone_id`
path · required | integer | The dnsZone's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dns-zones/1/zone-file" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 — The zone in BIND format. (`text/plain`)
## Dynamic IPs ### List dynamic IP sources {#get-organizations-organization-dynamic-ips}
`GET` `/organizations/{organization}/dynamic-ips` **Scope:** `monitoring.read` · **Changes data:** no Your dynamic IP sources (connections whose address changes, such as a home or office line), ordered by label, each with its tags and the DNS records and firewalls it keeps up to date. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dynamic-ips" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "label": "Office — Amsterdam", "current_ipv4": null, "current_ipv6": null, "ipv4_updated_at": null, "ipv6_updated_at": null, "last_used_at": null, "created_by": 1, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "observed_ipv4": null, "observed_ipv6": null, "alert_on_change": true, "firewall_strict": false, "firewall_fail_closed": false, "country_code": null, "city": null, "asn": null, "asn_org": null, "is_hosting": false, "silent_notified_at": null, "public_id": null, "last_attempt_at": null, "last_attempt_result": null, "last_attempt_ip": null, "last_attempt_agent": null, "agent_server_id": null, "targets_count": 0, "tags": [], "targets": [] } ] ```
--- ### Get a dynamic IP source {#get-organizations-organization-dynamic-ips-dynamicip}
`GET` `/organizations/{organization}/dynamic-ips/{dynamicIp}` **Scope:** `monitoring.read` · **Changes data:** no One source with its targets, its 20 most recent address events and the server agent it follows, if any. The update token is never included; `can_reveal` says whether it can be shown again. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dynamicIp_id`
path · required | integer | The dynamicIp's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dynamic-ips/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "label": "Office — Amsterdam", "current_ipv4": null, "current_ipv6": null, "ipv4_updated_at": null, "ipv6_updated_at": null, "last_used_at": null, "created_by": 1, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "observed_ipv4": null, "observed_ipv6": null, "alert_on_change": true, "firewall_strict": false, "firewall_fail_closed": false, "country_code": null, "city": null, "asn": null, "asn_org": null, "is_hosting": false, "silent_notified_at": null, "public_id": null, "last_attempt_at": null, "last_attempt_result": null, "last_attempt_ip": null, "last_attempt_agent": null, "agent_server_id": null, "targets_count": 0, "targets": [], "events": [], "agent_server": null, "can_reveal": false } ```
--- ### Get a dynamic IP source’s history {#get-organizations-organization-dynamic-ips-dynamicip-history}
`GET` `/organizations/{organization}/dynamic-ips/{dynamicIp}/history` **Scope:** `monitoring.read` · **Changes data:** no Address changes, what each one updated or failed to update, and changes to the source’s settings, newest first. Drawn from your audit log, so it ages out with your plan’s audit retention. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `dynamicIp_id`
path · required | integer | The dynamicIp's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dynamic-ips/1/history" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "entries": [], "truncated": false } ```
--- ### List servers a dynamic IP can follow {#get-organizations-organization-dynamic-ips-agent-servers}
`GET` `/organizations/{organization}/dynamic-ips/agent-servers` **Scope:** `monitoring.read` · **Changes data:** no Your servers with an InfraNest agent, which a dynamic IP source can take its address from, most recent agent check-in first. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/dynamic-ips/agent-servers" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 3, "name": "api-01", "agent_last_seen_at": "2026-01-15T11:57:00+00:00" } ] ```
--- # Certificates TLS certificates InfraNest tracks or issues, and when they expire. **Common tasks** - [List certificates and when they expire](#get-organizations-organization-certificates) - [See which hosts have no certificate](#get-organizations-organization-certificates-coverage) **Only in the app:** Ordering and renewing certificates. **How it works in the app:** [help article](https://infranest.io/docs/ssl-certificates). **Scopes:** Read with `domains.read` or `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Certificates](#certificates) 15
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Certificates ### List certificates {#get-organizations-organization-certificates}
`GET` `/organizations/{organization}/certificates` **Scope:** `monitoring.read` · **Changes data:** no Every TLS certificate in the organization, expired and expiring first, each with its health, findings and who renews it. Narrow it with `domain_id` or `monitoring_check_id`; the list is not paginated. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 10, "organization_id": 1, "source": "manual", "monitoring_check_id": null, "domain_id": null, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "61e58004edb797c08c1261beb441b56e31cc903bde63441cd503dca9aa79f099", "subject": "vpn.northwind.example", "issuer": "vpn.northwind.example", "serial": "EAABC774", "sans": [ "vpn.northwind.example" ], "not_before": "2025-01-03T12:00:00.000000Z", "not_after": "2026-01-03T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "rsa", "key_bits": 4096, "sig_algo": "SHA256-RSA", "trust": "self_signed", "validation": "OV", "is_wildcard": false, "is_multi_san": false, "managed": false, "renewal_source": null, "classification_override": null, "status": "expired", "superseded_by_id": null, "dismissed_at": null, "deployment_locations": [], "owner": null, "notes": null, "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": null, "chain_status": null, "sources": [ { "source": "manual", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": null, "caissuer_url": null, "no_auto_merge": false, "price_yearly": "0.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "internal", "renewal_type_basis": "guess", "served_status": null, "served_checked_at": null, "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null, "custom_fields": [], "renewer": null, "findings": [ { "tier": "problem", "code": "expired", "severity": "critical", "subject": { "type": "certificate", "id": 10, "name": "vpn.northwind.example" }, "certificate_ids": [ 10 ], "addresses": [], "attention_key": "certificate:10", "due_at": "2026-01-03T12:00:00+00:00", "evidence": { "expires_at": "2026-01-03T12:00:00+00:00", "days_left": -12, "renews_itself": false, "renewal_type": "internal", "renewal_type_basis": "guess", "renewal_source": null, "issuer": "vpn.northwind.example", "served_status": null, "role": null, "renewer": null }, "copy": { "title": { "key": "certificates.rules.expired.title", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } }, "detail": { "key": "certificates.rules.expired.detail", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } }, "short": { "key": "certificates.rules.expired.short", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } } }, "fix": { "kind": "route", "label": "certificates.rules.expired.fix", "route": "/certificates/10" }, "dismissible": false, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] } ], "finding": { "tier": "problem", "code": "expired", "severity": "critical", "subject": { "type": "certificate", "id": 10, "name": "vpn.northwind.example" }, "certificate_ids": [ 10 ], "addresses": [], "attention_key": "certificate:10", "due_at": "2026-01-03T12:00:00+00:00", "evidence": { "expires_at": "2026-01-03T12:00:00+00:00", "days_left": -12, "renews_itself": false, "renewal_type": "internal", "renewal_type_basis": "guess", "renewal_source": null, "issuer": "vpn.northwind.example", "served_status": null, "role": null, "renewer": null }, "copy": { "title": { "key": "certificates.rules.expired.title", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } }, "detail": { "key": "certificates.rules.expired.detail", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } }, "short": { "key": "certificates.rules.expired.short", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } } }, "fix": { "kind": "route", "label": "certificates.rules.expired.fix", "route": "/certificates/10" }, "dismissible": false, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] }, "health": { "grade": "warning", "letter": null, "issues": [ { "code": "self_signed", "severity": "warning" } ] }, "in_use": false, "tags": [], "check": null, "domain": null }, { "id": 2, "organization_id": 1, "source": "probe", "monitoring_check_id": 2, "domain_id": 3, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "b6b045177e3469ad718546c6233a2b7f0166d10e466deeaa6d16f6244f6654db", "subject": "northwind-app.dev", "issuer": "Let's Encrypt (R11)", "serial": "B4B32EC4", "sans": [ "northwind-app.dev", "*.northwind-app.dev" ], "not_before": "2025-11-07T12:00:00.000000Z", "not_after": "2026-02-05T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "ecdsa", "key_bits": 256, "sig_algo": "ECDSA-SHA256", "trust": "public", "validation": "DV", "is_wildcard": true, "is_multi_san": true, "managed": true, "renewal_source": "Let's Encrypt (R11)", "classification_override": null, "status": "expiring_soon", "superseded_by_id": null, "dismissed_at": null, "deployment_locations": [ { "endpoint": "northwind-app.dev:443", "last_seen": "2026-01-15T11:50:00+00:00", "first_seen": "2026-01-08T12:00:00+00:00" } ], "owner": null, "notes": null, "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": [ { "subject": "northwind-app.dev", "issuer": "Let's Encrypt (R11)", "is_ca": false, "self_signed": false, "not_after": "2026-02-05T12:00:00+00:00", "fingerprint": "b6b045177e3469ad718546c6233a2b7f0166d10e466deeaa6d16f6244f6654db" }, { "subject": "Let's Encrypt (R11)", "issuer": "ISRG Root X1", "is_ca": true, "self_signed": false, "not_after": "2030-01-15T12:00:00+00:00", "fingerprint": "c12c8a7dcb3f10b2cfd6efb6d14e6516f76cb6d3537ffae6a1f804b618d0cf3c" } ], "chain_status": "trusted", "sources": [ { "source": "probe", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": null, "caissuer_url": "http://r11.i.lencr.org/", "no_auto_merge": false, "price_yearly": "0.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "automatic", "renewal_type_basis": "guess", "served_status": "served", "served_checked_at": "2026-01-15T11:50:00.000000Z", "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null, "custom_fields": [], "renewer": null, "findings": [ { "tier": "problem", "code": "renewal_overdue", "severity": "warning", "subject": { "type": "certificate", "id": 2, "name": "northwind-app.dev" }, "certificate_ids": [ 2 ], "addresses": [ "northwind-app.dev:443" ], "attention_key": "certificate_renewal:2", "due_at": "2026-02-05T12:00:00+00:00", "evidence": { "expires_at": "2026-02-05T12:00:00+00:00", "days_left": 21, "renews_itself": true, "renewal_type": "automatic", "renewal_type_basis": "guess", "renewal_source": "Let's Encrypt (R11)", "issuer": "Let's Encrypt (R11)", "served_status": "served", "role": null, "renewer": null, "renews_around_at": "2026-01-06T00:00:00+00:00", "last_seen_at": "2026-01-15T06:00:00+00:00" }, "copy": { "title": { "key": "certificates.rules.renewal_overdue.title", "params": { "renews_at": "2026-01-06T00:00:00+00:00", "expires_at": "2026-02-05T12:00:00+00:00", "n": 9, "name": "northwind-app.dev" } }, "detail": { "key": "certificates.rules.renewal_overdue.detail", "params": { "renews_at": "2026-01-06T00:00:00+00:00", "expires_at": "2026-02-05T12:00:00+00:00", "n": 9, "name": "northwind-app.dev" } }, "short": { "key": "certificates.rules.renewal_overdue.short", "params": { "renews_at": "2026-01-06T00:00:00+00:00", "expires_at": "2026-02-05T12:00:00+00:00", "n": 9, "name": "northwind-app.dev" } } }, "fix": { "kind": "route", "label": "certificates.rules.renewal_overdue.fix", "route": "/certificates/2" }, "dismissible": true, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] } ], "finding": { "tier": "problem", "code": "renewal_overdue", "severity": "warning", "subject": { "type": "certificate", "id": 2, "name": "northwind-app.dev" }, "certificate_ids": [ 2 ], "addresses": [ "northwind-app.dev:443" ], "attention_key": "certificate_renewal:2", "due_at": "2026-02-05T12:00:00+00:00", "evidence": { "expires_at": "2026-02-05T12:00:00+00:00", "days_left": 21, "renews_itself": true, "renewal_type": "automatic", "renewal_type_basis": "guess", "renewal_source": "Let's Encrypt (R11)", "issuer": "Let's Encrypt (R11)", "served_status": "served", "role": null, "renewer": null, "renews_around_at": "2026-01-06T00:00:00+00:00", "last_seen_at": "2026-01-15T06:00:00+00:00" }, "copy": { "title": { "key": "certificates.rules.renewal_overdue.title", "params": { "renews_at": "2026-01-06T00:00:00+00:00", "expires_at": "2026-02-05T12:00:00+00:00", "n": 9, "name": "northwind-app.dev" } }, "detail": { "key": "certificates.rules.renewal_overdue.detail", "params": { "renews_at": "2026-01-06T00:00:00+00:00", "expires_at": "2026-02-05T12:00:00+00:00", "n": 9, "name": "northwind-app.dev" } }, "short": { "key": "certificates.rules.renewal_overdue.short", "params": { "renews_at": "2026-01-06T00:00:00+00:00", "expires_at": "2026-02-05T12:00:00+00:00", "n": 9, "name": "northwind-app.dev" } } }, "fix": { "kind": "route", "label": "certificates.rules.renewal_overdue.fix", "route": "/certificates/2" }, "dismissible": true, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] }, "health": { "grade": "good", "letter": "A", "issues": [] }, "in_use": true, "tags": [ { "id": 6, "name": "Northwind Platform", "color": "#14b8a6", "pivot": { "taggable_type": "App\\Models\\SslCertificate", "taggable_id": 2, "tag_id": 6 } } ], "check": { "id": 2, "name": "App — northwind-app.dev", "target": "https://northwind-app.dev", "interval_minutes": 0 }, "domain": { "id": 3, "name": "northwind-app", "tld": "dev", "full_name": "northwind-app.dev", "archivable": true, "dns_host": null, "management": "tracked", "sync_halt": null } } ] ```
--- ### Get a certificate {#get-organizations-organization-certificates-sslcertificate}
`GET` `/organizations/{organization}/certificates/{sslCertificate}` **Scope:** `monitoring.read` · **Changes data:** no One certificate with its domain, server, monitor, owner and tags, where it is served and its place in the renewal series. Also includes its findings and whether its files can be fetched from the provider. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "source": "probe", "monitoring_check_id": 1, "domain_id": 1, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9", "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "serial": "2395BA42", "sans": [ "northwind.com", "www.northwind.com" ], "not_before": "2025-12-30T12:00:00.000000Z", "not_after": "2026-03-30T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "rsa", "key_bits": 2048, "sig_algo": "SHA256-RSA", "trust": "public", "validation": "DV", "is_wildcard": false, "is_multi_san": true, "managed": true, "renewal_source": "Let's Encrypt (R3)", "classification_override": null, "status": "valid", "superseded_by_id": null, "dismissed_at": null, "deployment_locations": [ { "endpoint": "northwind.com:443", "last_seen": "2026-01-15T11:50:00+00:00", "first_seen": "2026-01-08T12:00:00+00:00" } ], "owner": null, "notes": null, "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": [ { "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "is_ca": false, "self_signed": false, "not_after": "2026-03-30T12:00:00+00:00", "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9" }, { "subject": "Let's Encrypt (R3)", "issuer": "ISRG Root X1", "is_ca": true, "self_signed": false, "not_after": "2030-01-15T12:00:00+00:00", "fingerprint": "e04ca19ed66485fb479283c98fbfb4cda1ab67de1965304902bec3f6a55a2c8e" } ], "chain_status": "trusted", "sources": [ { "source": "probe", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": "ok", "caissuer_url": "http://r3.i.lencr.org/", "no_auto_merge": false, "price_yearly": "0.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "automatic", "renewal_type_basis": "guess", "served_status": "served", "served_checked_at": "2026-01-15T11:50:00.000000Z", "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null, "health": { "grade": "good", "letter": "A", "issues": [] }, "in_use": true, "tags": [ { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\SslCertificate", "taggable_id": 1, "tag_id": 5 } } ], "check": { "id": 1, "name": "Marketing site", "target": "https://northwind.com", "interval_minutes": 0 }, "domain": { "id": 1, "name": "northwind", "tld": "com", "full_name": "northwind.com", "archivable": true, "dns_host": null, "management": "tracked", "sync_halt": null }, "cloud_server": null, "provider_credential": null, "superseded_by": null, "owner_user": null, "owner_candidates": [ { "id": 1, "name": "Alex Rivera" }, { "id": 3, "name": "Priya Raghunathan" } ], "bundle_available": false, "whereabouts": { "role": "current", "current_certificate": null, "renewer": null, "served_on": [ { "endpoint_id": 1, "host": "northwind.com", "port": 443, "as": "edge", "proxied": true, "last_checked_at": "2026-01-15T11:50:00+00:00", "last_check_source": "scan", "zone": { "id": 1, "name": "northwind.com", "domain_id": 1 }, "dns_record": { "id": 1, "type": "A", "name": "@", "value": "198.51.100.10" }, "cloud_server": { "id": 1, "name": "web-01" }, "monitor": { "id": 1, "name": "Marketing site" }, "renewer": null } ], "lifecycle": { "previous": null, "next": null, "replaced_at": null, "renewals": 0, "lead_days": null, "expected_renewal_at": "2026-02-28T12:00:00+00:00" }, "rollout": null }, "findings": [] } ```
--- ### Update a certificate {#patch-organizations-organization-certificates-sslcertificate}
`PATCH` `/organizations/{organization}/certificates/{sslCertificate}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes InfraNest’s record of the certificate: owner, notes, subscription end date, seller, tags and any corrections to how it is classified (purpose, trust, validation, managed, renewal type). Only the fields you send change; `owner_user_id` must be a member of the organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | | `owner`
body | string, nullable | Must not be greater than 255 characters. | | `owner_user_id`
body | integer, nullable | A member who owns it; checked against the organization below. | | `notes`
body | string, nullable | Must not be greater than 2000 characters. | | `subscription_ends_at`
body | string, nullable | A paid certificate's subscription and who sold it. Must be a valid date. | | `seller`
body | string, nullable | Must not be greater than 255 characters. | | `classification_override`
body | object, nullable | | | `tag_ids`
body | array | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/certificates/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"owner":"Platform team","notes":"Renewed automatically by the load balancer."}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "source": "probe", "monitoring_check_id": 1, "domain_id": 1, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9", "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "serial": "2395BA42", "sans": [ "northwind.com", "www.northwind.com" ], "not_before": "2025-12-30T12:00:00.000000Z", "not_after": "2026-03-30T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "rsa", "key_bits": 2048, "sig_algo": "SHA256-RSA", "trust": "public", "validation": "DV", "is_wildcard": false, "is_multi_san": true, "managed": true, "renewal_source": "Let's Encrypt (R3)", "classification_override": null, "status": "valid", "superseded_by_id": null, "dismissed_at": null, "deployment_locations": [ { "endpoint": "northwind.com:443", "last_seen": "2026-01-15T11:50:00+00:00", "first_seen": "2026-01-08T12:00:00+00:00" } ], "owner": "Platform team", "notes": "Renewed automatically by the load balancer.", "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": [ { "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "is_ca": false, "self_signed": false, "not_after": "2026-03-30T12:00:00+00:00", "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9" }, { "subject": "Let's Encrypt (R3)", "issuer": "ISRG Root X1", "is_ca": true, "self_signed": false, "not_after": "2030-01-15T12:00:00+00:00", "fingerprint": "e04ca19ed66485fb479283c98fbfb4cda1ab67de1965304902bec3f6a55a2c8e" } ], "chain_status": "trusted", "sources": [ { "source": "probe", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": "ok", "caissuer_url": "http://r3.i.lencr.org/", "no_auto_merge": false, "price_yearly": "0.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "automatic", "renewal_type_basis": "guess", "served_status": "served", "served_checked_at": "2026-01-15T11:50:00.000000Z", "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null, "tags": [ { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\SslCertificate", "taggable_id": 1, "tag_id": 5 } } ], "owner_user": null } ```
--- ### Get a certificate’s files from the provider {#get-organizations-organization-certificates-sslcertificate-bundle}
`GET` `/organizations/{organization}/certificates/{sslCertificate}/bundle` **Scope:** `monitoring.read` · **Changes data:** no Asks the provider the certificate came from for its files and returns the files; each fetch is recorded in your audit log. Requires write access, and is refused when the certificate did not come from a connected provider that can hand files back or is not linked to a domain. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/1/bundle" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Preview the CAA fix for a certificate {#get-organizations-organization-certificates-sslcertificate-caa-fix}
`GET` `/organizations/{organization}/certificates/{sslCertificate}/caa-fix` **Scope:** `monitoring.read` · **Changes data:** no The exact CAA record that would let this certificate’s authority issue it again, the DNS zone it would go in, the CAA records already there and any other certificate it would block. Refused when the certificate’s CAA policy does not block its renewal; nothing is changed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/1/caa-fix" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Dismiss or restore a certificate {#post-organizations-organization-certificates-sslcertificate-dismiss}
`POST` `/organizations/{organization}/certificates/{sslCertificate}/dismiss` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets the certificate aside so it no longer counts towards domain coverage. Send `dismissed: false` to restore it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/certificates/1/dismiss" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "source": "probe", "monitoring_check_id": 1, "domain_id": 1, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9", "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "serial": "2395BA42", "sans": [ "northwind.com", "www.northwind.com" ], "not_before": "2025-12-30T12:00:00.000000Z", "not_after": "2026-03-30T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "rsa", "key_bits": 2048, "sig_algo": "SHA256-RSA", "trust": "public", "validation": "DV", "is_wildcard": false, "is_multi_san": true, "managed": true, "renewal_source": "Let's Encrypt (R3)", "classification_override": null, "status": "valid", "superseded_by_id": null, "dismissed_at": "2026-01-15T12:00:00.000000Z", "deployment_locations": [ { "endpoint": "northwind.com:443", "last_seen": "2026-01-15T11:50:00+00:00", "first_seen": "2026-01-08T12:00:00+00:00" } ], "owner": null, "notes": null, "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": [ { "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "is_ca": false, "self_signed": false, "not_after": "2026-03-30T12:00:00+00:00", "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9" }, { "subject": "Let's Encrypt (R3)", "issuer": "ISRG Root X1", "is_ca": true, "self_signed": false, "not_after": "2030-01-15T12:00:00+00:00", "fingerprint": "e04ca19ed66485fb479283c98fbfb4cda1ab67de1965304902bec3f6a55a2c8e" } ], "chain_status": "trusted", "sources": [ { "source": "probe", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": "ok", "caissuer_url": "http://r3.i.lencr.org/", "no_auto_merge": false, "price_yearly": "0.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "automatic", "renewal_type_basis": "guess", "served_status": "served", "served_checked_at": "2026-01-15T11:50:00.000000Z", "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null, "tags": [ { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\SslCertificate", "taggable_id": 1, "tag_id": 5 } } ] } ```
--- ### Set a certificate’s price {#put-organizations-organization-certificates-sslcertificate-pricing}
`PUT` `/organizations/{organization}/certificates/{sslCertificate}/pricing` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets the yearly price InfraNest counts for this certificate, with optional currency and billing interval. Send an empty price to go back to the provider’s figure. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/certificates/1/pricing" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"price_yearly":59,"currency":"EUR"}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "source": "probe", "monitoring_check_id": 1, "domain_id": 1, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9", "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "serial": "2395BA42", "sans": [ "northwind.com", "www.northwind.com" ], "not_before": "2025-12-30T12:00:00.000000Z", "not_after": "2026-03-30T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "rsa", "key_bits": 2048, "sig_algo": "SHA256-RSA", "trust": "public", "validation": "DV", "is_wildcard": false, "is_multi_san": true, "managed": true, "renewal_source": "Let's Encrypt (R3)", "classification_override": null, "status": "valid", "superseded_by_id": null, "dismissed_at": null, "deployment_locations": [ { "endpoint": "northwind.com:443", "last_seen": "2026-01-15T11:50:00+00:00", "first_seen": "2026-01-08T12:00:00+00:00" } ], "owner": null, "notes": null, "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": [ { "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "is_ca": false, "self_signed": false, "not_after": "2026-03-30T12:00:00+00:00", "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9" }, { "subject": "Let's Encrypt (R3)", "issuer": "ISRG Root X1", "is_ca": true, "self_signed": false, "not_after": "2030-01-15T12:00:00+00:00", "fingerprint": "e04ca19ed66485fb479283c98fbfb4cda1ab67de1965304902bec3f6a55a2c8e" } ], "chain_status": "trusted", "sources": [ { "source": "probe", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": "ok", "caissuer_url": "http://r3.i.lencr.org/", "no_auto_merge": false, "price_yearly": "59.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "automatic", "renewal_type_basis": "guess", "served_status": "served", "served_checked_at": "2026-01-15T11:50:00.000000Z", "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null } ```
--- ### Get a certificate’s timeline {#get-organizations-organization-certificates-sslcertificate-timeline}
`GET` `/organizations/{organization}/certificates/{sslCertificate}/timeline` **Scope:** `monitoring.read` · **Changes data:** no The certificate’s history across its renewals: when it was issued, renewed and expired, plus the changes recorded in your audit log. For certificates you renew and pay for yourself, it also marks the day to order the renewal and when the subscription ends. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `sslCertificate_id`
path · required | integer | The sslCertificate's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/1/timeline" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "events": [ { "type": "issued", "at": "2025-12-30T12:00:00+00:00", "fingerprint": "6f6f72db3001" } ], "generations": 1, "markers": [] } ```
--- ### Get certificate coverage per domain {#get-organizations-organization-certificates-coverage}
`GET` `/organizations/{organization}/certificates/coverage` **Scope:** `monitoring.read` · **Changes data:** no For each of your domains, whether a certificate covers it (`covered`, `expiring`, `expired` or `uncovered`) and the best certificate that does. Superseded and dismissed certificates are left out. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/coverage" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "domain_id": 11, "domain": "northwind.cloud", "status": "covered", "certificate": { "id": 6, "subject": "*.northwind.cloud", "not_after": "2026-03-17T12:00:00.000000Z", "status": "valid", "source": "hetzner" } }, { "domain_id": 1, "domain": "northwind.com", "status": "covered", "certificate": { "id": 8, "subject": "cdn.northwind.com", "not_after": "2026-11-11T12:00:00.000000Z", "status": "valid", "source": "cloudflare" } } ] ```
--- ### List certificate findings {#get-organizations-organization-certificates-findings}
`GET` `/organizations/{organization}/certificates/findings` **Scope:** `monitoring.read` · **Changes data:** no Everything InfraNest found about your certificates that may need attention, from problems to recommendations, with counts per tier. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/findings" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "findings": [ { "tier": "problem", "code": "expired", "severity": "critical", "subject": { "type": "certificate", "id": 10, "name": "vpn.northwind.example" }, "certificate_ids": [ 10 ], "addresses": [], "attention_key": "certificate:10", "due_at": "2026-01-03T12:00:00+00:00", "evidence": { "expires_at": "2026-01-03T12:00:00+00:00", "days_left": -12, "renews_itself": false, "renewal_type": "internal", "renewal_type_basis": "guess", "renewal_source": null, "issuer": "vpn.northwind.example", "served_status": null, "role": null, "renewer": null }, "copy": { "title": { "key": "certificates.rules.expired.title", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } }, "detail": { "key": "certificates.rules.expired.detail", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } }, "short": { "key": "certificates.rules.expired.short", "params": { "expires_at": "2026-01-03T12:00:00+00:00", "name": "vpn.northwind.example" } } }, "fix": { "kind": "route", "label": "certificates.rules.expired.fix", "route": "/certificates/10" }, "dismissible": false, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] }, { "tier": "problem", "code": "domain_expires_first", "severity": "warning", "subject": { "type": "certificate", "id": 12, "name": "northwind.shop" }, "certificate_ids": [ 12 ], "addresses": [ "northwind.shop:443" ], "attention_key": "domain:10", "due_at": "2026-01-26T00:00:00+00:00", "evidence": { "expires_at": "2026-03-17T12:00:00+00:00", "days_left": 61, "renews_itself": true, "renewal_type": "automatic", "renewal_type_basis": "guess", "renewal_source": "Let's Encrypt (R11)", "issuer": "Let's Encrypt (R11)", "served_status": "served", "role": null, "renewer": null, "domain_id": 10, "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "domain_auto_renew": false }, "copy": { "title": { "key": "certificates.rules.domain_expires_first.title", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } }, "detail": { "key": "certificates.rules.domain_expires_first.detail", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } }, "short": { "key": "certificates.rules.domain_expires_first.short", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } } }, "fix": { "kind": "route", "label": "certificates.rules.domain_expires_first.fix", "route": "/domains/10" }, "dismissible": true, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] } ], "counts": { "problem": 6, "recommendation": 6, "info": 0 } } ```
--- ### Preview domains to add from certificates {#get-organizations-organization-certificates-fixes-domains}
`GET` `/organizations/{organization}/certificates/fixes/domains` **Scope:** `domains.read` · **Changes data:** no The domain names your certificates use that InfraNest does not track yet, where each is registered as far as is known, and the certificates that use it. Also says how many more domains your plan allows; nothing is changed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/fixes/domains" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "domains": [], "room": null, "allowed": true } ```
--- ### List certificate fixes already applied {#get-organizations-organization-certificates-fixes-done}
`GET` `/organizations/{organization}/certificates/fixes/done` **Scope:** `monitoring.read` · **Changes data:** no The certificate recommendations already acted on, newest first: monitors and domains added from a recommendation and CAA records added to unblock a renewal, with who did it and when. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/fixes/done" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "lines": [] } ```
--- ### Preview monitors to add for certificates {#get-organizations-organization-certificates-fixes-monitors}
`GET` `/organizations/{organization}/certificates/fixes/monitors` **Scope:** `monitoring.read` · **Changes data:** no The addresses InfraNest suggests watching with an SSL monitor, the monitor each would get and the certificates involved. Also says how many more monitors your plan allows; nothing is changed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/fixes/monitors" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "addresses": [ { "address": "archive.northwind.com:443", "host": "archive.northwind.com", "port": 443, "certificates": [ { "id": 18, "name": "archive.northwind.com" } ], "monitor": { "name": "archive.northwind.com", "type": "ssl", "target": "archive.northwind.com", "interval_seconds": 3600 }, "unsupported": null }, { "address": "billing.northwind.nl:443", "host": "billing.northwind.nl", "port": 443, "certificates": [ { "id": 14, "name": "billing.northwind.nl" } ], "monitor": { "name": "billing.northwind.nl", "type": "ssl", "target": "billing.northwind.nl", "interval_seconds": 3600 }, "unsupported": null } ], "room": null, "allowed": true } ```
--- ### Get the certificate overview {#get-organizations-organization-certificates-overview}
`GET` `/organizations/{organization}/certificates/overview` **Scope:** `monitoring.read` · **Changes data:** no An address-by-address view of the certificates your visitors actually receive, grouped by domain, with a summary, what needs you, the renewals due in the next 90 days and recommendations. Also lists the members who can be made a certificate’s owner. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/certificates/overview" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "summary": { "state": "needs_you", "addresses_needing_you": 5, "other_problems": 1, "first_problem": { "endpoint_id": 7 }, "todo": [ { "endpoint_id": 7, "host": "northwind.shop", "port": 443, "address": "northwind.shop:443", "finding": { "tier": "problem", "code": "domain_expires_first", "severity": "warning", "subject": { "type": "certificate", "id": 12, "name": "northwind.shop" }, "certificate_ids": [ 12 ], "addresses": [ "northwind.shop:443" ], "attention_key": "domain:10", "due_at": "2026-01-26T00:00:00+00:00", "evidence": { "expires_at": "2026-03-17T12:00:00+00:00", "days_left": 61, "renews_itself": true, "renewal_type": "automatic", "renewal_type_basis": "guess", "renewal_source": "Let's Encrypt (R11)", "issuer": "Let's Encrypt (R11)", "served_status": "served", "role": null, "renewer": null, "domain_id": 10, "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "domain_auto_renew": false }, "copy": { "title": { "key": "certificates.rules.domain_expires_first.title", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } }, "detail": { "key": "certificates.rules.domain_expires_first.detail", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } }, "short": { "key": "certificates.rules.domain_expires_first.short", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } } }, "fix": { "kind": "route", "label": "certificates.rules.domain_expires_first.fix", "route": "/domains/10" }, "dismissible": true, "advisor": null, "decision": null, "automation": null, "sources": [], "also": [] }, "attention_keys": [ "domain:10" ] } ], "decided": [], "addresses": 12, "domains": 5, "renew_automatically": 8, "monitored": 5, "recommendations": 6, "watched": 1, "next_renewal": { "host": "staging.northwind-app.dev", "more": 0, "at": "2026-01-18T00:00:00+00:00", "who": "automatic" } }, "timeline": [ { "certificate_id": 16, "subject": "northwind-mail.com", "issuer": "Sectigo RSA Organization Validation Secure Server CA", "expires_at": "2026-02-02T12:00:00+00:00", "renews_at": null, "who": "you", "endpoint_ids": [ 11 ], "hosts": [ "mx1.northwind-mail.com" ], "listed": true } ], "needs_you": [ { "kind": "problem", "at": "2026-01-26T00:00:00+00:00", "now": false, "severity": "warning", "endpoint_id": 7, "hosts": [ "northwind.shop" ], "reason": { "key": "certificates.rules.domain_expires_first.short", "params": { "domain": "northwind.shop", "domain_expires_at": "2026-01-26T00:00:00+00:00", "expires_at": "2026-03-17T12:00:00+00:00", "name": "northwind.shop" } }, "who": null } ], "groups": [ { "key": "domain:northwind-mail.com", "domain_id": 16, "name": "northwind-mail.com", "in_domains": true, "rows": [ { "domain_id": 16, "endpoint_id": 11, "host": "mx1.northwind-mail.com", "port": 993, "address": "mx1.northwind-mail.com:993", "status": "problem", "severity": "warning", "reason": { "key": "certificates.rules.rollout_incomplete.short", "params": { "address": "mx1.northwind-mail.com:993", "name": "northwind-mail.com", "done": 1, "total": 2, "expires_at": "2026-02-02T12:00:00+00:00", "server": "mail-01" } }, "check_note": null, "certificate": { "id": 16, "subject": "northwind-mail.com", "issuer": "Sectigo RSA Organization Validation Secure Server CA", "not_after": "2026-02-02T12:00:00+00:00", "status": "superseded", "role": null }, "origin_certificate": null, "served_by": { "kind": "server", "provider": "transip", "provider_label": "TransIP", "cloud_server": { "id": 14, "name": "mail-01" }, "resolved_ips": [ "198.51.100.166" ], "edge_network": null, "network": null, "display": { "kind": "server", "name": "mail-01", "ip": "198.51.100.166" }, "dns_record": { "id": 53, "type": "A", "name": "mx1", "value": "198.51.100.166", "zone": { "id": 9, "name": "northwind-mail.com" } } }, "renewal": { "type": "manual_paid", "source": null, "renews_itself": false, "actor": "you" }, "ownership": { "is": "yours", "basis": "domain", "override": null }, "monitor": null, "last_checked_at": "2026-01-15T11:50:00+00:00", "last_check_source": "scan", "last_attempt_at": "2026-01-15T11:50:00+00:00", "failing": null, "findings": [ { "tier": "problem", "code": "rollout_incomplete", "severity": "warning", "subject": { "type": "address", "id": 11, "name": "mx1.northwind-mail.com:993" }, "certificate_ids": [ 16 ], "addresses": [ "mx1.northwind-mail.com:993" ], "attention_key": "certificate_rollout:11", "due_at": "2026-02-02T12:00:00+00:00", "evidence": { "endpoint_id": 11, "serves_certificate_id": 16, "successor_id": 15, "expires_at": "2026-02-02T12:00:00+00:00", "done": 1, "total": 2, "last_checked_at": "2026-01-15T11:50:00+00:00" }, "copy": { "title": { "key": "certificates.rules.rollout_incomplete.title", "params": { "address": "mx1.northwind-mail.com:993", "name": "northwind-mail.com", "done": 1, "total": 2, "expires_at": "2026-02-02T12:00:00+00:00" } }, "detail": { "key": "certificates.rules.rollout_incomplete.detailServer", "params": { "address": "mx1.northwind-mail.com:993", "name": "northwind-mail.com", "done": 1, "total": 2, "expires_at": "2026-02-02T12:00:00+00:00", "server": "mail-01" }, "count": 2 }, "short": { "key": "certificates.rules.rollout_incomplete.short", "params": { "address": "mx1.northwind-mail.com:993", "name": "northwind-mail.com", "done": 1, "total": 2, "expires_at": "2026-02-02T12:00:00+00:00", "server": "mail-01" } } }, "fix": { "kind": "action", "label": "certificates.rules.rollout_incomplete.fix", "action": "certificate.scan", "method": "POST", "endpoint": "/organizations/1/certificates/scan", "body": { "host": "mx1.northwind-mail.com", "port": 993 } }, "dismissible": true, "advisor": null, "decision": null, "automation": { "template": "cert_rollout_not_live_alert", "action": "send_notification", "provider": null, "connected": true, "conditions": [], "title": { "key": "certificates.automate.cert_rollout_not_live_alert", "params": { "days": 0 } } }, "sources": [ { "source": "check", "tone": "bad", "copy": { "key": "certificates.evidence.check.oldCertificate", "params": [] } } ], "also": [] } ], "set_aside": false } ], "count": 2, "problems": 1, "next_expiry": "2026-02-02T12:00:00+00:00" } ], "watched": [ { "endpoint_id": 13, "host": "status.supplier.example", "port": 443, "address": "status.supplier.example:443", "domain_id": null, "status": "ok", "severity": null, "reason": null, "check_note": null, "certificate": { "id": 19, "subject": "status.supplier.example", "issuer": "Let's Encrypt (R3)", "not_after": "2026-03-28T12:00:00+00:00", "status": "valid", "role": null }, "origin_certificate": null, "served_by": { "kind": "address", "provider": null, "provider_label": null, "cloud_server": null, "resolved_ips": [ "203.0.113.210" ], "edge_network": null, "network": null, "display": { "kind": "ip", "name": "203.0.113.210", "ip": "203.0.113.210" }, "dns_record": null }, "renewal": { "type": "automatic", "source": "Let's Encrypt (R3)", "renews_itself": true, "actor": "automatic" }, "ownership": { "is": "watched", "basis": "none", "override": null }, "monitor": { "id": 13, "name": "Supplier status", "status": "up", "last_checked_at": "2026-01-15T11:58:00+00:00" }, "last_checked_at": "2026-01-15T11:50:00+00:00", "last_check_source": "scan", "last_attempt_at": "2026-01-15T11:50:00+00:00", "failing": null, "findings": [] } ], "recommendations": [ { "tier": "recommendation", "code": "chain_incomplete", "severity": "warning", "subject": { "type": "certificate", "id": 4, "name": "docs.northwind.com" }, "certificate_ids": [ 4 ], "addresses": [ "docs.northwind.com:443" ], "attention_key": null, "due_at": null, "evidence": { "expires_at": "2026-03-08T12:00:00+00:00", "days_left": 52, "renews_itself": true, "renewal_type": "automatic", "renewal_type_basis": "guess", "renewal_source": "Let's Encrypt (R11)", "issuer": "Let's Encrypt (R11)", "served_status": "served", "role": null, "renewer": null, "chain_status": "incomplete" }, "copy": { "title": { "key": "certificates.rules.chain_incomplete.title", "params": { "name": "docs.northwind.com" } }, "detail": { "key": "certificates.rules.chain_incomplete.detail", "params": { "name": "docs.northwind.com" } }, "short": { "key": "certificates.rules.chain_incomplete.short", "params": { "name": "docs.northwind.com" } } }, "fix": { "kind": "route", "label": "certificates.rules.chain_incomplete.fix", "route": "/certificates/4" }, "dismissible": true, "advisor": { "resource_type": "certificate", "resource_id": "4", "check_id": "chain" }, "decision": null, "automation": null, "sources": [], "also": [] } ], "recommendation_groups": [ { "code": "chain_incomplete", "severity": "warning", "items": [ { "tier": "recommendation", "code": "chain_incomplete", "severity": "warning", "subject": { "type": "certificate", "id": 4, "name": "docs.northwind.com" }, "certificate_ids": [ 4 ], "addresses": [ "docs.northwind.com:443" ], "attention_key": null, "due_at": null, "evidence": { "expires_at": "2026-03-08T12:00:00+00:00", "days_left": 52, "renews_itself": true, "renewal_type": "automatic", "renewal_type_basis": "guess", "renewal_source": "Let's Encrypt (R11)", "issuer": "Let's Encrypt (R11)", "served_status": "served", "role": null, "renewer": null, "chain_status": "incomplete" }, "copy": { "title": { "key": "certificates.rules.chain_incomplete.title", "params": { "name": "docs.northwind.com" } }, "detail": { "key": "certificates.rules.chain_incomplete.detail", "params": { "name": "docs.northwind.com" } }, "short": { "key": "certificates.rules.chain_incomplete.short", "params": { "name": "docs.northwind.com" } } }, "fix": { "kind": "route", "label": "certificates.rules.chain_incomplete.fix", "route": "/certificates/4" }, "dismissible": true, "advisor": { "resource_type": "certificate", "resource_id": "4", "check_id": "chain" }, "decision": null, "automation": null, "sources": [], "also": [] } ], "count": 1, "copy": { "title": { "key": "certificates.overview.recGroups.chain_incomplete.title", "params": { "n": 1 } }, "detail": { "key": "certificates.overview.recGroups.chain_incomplete.detail", "params": { "n": 1 } } }, "fix": { "kind": "route", "label": "certificates.rules.chain_incomplete.fix", "route": "/certificates/4" }, "dismiss": [ { "resource_type": "certificate", "resource_id": "4", "check_id": "chain" } ] } ], "dismissed_recommendations": [], "tidied": { "replaced": 2, "not_served": 0, "provider": 0, "merged": 1, "dismissed": 0, "untracked": 0 }, "untracked": [], "inventory": [ { "id": 10, "subject": "vpn.northwind.example", "issuer": "vpn.northwind.example", "source": "manual", "sources": [ "manual" ], "not_after": "2026-01-03T12:00:00+00:00", "status": "expired", "role": "not_seen", "hosts": [], "superseded_by_id": null } ], "owner_candidates": [ { "id": 1, "name": "Alex Rivera" } ] } ```
--- ### Assign an owner to certificates {#post-organizations-organization-certificates-owner}
`POST` `/organizations/{organization}/certificates/owner` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Makes the member given in `user_id` the owner of each certificate in `certificate_ids` (up to 200). Refused when that person is not a member of the organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `certificate_ids`
body | array | | | `user_id`
body · required | integer | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/certificates/owner" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"certificate_ids":[1],"user_id":1}' ``` **Response** · 200 ```json { "assigned": 1, "owner": { "id": 1, "name": "Alex Rivera" } } ```
--- # Servers and cloud Servers, volumes, networks, firewalls and load balancers. Provider actions stay in the app; tokens edit InfraNest's own records and alerting. **Common tasks** - [List your servers](#get-organizations-organization-cloud-servers) - [Snooze a server's alerts](#post-organizations-organization-cloud-servers-cloudserver-snooze) - [Every address you hold](#get-organizations-organization-cloud-primary-ips) **Only in the app:** Creating, deleting, resizing and rebuilding servers, and anything else billed by the provider. **How it works in the app:** [help article](https://infranest.io/docs/manage-a-server). **Scopes:** Read with `cloud.read` or `monitoring.read`, change with `cloud.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Cloud firewalls](#cloud-firewalls) 3 · [Cloud load balancers](#cloud-load-balancers) 2 · [Cloud networks](#cloud-networks) 2 · [Cloud primary IPs](#cloud-primary-ips) 1 · [Cloud servers](#cloud-servers) 34 · [Cloud snapshots](#cloud-snapshots) 2 · [Cloud volumes](#cloud-volumes) 2 · [Connect requests](#connect-requests) 1 · [Endpoints](#endpoints) 2 · [Firewall templates](#firewall-templates) 2
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Cloud firewalls ### List firewalls {#get-organizations-organization-cloud-firewalls}
`GET` `/organizations/{organization}/cloud-firewalls` **Scope:** `cloud.read` · **Changes data:** no Every cloud firewall InfraNest tracks in the organization, a page at a time (`per_page`, default 20), ordered by account and name. Each includes the entries a Dynamic IP keeps up to date in it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-firewalls" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 6, "organization_id": 1, "provider": "digitalocean", "provider_firewall_id": "fw-6", "name": "legacy-vpn", "rules": [ { "port": "1194", "protocol": "udp", "direction": "in", "source_ips": [ "0.0.0.0/0" ], "description": "OpenVPN" } ], "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 9, "applied_to_count": 0, "applied_server_ids": [], "delete_protection": false, "capabilities": { "native_delete_protection": false, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": false, "snapshots": true, "backups": true, "isos": false, "rescue": false, "placement_groups": false, "public_network_toggle": false, "scheduled_backups": true, "private_networks": true, "private_network_attach": false, "network_join_on_create": true, "network_attach_ip_choice": false, "private_network_unfiltered": false, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": false, "network_routes": false, "network_rename": true, "network_ip_range_extendable": false, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": false, "volume_native_delete_protection": false, "network_native_delete_protection": false, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": false, "rdns_address_list": false, "reverse_dns": false, "snapshot_description": false, "snapshot_protection": false }, "sync_halt": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [], "dynamic_ip_targets": [] }, { "id": 4, "organization_id": 1, "provider": "digitalocean", "provider_firewall_id": "fw-4", "name": "staging", "rules": [ { "port": "443", "protocol": "tcp", "direction": "in", "source_ips": [ "0.0.0.0/0" ], "description": "HTTPS" }, { "port": "22", "protocol": "tcp", "direction": "in", "source_ips": [ "203.0.113.0/24" ], "description": "SSH from the office" } ], "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 9, "applied_to_count": 2, "applied_server_ids": [ "109", "110" ], "delete_protection": false, "capabilities": { "native_delete_protection": false, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": false, "snapshots": true, "backups": true, "isos": false, "rescue": false, "placement_groups": false, "public_network_toggle": false, "scheduled_backups": true, "private_networks": true, "private_network_attach": false, "network_join_on_create": true, "network_attach_ip_choice": false, "private_network_unfiltered": false, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": false, "network_routes": false, "network_rename": true, "network_ip_range_extendable": false, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": false, "volume_native_delete_protection": false, "network_native_delete_protection": false, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": false, "rdns_address_list": false, "reverse_dns": false, "snapshot_description": false, "snapshot_protection": false }, "sync_halt": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [], "dynamic_ip_targets": [] } ], "first_page_url": "http://localhost/api/organizations/1/cloud-firewalls?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/cloud-firewalls?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/cloud-firewalls?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/cloud-firewalls", "per_page": 20, "prev_page_url": null, "to": 6, "total": 6 } ```
--- ### Get a firewall {#get-organizations-organization-cloud-firewalls-cloudfirewall}
`GET` `/organizations/{organization}/cloud-firewalls/{cloudFirewall}` **Scope:** `cloud.read` · **Changes data:** no One firewall with its rules, tags, Dynamic IP entries, what its provider allows and the protocols it accepts. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudFirewall_id`
path · required | integer | The cloudFirewall's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-firewalls/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_firewall_id": "fw-1", "name": "web-tier", "rules": [ { "port": "443", "protocol": "tcp", "direction": "in", "source_ips": [ "0.0.0.0/0", "::/0" ], "description": "HTTPS" }, { "port": "80", "protocol": "tcp", "direction": "in", "source_ips": [ "0.0.0.0/0", "::/0" ], "description": "HTTP" } ], "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "applied_to_count": 4, "applied_server_ids": [ "100", "101" ], "delete_protection": false, "sync_halt": null, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "firewall_protocols": [ "tcp", "udp" ], "tags": [], "dynamic_ip_targets": [], "untracked_sources": [], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null } } ```
--- ### Check a firewall for drift {#get-organizations-organization-cloud-firewalls-cloudfirewall-drift}
`GET` `/organizations/{organization}/cloud-firewalls/{cloudFirewall}/drift` **Scope:** `cloud.read` · **Changes data:** no Compares the firewall’s rules in InfraNest with the rules live at the cloud provider and returns `drifted` with the provider’s rules. When the provider cannot be asked, `drifted` is false and `provider_rules` is null. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudFirewall_id`
path · required | integer | The cloudFirewall's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-firewalls/1/drift" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
## Cloud load balancers ### List load balancers {#get-organizations-organization-cloud-load-balancers}
`GET` `/organizations/{organization}/cloud-load-balancers` **Scope:** `cloud.read` · **Changes data:** no Every load balancer InfraNest tracks in the organization, a page at a time. Filter with `search`, `running`, `status`, `datacenter`, `tag` or `credential`, and sort with `sort` and `dir`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-load-balancers" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 3, "organization_id": 1, "provider": "digitalocean", "provider_lb_id": "lb-3", "name": "staging-lb", "status": "running", "load_balancer_type": "lb-small", "ipv4": "203.0.113.60", "ipv6": "2001:db8::60", "datacenter": "ams3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 9, "algorithm": "round_robin", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 443 } ], "targets": [ { "type": "server", "server_id": 10 } ], "price_monthly": "12.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "sync_halt": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] }, { "id": 2, "organization_id": 1, "provider": "hetzner", "provider_lb_id": "lb-2", "name": "api-lb", "status": "running", "load_balancer_type": "lb21", "ipv4": "203.0.113.51", "ipv6": "2001:db8::51", "datacenter": "fsn1-dc14", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "algorithm": "least_connections", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 443 } ], "targets": [ { "type": "server", "server_id": 3 }, { "type": "server", "server_id": 6 } ], "price_monthly": "19.90", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "sync_halt": null, "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] } ], "first_page_url": "http://localhost/api/organizations/1/cloud-load-balancers?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/cloud-load-balancers?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/cloud-load-balancers?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/cloud-load-balancers", "per_page": 25, "prev_page_url": null, "to": 3, "total": 3, "meta": { "counts": { "total": 3, "running": 3 }, "integrations": [ { "id": 9, "provider": "digitalocean", "label": "DigitalOcean — Staging" }, { "id": 1, "provider": "hetzner", "label": "Hetzner — Production" } ], "tags": [] } } ```
--- ### Get a load balancer {#get-organizations-organization-cloud-load-balancers-cloudloadbalancer}
`GET` `/organizations/{organization}/cloud-load-balancers/{cloudLoadBalancer}` **Scope:** `cloud.read` · **Changes data:** no One load balancer with its tags and what its provider allows. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudLoadBalancer_id`
path · required | integer | The cloudLoadBalancer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-load-balancers/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_lb_id": "lb-1", "name": "web-lb", "status": "running", "load_balancer_type": "lb11", "ipv4": "203.0.113.50", "ipv6": "2001:db8::50", "datacenter": "fsn1-dc14", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "algorithm": "round_robin", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 443 } ], "targets": [ { "type": "server", "server_id": 1 }, { "type": "server", "server_id": 2 } ], "price_monthly": "5.39", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "sync_halt": null, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] } ```
## Cloud networks ### List networks {#get-organizations-organization-cloud-networks}
`GET` `/organizations/{organization}/cloud-networks` **Scope:** `cloud.read` · **Changes data:** no Every private network InfraNest tracks in the organization, ordered by name, with its tags. Not paginated. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-networks" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": 1, "organization_id": 1, "provider_credential_id": 1, "provider": "hetzner", "provider_network_id": "net-1", "name": "private-backbone", "ip_range": "10.0.0.0/16", "network_zone": "eu-central", "subnets_count": 2, "routes_count": 0, "servers_count": 6, "protection": { "delete": false }, "labels": [], "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subnets": [ { "type": "cloud", "gateway": "10.0.0.1", "ip_range": "10.0.1.0/24", "network_zone": "eu-central" }, { "type": "cloud", "gateway": "10.0.0.1", "ip_range": "10.0.2.0/24", "network_zone": "eu-central" } ], "routes": [], "servers": [ "100", "101" ], "delete_protection": false, "sync_halt": null, "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] }, { "id": 3, "organization_id": 1, "provider_credential_id": 9, "provider": "digitalocean", "provider_network_id": "net-3", "name": "staging-vpc", "ip_range": "10.10.0.0/20", "network_zone": "ams3", "subnets_count": 1, "routes_count": 0, "servers_count": 2, "protection": { "delete": false }, "labels": [], "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subnets": [ { "type": "cloud", "ip_range": "10.10.0.0/20", "network_zone": "ams3" } ], "routes": [], "servers": [ "109", "110" ], "delete_protection": false, "sync_halt": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] } ] } ```
--- ### Get a network {#get-organizations-organization-cloud-networks-cloudnetwork}
`GET` `/organizations/{organization}/cloud-networks/{cloudNetwork}` **Scope:** `cloud.read` · **Changes data:** no One private network with its tags and what its provider allows. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudNetwork_id`
path · required | integer | The cloudNetwork's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-networks/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider_credential_id": 1, "provider": "hetzner", "provider_network_id": "net-1", "name": "private-backbone", "ip_range": "10.0.0.0/16", "network_zone": "eu-central", "subnets_count": 2, "routes_count": 0, "servers_count": 6, "protection": { "delete": false }, "labels": [], "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subnets": [ { "type": "cloud", "gateway": "10.0.0.1", "ip_range": "10.0.1.0/24", "network_zone": "eu-central" }, { "type": "cloud", "gateway": "10.0.0.1", "ip_range": "10.0.2.0/24", "network_zone": "eu-central" } ], "routes": [], "servers": [ "100", "101" ], "delete_protection": false, "sync_halt": null, "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [], "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true } } ```
## Cloud primary IPs ### List IP addresses {#get-organizations-organization-cloud-primary-ips}
`GET` `/organizations/{organization}/cloud-primary-ips` **Scope:** `cloud.read` · **Changes data:** no The IP addresses across your cloud accounts, with the server each is attached to. Where a provider cannot list addresses directly, the list is built from your servers and misses unattached addresses; those accounts are named in `server_derived_credentials`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-primary-ips" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": "ip_1", "name": "web-01-ipv4", "ip": "198.51.100.10", "type": "ipv4", "server_id": 1, "server_name": "web-01", "last_server_name": "web-01", "unattached": false, "dns_ptr": [ { "ip": "198.51.100.10", "dns_ptr": "web-01.northwind.cloud" } ], "datacenter": "nbg1-dc3", "location_city": "Nuremberg", "location_country": "de", "protection": { "delete": false }, "auto_delete": false, "blocked": false, "price_monthly": 0.6, "price_currency": "eur", "price_source": "api", "cloud_ip_id": 1, "credential_id": 1, "credential_label": "Hetzner — Production", "provider": "hetzner", "source": "provider", "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true } }, { "id": "ip_2", "name": "web-02-ipv4", "ip": "198.51.100.11", "type": "ipv4", "server_id": 2, "server_name": "web-02", "last_server_name": "web-02", "unattached": false, "dns_ptr": [ { "ip": "198.51.100.11", "dns_ptr": "web-02.northwind.cloud" } ], "datacenter": "nbg1-dc3", "location_city": "Nuremberg", "location_country": "de", "protection": { "delete": false }, "auto_delete": false, "blocked": false, "price_monthly": 0.6, "price_currency": "eur", "price_source": "api", "cloud_ip_id": 2, "credential_id": 1, "credential_label": "Hetzner — Production", "provider": "hetzner", "source": "provider", "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true } } ], "server_derived_credentials": [ { "id": 5, "label": "TransIP — Amsterdam", "provider": "transip" }, { "id": 8, "label": "OVHcloud — Beauharnois", "provider": "ovh" } ] } ```
## Cloud servers ### List servers {#get-organizations-organization-cloud-servers}
`GET` `/organizations/{organization}/cloud-servers` **Scope:** `cloud.read` · **Changes data:** no Every server InfraNest tracks in the organization, a page at a time, with its provider, status, addresses and tags. Filter with `search`, `status`, `running`, `datacenter`, `tag`, `credential`, `provider` or `tracking=untracked`, and sort with `sort` and `dir`; self-managed machines are counted in `meta` but not listed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 13, "organization_id": 1, "provider": "digitalocean", "provider_server_id": "112", "name": "analytics-01", "status": "stopped", "server_type": "c-4", "image": "ubuntu-24-04-x64", "ipv4": "192.0.2.80", "ipv6": null, "datacenter": "nyc3", "provisioned_at": "2025-07-25T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 50, "price_monthly": "84.00", "price_hourly": "0.1151", "location_city": "New York", "location_country": "US", "included_traffic": 6597069766656, "outgoing_traffic": 131941395333, "incoming_traffic": 65970697667, "ipv4_dns_ptr": "analytics-01.northwind.cloud", "firewall_enabled": false }, "provider_credential_id": 9, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": false, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "provider", "agent_health": null, "monitoring_enabled": false, "monitoring_enabled_at": null, "volumes_count": 0, "agent_last_seen_at": null, "agent_tokens_count": 0, "custom_fields": { "1": "Data" }, "capabilities": { "native_delete_protection": false, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": false, "snapshots": true, "backups": true, "isos": false, "rescue": false, "placement_groups": false, "public_network_toggle": false, "scheduled_backups": true, "private_networks": true, "private_network_attach": false, "network_join_on_create": true, "network_attach_ip_choice": false, "private_network_unfiltered": false, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": false, "network_routes": false, "network_rename": true, "network_ip_range_extendable": false, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": false, "volume_native_delete_protection": false, "network_native_delete_protection": false, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": false, "rdns_address_list": false, "reverse_dns": false, "snapshot_description": false, "snapshot_protection": false }, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 50, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 50 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "stopped", "sync_halt": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [ { "id": 2, "name": "Staging", "color": "#f59e0b", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 13, "tag_id": 2 } } ] }, { "id": 12, "organization_id": 1, "provider": "digitalocean", "provider_server_id": "111", "name": "queue-01", "status": "running", "server_type": "s-4vcpu-8gb", "image": "ubuntu-24-04-x64", "ipv4": "192.0.2.40", "ipv6": "2001:db8:3:d0::a22:1/64", "datacenter": "fra1", "provisioned_at": "2025-08-06T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "48.00", "price_hourly": "0.0658", "location_city": "Frankfurt", "location_country": "DE", "included_traffic": 5497558138880, "outgoing_traffic": 1044536046387, "incoming_traffic": 384829069722, "ipv4_dns_ptr": "queue-01.northwind.cloud", "firewall_enabled": false }, "provider_credential_id": 9, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": false, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "agent_health": null, "monitoring_enabled": true, "monitoring_enabled_at": null, "volumes_count": 0, "agent_last_seen_at": null, "agent_tokens_count": 0, "custom_fields": { "1": "Platform" }, "capabilities": { "native_delete_protection": false, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": false, "snapshots": true, "backups": true, "isos": false, "rescue": false, "placement_groups": false, "public_network_toggle": false, "scheduled_backups": true, "private_networks": true, "private_network_attach": false, "network_join_on_create": true, "network_attach_ip_choice": false, "private_network_unfiltered": false, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": false, "network_routes": false, "network_rename": true, "network_ip_range_extendable": false, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": false, "volume_native_delete_protection": false, "network_native_delete_protection": false, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": false, "rdns_address_list": false, "reverse_dns": false, "snapshot_description": false, "snapshot_protection": false }, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [ { "id": 1, "name": "Production", "color": "#ef4444", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 12, "tag_id": 1 } } ] } ], "first_page_url": "http://localhost/api/organizations/1/cloud-servers?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/cloud-servers?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/cloud-servers?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/cloud-servers", "per_page": 25, "prev_page_url": null, "to": 14, "total": 14, "meta": { "locked_ids": [], "counts": { "total": 14, "running": 12, "untracked": 0, "self_managed": 0 }, "integrations": [ { "id": 9, "provider": "digitalocean", "label": "DigitalOcean — Staging", "capabilities": { "native_delete_protection": false, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": false, "snapshots": true, "backups": true, "isos": false, "rescue": false, "placement_groups": false, "public_network_toggle": false, "scheduled_backups": true, "private_networks": true, "private_network_attach": false, "network_join_on_create": true, "network_attach_ip_choice": false, "private_network_unfiltered": false, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": false, "network_routes": false, "network_rename": true, "network_ip_range_extendable": false, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": false, "volume_native_delete_protection": false, "network_native_delete_protection": false, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": false, "rdns_address_list": false, "reverse_dns": false, "snapshot_description": false, "snapshot_protection": false } }, { "id": 1, "provider": "hetzner", "label": "Hetzner — Production", "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true } } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6" }, { "id": 6, "name": "Northwind Platform", "color": "#14b8a6" } ], "custom_fields": [ { "id": 1, "key": "cost_centre", "label": "Cost centre", "type": "select", "options": [ "Platform", "Marketing" ] } ] } } ```
--- ### Get a server {#get-organizations-organization-cloud-servers-cloudserver}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}` **Scope:** `cloud.read` · **Changes data:** no One server with its volumes, tags, cost breakdown and what its provider lets InfraNest do with it. Also says whether it can be renamed and whether its monitoring agent’s readings are being refused. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_server_id": "100", "name": "web-01", "status": "running", "server_type": "cpx31", "image": "ubuntu-24.04", "ipv4": "198.51.100.10", "ipv6": "2001:db8:1c1e:5a10::/64", "datacenter": "nbg1-dc3", "provisioned_at": "2025-12-16T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "15.90", "price_hourly": "0.0218", "location_city": "Nuremberg", "location_country": "DE", "included_traffic": 21990232555520, "outgoing_traffic": 6816972092211, "incoming_traffic": 1979120929997, "ipv4_dns_ptr": "web-01.northwind.cloud", "firewall_enabled": false, "backup_window": "22-02" }, "provider_credential_id": 1, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": true, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "agent_health": null, "monitoring_enabled": true, "monitoring_enabled_at": null, "volumes_count": 1, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "volumes": [ { "id": 3, "organization_id": 1, "cloud_server_id": 1, "provider": "hetzner", "provider_volume_id": "vol-3", "name": "uploads", "size_gb": 250, "status": "attached", "datacenter": "nbg1-dc3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "price_monthly": "12.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 5 } } ], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "cost": { "base": 15.9, "currency": "EUR", "source": "api", "extras": [ { "key": "backups", "amount": 3.18, "source": "computed", "own": true }, { "key": "volume", "label": "uploads", "amount": 12, "source": "api", "own": false } ], "total": 31.75 }, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "capability_blockers": [], "rename_refusal": null, "agent_silent_after_seconds": 300, "agent_refusal": null, "uptime_seconds": 3542400 } ```
--- ### Update a server {#put-organizations-organization-cloud-servers-cloudserver}
`PUT` `/organizations/{organization}/cloud-servers/{cloudServer}` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes a server’s name, city, country or tags; only the fields you send change. A new name is sent to the cloud provider first and refused when the provider cannot rename servers, while the provider’s own facts (addresses, type, status) can only be edited on a self-managed server. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `provider`
body | string, nullable | | | `provider_server_id`
body | string, nullable | Must not be greater than 255 characters. | | `name`
body | string | Must not be greater than 255 characters. | | `status`
body | string | One of: `running`, `stopped`, `starting`, `stopping`, `rebuilding`, `migrating`, `provisioning`, `error`, `unknown`. | | `server_type`
body | string, nullable | Must not be greater than 100 characters. | | `image`
body | string, nullable | Must not be greater than 100 characters. | | `ipv4`
body | string, nullable | Must not be greater than 45 characters. | | `ipv6`
body | string, nullable | Must not be greater than 45 characters. | | `datacenter`
body | string, nullable | Must not be greater than 100 characters. | | `provisioned_at`
body | string, nullable | Must be a valid date. | | `meta`
body | object | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/cloud-servers/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"office-nas-01"}' ``` **Response** · 200 ```json { "id": 15, "organization_id": 1, "provider": "self_managed", "provider_server_id": null, "name": "office-nas-01", "status": "running", "server_type": null, "image": null, "ipv4": "192.0.2.40", "ipv6": null, "datacenter": null, "provisioned_at": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": null, "provider_credential_id": null, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": false, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "provider", "agent_health": null, "monitoring_enabled": false, "monitoring_enabled_at": null, "volumes_count": 0, "hardware": { "vcpus": null, "ram_gb": null, "disk_gb": null, "plan": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "volumes": [], "tags": [], "provider_credential": null, "cost": { "base": null, "currency": "EUR", "source": "api", "extras": [], "total": null }, "capabilities": [], "capability_blockers": [], "rename_refusal": null, "agent_silent_after_seconds": 300, "agent_refusal": null, "uptime_seconds": null } ```
--- ### List a server’s add-ons {#get-organizations-organization-cloud-servers-cloudserver-addons}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/addons` **Scope:** `cloud.read` · **Changes data:** no The add-ons the cloud provider reports for this server, asked live. Empty when the provider account’s access does not cover it; refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/addons" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Get what refers to a server’s addresses {#get-organizations-organization-cloud-servers-cloudserver-address-references}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/address-references` **Scope:** `cloud.read` · **Changes data:** no The DNS records, firewall rules and monitors that name this server’s IPv4 or IPv6 address, with a total. Also lists the other servers a DNS record could be moved to, which is useful before deleting the server. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/address-references" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "addresses": [ "198.51.100.10", "2001:db8:1c1e:5a10::/64" ], "dns_records": [ { "zone_id": 1, "zone_name": "northwind.com", "record_id": 1, "name": "northwind.com", "fqdn": "northwind.com", "type": "A", "value": "198.51.100.10", "writable": false, "aliases": [ { "zone_id": 1, "record_id": 3, "fqdn": "api.northwind.com" } ] }, { "zone_id": 1, "zone_name": "northwind.com", "record_id": 2, "name": "www", "fqdn": "www.northwind.com", "type": "A", "value": "198.51.100.10", "writable": false, "aliases": [] } ], "firewall_rules": [], "monitors": [], "total": 21, "move_targets": [ { "id": 13, "name": "analytics-01", "ipv4": "192.0.2.80", "ipv6": null, "ipv6_basis": null }, { "id": 3, "name": "api-01", "ipv4": "198.51.100.20", "ipv6": "2001:db8:1c1e:5a20::1", "ipv6_basis": "default" } ] } ```
--- ### Get the latest monitoring agent release {#get-organizations-organization-cloud-servers-cloudserver-agent-release}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/agent-release` **Scope:** `cloud.read` · **Changes data:** no The current release of the InfraNest monitoring agent with its checksums, the install script URLs and the address the agent should report to. The answer is the same for every server. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/agent-release" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "version": null, "published_at": null, "install_sh_sha256": null, "install_ps1_sha256": null, "repo": "InfraNest-Infrastructure-Organized/infranest-agent", "install_sh_url": "https://get.infranest.io/agent.sh", "install_ps1_url": "https://get.infranest.io/agent.ps1", "checksums_url": "https://github.com/InfraNest-Infrastructure-Organized/infranest-agent/releases/latest/download/SHA256SUMS", "releases_url": "https://github.com/InfraNest-Infrastructure-Organized/infranest-agent/releases", "ingest_url": "http://localhost" } ```
--- ### Get a server’s agent snapshot {#get-organizations-organization-cloud-servers-cloudserver-agent-snapshot}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/agent-snapshot` **Scope:** `cloud.read` · **Changes data:** no The latest state the monitoring agent reported from inside the server: disk mounts with forecasts, top processes, watched services, containers and ignored units. Also includes the agent’s own health, version status and whether alerts are currently quiet. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/agent-snapshot" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "container_names": [], "mounts": [ { "mount_point": "/", "device": "/dev/sda1", "used_bytes": 58411555225, "total_bytes": 171798691840, "used_percent": 34, "inodes_percent": null, "collected_at": "2026-01-15T12:00:00.000000Z", "usage": null, "scan": null, "forecast": { "days_left": 61, "percent_per_day": 1.08, "fit": 0.99, "from_percent": 34 } } ], "processes": [], "services": [], "services_summary": { "watched": 0, "active": 0, "failed": 0, "ignored": 0 }, "ignored_units": [], "containers": null, "collected_at": "2026-01-15T12:00:00.000000Z", "metrics_source": "agent", "uptime_seconds": 3542400, "agent_health": null, "agent_status": "unknown", "agent_latest_version": null, "has_token": false, "alerts_quiet_until": null } ```
--- ### List a server’s agent tokens {#get-organizations-organization-cloud-servers-cloudserver-agent-tokens}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/agent-tokens` **Scope:** `cloud.read` · **Changes data:** no The tokens the monitoring agent on this server can report with, newest first, with who created each and when it was last used. The token values themselves are never returned. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/agent-tokens" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
--- ### Override an alert rule for a server {#put-organizations-organization-cloud-servers-cloudserver-alert-rule-overrides}
`PUT` `/organizations/{organization}/cloud-servers/{cloudServer}/alert-rule-overrides` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets this server’s own version of an organization alert, identified by `default_id` or a catalogue `key`, with any of `operator`, `threshold`, `duration_minutes` and `is_active`. Send `is_active: false` to stop watching it on this server only; anything you leave out is copied from the organization default. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `default_id`
body | integer | This field is required when key is not present. | | `key`
body | string | This field is required when default_id is not present. | | `operator`
body | string | One of: `>`, `<`. | | `threshold`
body | number | | | `duration_minutes`
body | string | The widest range any kind allows; the kind's own bounds are checked once it is known. | | `is_active`
body | boolean | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/cloud-servers/1/alert-rule-overrides" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"key":"cpu_percent","threshold":85}' ``` **Response** · 201 — Created ```json { "id": 2, "organization_id": 1, "server_id": 1, "metric": "cpu_percent", "operator": ">", "threshold": 85, "duration_minutes": 15, "is_active": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "alert_rule_default_id": 1, "kind": "metric" } ```
--- ### List a server’s alert rules {#get-organizations-organization-cloud-servers-cloudserver-alert-rules}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/alert-rules` **Scope:** `cloud.read` · **Changes data:** no Every alert in the catalogue for this server, each marked as inherited from the organization, overridden here or not configured, plus any older standalone rules. With `active=1` it returns only the rules actually being evaluated, as a flat list. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/alert-rules" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "key": "cpu_percent", "group": "struggling", "kind": "metric", "metric": "cpu_percent", "agent_only": false, "duration_unit": "minutes", "duration_bounds": { "min": 1, "max": 1440 }, "recommended": { "operator": ">", "threshold": 90, "duration_minutes": 10 }, "origin": "inherited", "configured": true, "default_active": true, "is_active": true, "operator": ">", "threshold": 85, "duration_minutes": 15, "default_id": 1, "override_id": null, "inherited_threshold": null, "inherited_duration_minutes": null, "is_breaching": false, "advisory": false, "in_breach_since": null, "subject": null }, { "key": "memory_percent", "group": "struggling", "kind": "metric", "metric": "memory_percent", "agent_only": true, "duration_unit": "minutes", "duration_bounds": { "min": 1, "max": 1440 }, "recommended": { "operator": ">", "threshold": 90, "duration_minutes": 10 }, "origin": "unconfigured", "configured": false, "default_active": false, "is_active": false, "operator": ">", "threshold": 90, "duration_minutes": 10, "default_id": null, "override_id": null, "inherited_threshold": null, "inherited_duration_minutes": null, "is_breaching": false, "advisory": false, "in_breach_since": null, "subject": null } ], "legacy": [], "min_duration_minutes": 1 } ```
--- ### Update a server alert rule {#patch-organizations-organization-cloud-servers-cloudserver-alert-rules-alertrule}
`PATCH` `/organizations/{organization}/cloud-servers/{cloudServer}/alert-rules/{alertRule}` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes the `operator`, `threshold`, `duration_minutes` or `is_active` of one of this server’s rules. Changing the operator or threshold clears any breach the rule was tracking; durations outside the allowed range, or shorter than your plan allows, are refused. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `alertRule_id`
path · required | integer | The alertRule's ID. | | `operator`
body | string | One of: `>`, `<`. | | `threshold`
body | number | | | `duration_minutes`
body | string | The widest range any kind allows; the kind's own bounds are checked once it is known. | | `is_active`
body | boolean | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/cloud-servers/1/alert-rules/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"threshold":85}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "server_id": 1, "metric": "cpu_percent", "operator": ">", "threshold": 85, "duration_minutes": 10, "is_active": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "alert_rule_default_id": null, "kind": "metric" } ```
--- ### Reset or delete a server alert rule {#delete-organizations-organization-cloud-servers-cloudserver-alert-rules-alertrule}
`DELETE` `/organizations/{organization}/cloud-servers/{cloudServer}/alert-rules/{alertRule}` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes the rule from this server. For an override, the organization’s default applies again; an older standalone rule is simply deleted. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `alertRule_id`
path · required | integer | The alertRule's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/cloud-servers/1/alert-rules/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Deleted." } ```
--- ### Get what needs attention on a server {#get-organizations-organization-cloud-servers-cloudserver-attention}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/attention` **Scope:** `cloud.read` · **Changes data:** no A short summary of the server: its uptime, whether alerts are quiet and until when, and the metric trend closest to crossing its alert threshold. Fields are null when there is nothing to say. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/attention" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "uptime_seconds": 3542400, "alerts_quiet_until": null, "silence": null, "trend": { "metric": "disk_used_percent", "mount_point": "/", "current": 34, "per_day": 1.08, "days_left": 61, "threshold": null, "also_moving": 0, "window_days": 7 }, "others_ok": true, "certificates": [] } ```
--- ### Set a server’s backup price {#put-organizations-organization-cloud-servers-cloudserver-backup-pricing}
`PUT` `/organizations/{organization}/cloud-servers/{cloudServer}/backup-pricing` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Overrides the monthly backup cost InfraNest counts for this server with `backup_price_monthly`, net of tax. Send `null` to go back to the provider’s price; the server’s base price is not affected. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `backup_price_monthly`
body | number, nullable | Must be at least 0. Must not be greater than 9999999.99. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/cloud-servers/1/backup-pricing" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"backup_price_monthly":1.58}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_server_id": "100", "name": "web-01", "status": "running", "server_type": "cpx31", "image": "ubuntu-24.04", "ipv4": "198.51.100.10", "ipv6": "2001:db8:1c1e:5a10::/64", "datacenter": "nbg1-dc3", "provisioned_at": "2025-12-16T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "15.90", "price_hourly": "0.0218", "location_city": "Nuremberg", "location_country": "DE", "included_traffic": 21990232555520, "outgoing_traffic": 6816972092211, "incoming_traffic": 1979120929997, "ipv4_dns_ptr": "web-01.northwind.cloud", "firewall_enabled": false, "backup_window": "22-02" }, "provider_credential_id": 1, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": "1.58", "delete_protection": true, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "agent_health": null, "monitoring_enabled": true, "monitoring_enabled_at": null, "volumes_count": 1, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "volumes": [ { "id": 3, "organization_id": 1, "cloud_server_id": 1, "provider": "hetzner", "provider_volume_id": "vol-3", "name": "uploads", "size_gb": 250, "status": "attached", "datacenter": "nbg1-dc3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "price_monthly": "12.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 5 } } ], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "cost": { "base": 15.9, "currency": "EUR", "source": "api", "extras": [ { "key": "backups", "amount": 1.58, "source": "manual", "own": true }, { "key": "volume", "label": "uploads", "amount": 12, "source": "api", "own": false } ], "total": 30.15 }, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "capability_blockers": [], "rename_refusal": null, "agent_silent_after_seconds": 300, "agent_refusal": null, "uptime_seconds": 3542400 } ```
--- ### List a server’s backups {#get-organizations-organization-cloud-servers-cloudserver-backups}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/backups` **Scope:** `cloud.read` · **Changes data:** no The backups the cloud provider holds for this server, asked live. Empty when the provider account’s access does not cover it; refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/backups" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "images": [ { "id": 90000101, "type": "backup", "status": "available", "description": "web-01 backup", "created": "2026-01-14T22:06:00+00:00", "disk_size": 160, "image_size": 17.84, "protection": { "delete": false }, "bound_to": 100 }, { "id": 90000102, "type": "backup", "status": "available", "description": "web-01 backup", "created": "2026-01-13T22:06:00+00:00", "disk_size": 160, "image_size": 17.8, "protection": { "delete": false }, "bound_to": 100 } ] } ```
--- ### Name a container {#put-organizations-organization-cloud-servers-cloudserver-container-names}
`PUT` `/organizations/{organization}/cloud-servers/{cloudServer}/container-names` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Gives one of the server’s reported containers a display name in InfraNest, identified by `container_id`. Returns the server’s full list of saved container names; each server has a cap on saved names. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `container_id`
body · required | string | Must match the regex /\A[0-9a-f]{12,64}\z/. | | `display_name`
body · required | string | Must match the regex /\A[^\p{C}]+\z/u. Must not be greater than 100 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/cloud-servers/1/container-names" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"container_id":"3f2a9c1b7d4e","display_name":"web"}' ``` **Response** · 200 ```json { "container_names": [ { "container_id": "3f2a9c1b7d4e", "display_name": "web" } ] } ```
--- ### Remove a container name {#delete-organizations-organization-cloud-servers-cloudserver-container-names}
`DELETE` `/organizations/{organization}/cloud-servers/{cloudServer}/container-names` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes the display name saved for the container given in `container_id`. Returns the server’s remaining saved container names. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `container_id`
body · required | string | Must match the regex /\A[0-9a-f]{12,64}\z/. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/cloud-servers/1/container-names" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "container_names": [] } ```
--- ### Get a server’s own firewall {#get-organizations-organization-cloud-servers-cloudserver-firewall}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/firewall` **Scope:** `cloud.read` · **Changes data:** no The firewall attached to this server, read live from the cloud provider. Refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/firewall" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Ignore a failed service {#post-organizations-organization-cloud-servers-cloudserver-ignored-units}
`POST` `/organizations/{organization}/cloud-servers/{cloudServer}/ignored-units` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Stops a failed system service, named in `unit`, from raising alerts on this server, without silencing other service failures. Any name is accepted; returns the server’s list of ignored units. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cloud-servers/1/ignored-units" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"unit":"snapd.service"}' ``` **Response** · 201 — Created ```json { "ignored_units": [ { "unit": "snapd.service", "ignored_at": "2026-01-15T12:00:00+00:00", "ignored_by": "Alex Rivera" } ] } ```
--- ### Stop ignoring a failed service {#delete-organizations-organization-cloud-servers-cloudserver-ignored-units}
`DELETE` `/organizations/{organization}/cloud-servers/{cloudServer}/ignored-units` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Lets the service named in `unit` raise alerts again on this server; if it is still failed, it counts from now. Returns the remaining ignored units. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/cloud-servers/1/ignored-units" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "ignored_units": [] } ```
--- ### List images available to a server {#get-organizations-organization-cloud-servers-cloudserver-images}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/images` **Scope:** `cloud.read` · **Changes data:** no The operating system images the cloud provider offers for this server, under `images`. Empty when the provider account’s access does not cover the catalogue; refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/images" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### List ISOs available to a server {#get-organizations-organization-cloud-servers-cloudserver-isos}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/isos` **Scope:** `cloud.read` · **Changes data:** no The ISO images the cloud provider offers for this server’s type, under `isos`. Refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/isos" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Get a server’s stored metric series {#get-organizations-organization-cloud-servers-cloudserver-metric-series}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/metric-series` **Scope:** `cloud.read` · **Changes data:** no Time series InfraNest has stored for this server, as `[unix seconds, value]` pairs, oldest first, with the alert threshold that applies to each metric. Pick a comma-separated `metrics` list, then either a fixed `range` (`live`, `h1`, `h12`, `h24`, `d7`, `d30`; default `h24`) or your own window with `from` and `to` (ISO 8601) and optionally `resolution`: `raw` for every sample (kept for 48 hours, at most 48 hours per request) or `hour` for hourly averages (at most 92 days per request). Without `resolution`, raw samples are used when they still cover the window. A longer range is refused with `range_too_large`. Points are paged with `page` and `per_page` (up to 10,000, the default); `pagination` says how many points the window holds and `window` the range that was applied. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `metrics`
query | string | Comma-separated metrics. Default `cpu_percent,memory_percent,worst_mount_percent,load_1`. | | `range`
query | string | A fixed window: `live`, `h1`, `h12`, `h24`, `d7` or `d30`. Default `h24`. | | `from`
query | string | Start of your own window, ISO 8601, instead of `range`. | | `to`
query | string | End of your own window, ISO 8601; default now. | | `resolution`
query | string | `raw` or `hour`; default: raw when the samples still cover the window. | | `per_page`
query | integer | Points per page, 1 to 10,000. Default 10,000. | | `page`
query | integer | Page number, from 1. | | `range`
body | string | | | `metrics`
body | string | | | `page`
body | integer | Must be at least 1. | | `per_page`
body | string | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/metric-series" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "range": "h24", "window": { "from": "2026-01-14T12:00:00Z", "to": "2026-01-15T12:00:00Z" }, "source": "agent", "resolution": "raw", "series": { "cpu_percent": [ [ 1768392000, 24.25 ], [ 1768392300, 23.23 ] ], "memory_percent": [ [ 1768392000, 62.8 ], [ 1768392300, 62.43 ] ], "worst_mount_percent": [ [ 1768392000, 33 ], [ 1768392300, 33 ] ], "load_1": [ [ 1768392000, 0.97 ], [ 1768392300, 0.93 ] ] }, "pagination": { "page": 1, "per_page": 10000, "total": 289, "last_page": 1 }, "thresholds": { "cpu_percent": { "operator": ">", "threshold": 85, "duration_minutes": 15 }, "memory_percent": null, "worst_mount_percent": null, "load_1": null }, "facts": { "vcpus": 4, "worst_mount": { "mount_point": "/", "used_bytes": 58411555225, "total_bytes": 171798691840, "days_left": 61 } } } ```
--- ### Get a server’s provider metrics {#get-organizations-organization-cloud-servers-cloudserver-metrics}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/metrics` **Scope:** `cloud.read` · **Changes data:** no CPU, disk and network figures asked live from the cloud provider. Pick the figures with `types` (default `cpu,disk,network`) and set the window with `start`, `end` and `step` in seconds (default: the last hour at 60 seconds); refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/metrics" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "metrics": { "start": "2026-01-15T11:00:00+00:00", "end": "2026-01-15T12:00:00+00:00", "step": 60, "time_series": { "cpu": { "values": [ [ 1768474800, "22.12" ], [ 1768475100, "23.11" ] ] }, "network.0.bandwidth.in": { "values": [ [ 1768474800, "16381032" ], [ 1768475100, "18508096" ] ] }, "network.0.bandwidth.out": { "values": [ [ 1768474800, "38222409" ], [ 1768475100, "43185557" ] ] }, "disk.0.bandwidth.read": { "values": [ [ 1768474800, "5460344" ], [ 1768475100, "6169365" ] ] }, "disk.0.bandwidth.write": { "values": [ [ 1768474800, "8190516" ], [ 1768475100, "9254048" ] ] } } } } ```
--- ### Turn on monitoring for a server {#post-organizations-organization-cloud-servers-cloudserver-monitoring}
`POST` `/organizations/{organization}/cloud-servers/{cloudServer}/monitoring` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Starts monitoring the server in InfraNest and returns how many monitored servers your plan allows and uses. Refused when you have reached that limit; repeating it on a monitored server changes nothing. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cloud-servers/1/monitoring" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "monitoring_enabled": true, "monitoring_enabled_at": null, "usage": 11, "limit": null } ```
--- ### Turn off monitoring for a server {#delete-organizations-organization-cloud-servers-cloudserver-monitoring}
`DELETE` `/organizations/{organization}/cloud-servers/{cloudServer}/monitoring` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Stops monitoring the server and frees its place in your plan’s monitored-server count. The server itself is not affected. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/cloud-servers/1/monitoring" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "monitoring_enabled": false, "monitoring_enabled_at": null, "usage": 10, "limit": null } ```
--- ### Set a server’s price {#put-organizations-organization-cloud-servers-cloudserver-pricing}
`PUT` `/organizations/{organization}/cloud-servers/{cloudServer}/pricing` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets a manual price for the server in InfraNest, with optional currency and billing interval. A manual price is no longer overwritten by the provider’s price at each sync; nothing changes at the provider. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/cloud-servers/1/pricing" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"price_monthly":7.9,"currency":"EUR"}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_server_id": "100", "name": "web-01", "status": "running", "server_type": "cpx31", "image": "ubuntu-24.04", "ipv4": "198.51.100.10", "ipv6": "2001:db8:1c1e:5a10::/64", "datacenter": "nbg1-dc3", "provisioned_at": "2025-12-16T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "15.90", "price_hourly": "0.0218", "location_city": "Nuremberg", "location_country": "DE", "included_traffic": 21990232555520, "outgoing_traffic": 6816972092211, "incoming_traffic": 1979120929997, "ipv4_dns_ptr": "web-01.northwind.cloud", "firewall_enabled": false, "backup_window": "22-02" }, "provider_credential_id": 1, "price_monthly": "7.90", "price_source": "manual", "price_currency": null, "backup_price_override": null, "delete_protection": true, "untracked_at": null, "price_amount": "7.9000", "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "agent_health": null, "monitoring_enabled": true, "monitoring_enabled_at": null, "volumes_count": 1, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "volumes": [ { "id": 3, "organization_id": 1, "cloud_server_id": 1, "provider": "hetzner", "provider_volume_id": "vol-3", "name": "uploads", "size_gb": 250, "status": "attached", "datacenter": "nbg1-dc3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "price_monthly": "12.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 5 } } ], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "cost": { "base": 7.9, "currency": "EUR", "source": "manual", "extras": [ { "key": "backups", "amount": 1.58, "source": "computed", "own": true }, { "key": "volume", "label": "uploads", "amount": 12, "source": "api", "own": false } ], "total": 22.15 }, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "capability_blockers": [], "rename_refusal": null, "agent_silent_after_seconds": 300, "agent_refusal": null, "uptime_seconds": 3542400 } ```
--- ### List a server’s provider actions {#get-organizations-organization-cloud-servers-cloudserver-provider-actions}
`GET` `/organizations/{organization}/cloud-servers/{cloudServer}/provider-actions` **Scope:** `cloud.read` · **Changes data:** no The recent actions the cloud provider reports for this server, asked live. Empty when the provider account’s access does not cover it; refused when the server has no linked provider account. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/1/provider-actions" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Reset a server’s price {#post-organizations-organization-cloud-servers-cloudserver-reset-pricing}
`POST` `/organizations/{organization}/cloud-servers/{cloudServer}/reset-pricing` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Drops the manual price and goes back to the price last synced from the provider. Returns the server with its updated cost breakdown. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cloud-servers/1/reset-pricing" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_server_id": "100", "name": "web-01", "status": "running", "server_type": "cpx31", "image": "ubuntu-24.04", "ipv4": "198.51.100.10", "ipv6": "2001:db8:1c1e:5a10::/64", "datacenter": "nbg1-dc3", "provisioned_at": "2025-12-16T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "15.90", "price_hourly": "0.0218", "location_city": "Nuremberg", "location_country": "DE", "included_traffic": 21990232555520, "outgoing_traffic": 6816972092211, "incoming_traffic": 1979120929997, "ipv4_dns_ptr": "web-01.northwind.cloud", "firewall_enabled": false, "backup_window": "22-02" }, "provider_credential_id": 1, "price_monthly": "15.90", "price_source": "api", "price_currency": "EUR", "backup_price_override": null, "delete_protection": true, "untracked_at": null, "price_amount": "15.9000", "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "agent_health": null, "monitoring_enabled": true, "monitoring_enabled_at": null, "volumes_count": 1, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "volumes": [ { "id": 3, "organization_id": 1, "cloud_server_id": 1, "provider": "hetzner", "provider_volume_id": "vol-3", "name": "uploads", "size_gb": 250, "status": "attached", "datacenter": "nbg1-dc3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "price_monthly": "12.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 5 } } ], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "cost": { "base": 15.9, "currency": "EUR", "source": "api", "extras": [ { "key": "backups", "amount": 3.18, "source": "computed", "own": true }, { "key": "volume", "label": "uploads", "amount": 12, "source": "api", "own": false } ], "total": 31.75 }, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "capability_blockers": [], "rename_refusal": null, "agent_silent_after_seconds": 300, "agent_refusal": null, "uptime_seconds": 3542400 } ```
--- ### Snooze a server’s alerts {#post-organizations-organization-cloud-servers-cloudserver-snooze}
`POST` `/organizations/{organization}/cloud-servers/{cloudServer}/snooze` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Silences the server’s alerts for `minutes` (5 to 1440) by creating a short maintenance window, and returns when it ends. Refused when your plan does not include maintenance windows. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | | `minutes`
body | integer | Capped at a day. Beyond that somebody wants a scheduled window with a name and a reason, which is the full form — a snooze that can run for a week is a way to switch monitoring off and forget you did. Must be at least 5. Must not be greater than 1440. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cloud-servers/1/snooze" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"minutes":60}' ``` **Response** · 201 — Created ```json { "maintenance_window_id": 5, "until": "2026-01-15T13:00:00+00:00" } ```
--- ### End a server’s snooze {#delete-organizations-organization-cloud-servers-cloudserver-snooze}
`DELETE` `/organizations/{organization}/cloud-servers/{cloudServer}/snooze` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Ends a snooze before it runs out. Scheduled maintenance windows are left alone, so the response says whether anything was ended and until when alerts are still quiet. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/cloud-servers/1/snooze" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "ended": 0, "alerts_quiet_until": null } ```
--- ### Stop tracking a server {#post-organizations-organization-cloud-servers-cloudserver-untrack}
`POST` `/organizations/{organization}/cloud-servers/{cloudServer}/untrack` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Hides the server from every list, cost and alert in InfraNest without touching it at the provider, where it keeps running. It no longer counts towards your plan, and tracking it again restores its history. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudServer_id`
path · required | integer | The cloudServer's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cloud-servers/1/untrack" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Server is no longer tracked." } ```
--- ### Track a server again {#post-organizations-organization-cloud-servers-serverid-retrack}
`POST` `/organizations/{organization}/cloud-servers/{serverId}/retrack` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Brings an untracked server back into lists, costs and alerts, with its history, tags and alert rules intact. It counts towards your plan again, so it is refused when you have reached your server limit. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `serverId`
path · required | string | The serverId. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cloud-servers/1/retrack" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider": "hetzner", "provider_server_id": "100", "name": "web-01", "status": "running", "server_type": "cpx31", "image": "ubuntu-24.04", "ipv4": "198.51.100.10", "ipv6": "2001:db8:1c1e:5a10::/64", "datacenter": "nbg1-dc3", "provisioned_at": "2025-12-16T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "15.90", "price_hourly": "0.0218", "location_city": "Nuremberg", "location_country": "DE", "included_traffic": 21990232555520, "outgoing_traffic": 6816972092211, "incoming_traffic": 1979120929997, "ipv4_dns_ptr": "web-01.northwind.cloud", "firewall_enabled": false, "backup_window": "22-02" }, "provider_credential_id": 1, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": true, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "agent_health": null, "monitoring_enabled": true, "monitoring_enabled_at": null, "volumes_count": 1, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "volumes": [ { "id": 3, "organization_id": 1, "cloud_server_id": 1, "provider": "hetzner", "provider_volume_id": "vol-3", "name": "uploads", "size_gb": 250, "status": "attached", "datacenter": "nbg1-dc3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "price_monthly": "12.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 3 } }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 1, "tag_id": 5 } } ], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "cost": { "base": 15.9, "currency": "EUR", "source": "api", "extras": [ { "key": "backups", "amount": 3.18, "source": "computed", "own": true }, { "key": "volume", "label": "uploads", "amount": 12, "source": "api", "own": false } ], "total": 31.75 }, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "capability_blockers": [], "rename_refusal": null, "agent_silent_after_seconds": 300, "agent_refusal": null, "uptime_seconds": 3542400 } ```
--- ### List servers with their latest metrics {#get-organizations-organization-cloud-servers-metrics-overview}
`GET` `/organizations/{organization}/cloud-servers/metrics-overview` **Scope:** `cloud.read` · **Changes data:** no Every server in the organization with its latest CPU, memory, disk, load and network readings, its alert-rule counts and whether it is breaching, a page at a time. Monitored servers come first; filter with `search`, `credential`, `tag` or `status` (for example `breaching`, `norules`, `notreporting`, `monitored`). | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-servers/metrics-overview" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 12, "organization_id": 1, "provider": "digitalocean", "provider_server_id": "111", "name": "queue-01", "status": "running", "server_type": "s-4vcpu-8gb", "image": "ubuntu-24-04-x64", "ipv4": "192.0.2.40", "ipv6": "2001:db8:3:d0::a22:1/64", "datacenter": "fra1", "provisioned_at": "2025-08-06T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "price_monthly": "48.00", "price_hourly": "0.0658", "location_city": "Frankfurt", "location_country": "DE", "included_traffic": 5497558138880, "outgoing_traffic": 1044536046387, "incoming_traffic": 384829069722, "ipv4_dns_ptr": "queue-01.northwind.cloud", "firewall_enabled": false }, "provider_credential_id": 9, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": false, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "agent", "monitoring_enabled": true, "monitoring_enabled_at": null, "breached_rules": 0, "cpu_percent": 64.03, "network_in_bps": 14121908, "network_out_bps": 10591431, "memory_percent": 67.71, "worst_mount_percent": 55, "load_1": 2.56, "load_5": 2.49, "load_15": 2.42, "collected_at": "2026-01-13 12:05:00", "agent_last_seen_at": null, "agent_tokens_count": 0, "silence": null, "active_rules": 1, "in_breach": false, "coverage": "agent", "provider_metrics": true, "worst_mount_point": "/", "disk_forecast": { "mount_point": "/", "days_left": 39, "percent_per_day": 1.08, "fit": 0.99, "from_percent": 57 }, "load_24h": [ 2.28, 2.11 ], "agent_version": null, "agent_status": "unknown", "agent_refusal": null, "hardware": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160, "plan": { "vcpus": 4, "ram_gb": 8, "disk_gb": 160 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "tags": [ { "id": 1, "name": "Production", "color": "#ef4444", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 12, "tag_id": 1 } } ] }, { "id": 10, "organization_id": 1, "provider": "digitalocean", "provider_server_id": "109", "name": "staging-web-01", "status": "running", "server_type": "s-2vcpu-4gb", "image": "ubuntu-24-04-x64", "ipv4": "192.0.2.10", "ipv6": "2001:db8:800:c1::a10:1/64", "datacenter": "ams3", "provisioned_at": "2025-08-30T12:00:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "meta": { "vcpus": 2, "ram_gb": 4, "disk_gb": 80, "price_monthly": "24.00", "price_hourly": "0.0329", "location_city": "Amsterdam", "location_country": "NL", "included_traffic": 4398046511104, "outgoing_traffic": 483785116221, "incoming_traffic": 219902325555, "ipv4_dns_ptr": "staging-web-01.northwind.cloud", "firewall_enabled": false }, "provider_credential_id": 9, "price_monthly": null, "price_source": "api", "price_currency": null, "backup_price_override": null, "delete_protection": false, "untracked_at": null, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "metrics_source": "provider", "monitoring_enabled": true, "monitoring_enabled_at": null, "breached_rules": 0, "cpu_percent": 8.67, "network_in_bps": 3530477, "network_out_bps": 7060954, "memory_percent": null, "worst_mount_percent": null, "load_1": null, "load_5": null, "load_15": null, "collected_at": "2026-01-13 12:05:00", "agent_last_seen_at": null, "agent_tokens_count": 0, "silence": null, "active_rules": 1, "in_breach": false, "coverage": "provider", "provider_metrics": true, "worst_mount_point": null, "disk_forecast": null, "load_24h": null, "agent_version": null, "agent_status": "unknown", "agent_refusal": null, "hardware": { "vcpus": 2, "ram_gb": 4, "disk_gb": 80, "plan": { "vcpus": 2, "ram_gb": 4, "disk_gb": 80 }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": "running", "sync_halt": null, "tags": [ { "id": 2, "name": "Staging", "color": "#f59e0b", "pivot": { "taggable_type": "App\\Models\\CloudServer", "taggable_id": 10, "tag_id": 2 } } ] } ], "first_page_url": "http://localhost/api/organizations/1/cloud-servers/metrics-overview?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/cloud-servers/metrics-overview?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/cloud-servers/metrics-overview?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/cloud-servers/metrics-overview", "per_page": 25, "prev_page_url": null, "to": 14, "total": 14, "meta": { "counts": { "total": 14, "breaching": 0, "noagent": 9, "notreporting": 4, "ok": 10, "norules": 0, "notmonitored": 3, "monitored": 11, "avgCpu": 29 }, "agent_silent_after_seconds": 300, "counted_at": "2026-01-15T12:00:00.000000Z", "integrations": [ { "id": 9, "provider": "digitalocean", "label": "DigitalOcean — Staging" }, { "id": 1, "provider": "hetzner", "label": "Hetzner — Production" } ], "tags": [ { "id": 3, "name": "Customer-facing", "color": "#3b82f6" }, { "id": 6, "name": "Northwind Platform", "color": "#14b8a6" } ] } } ```
## Cloud snapshots ### List snapshots {#get-organizations-organization-cloud-snapshots}
`GET` `/organizations/{organization}/cloud-snapshots` **Scope:** `cloud.read` · **Changes data:** no Every server snapshot InfraNest tracks in the organization, a page at a time, with the server it belongs to. Filter with `server_id`, `status`, `credential` or `search`, and sort with `sort` and `dir`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-snapshots" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 3, "organization_id": 1, "provider_credential_id": null, "cloud_server_id": 1, "provider": "hetzner", "provider_snapshot_id": "snap-302", "name": "web-01 golden image", "description": "Base image the web tier is rebuilt from.", "status": "available", "disk_size_gb": 80, "image_size_gb": 6.2, "source_server_name": "web-01", "protection_delete": true, "created_at": "2025-12-12T12:00:00.000000Z", "updated_at": "2025-12-12T12:00:00.000000Z", "deleted_at": null, "price_monthly": "0.07", "price_amount": null, "price_currency": "EUR", "price_source": "api", "billing_interval": "monthly", "interval_count": 1, "capabilities": [], "provider_credential": null, "server": { "id": 1, "name": "web-01", "ipv4": "198.51.100.10", "status": "running", "hardware": { "vcpus": null, "ram_gb": null, "disk_gb": null, "plan": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": null, "sync_halt": null } }, { "id": 2, "organization_id": 1, "provider_credential_id": null, "cloud_server_id": 4, "provider": "hetzner", "provider_snapshot_id": "snap-301", "name": "db-01 nightly", "description": null, "status": "available", "disk_size_gb": 40, "image_size_gb": 11.1, "source_server_name": "db-01", "protection_delete": false, "created_at": "2026-01-14T12:00:00.000000Z", "updated_at": "2026-01-14T12:00:00.000000Z", "deleted_at": null, "price_monthly": "0.13", "price_amount": null, "price_currency": "EUR", "price_source": "api", "billing_interval": "monthly", "interval_count": 1, "capabilities": [], "provider_credential": null, "server": { "id": 4, "name": "db-01", "ipv4": "198.51.100.30", "status": "running", "hardware": { "vcpus": null, "ram_gb": null, "disk_gb": null, "plan": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": null, "sync_halt": null } } ], "first_page_url": "http://localhost/api/organizations/1/cloud-snapshots?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/cloud-snapshots?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/cloud-snapshots?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/cloud-snapshots", "per_page": 25, "prev_page_url": null, "to": 6, "total": 6, "meta": { "counts": { "total": 6, "available": 6, "creating": 0, "unavailable": 0 }, "integrations": [] } } ```
--- ### Get a snapshot {#get-organizations-organization-cloud-snapshots-cloudsnapshot}
`GET` `/organizations/{organization}/cloud-snapshots/{cloudSnapshot}` **Scope:** `cloud.read` · **Changes data:** no One snapshot with the server it was taken from. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudSnapshot_id`
path · required | integer | The cloudSnapshot's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-snapshots/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "provider_credential_id": null, "cloud_server_id": 4, "provider": "hetzner", "provider_snapshot_id": "snap-300", "name": "db-01 before schema migration", "description": "Taken automatically before the 2026-08 migration.", "status": "available", "disk_size_gb": 40, "image_size_gb": 11.4, "source_server_name": "db-01", "protection_delete": true, "created_at": "2026-01-06T12:00:00.000000Z", "updated_at": "2026-01-06T12:00:00.000000Z", "deleted_at": null, "price_monthly": "0.14", "price_amount": null, "price_currency": "EUR", "price_source": "api", "billing_interval": "monthly", "interval_count": 1, "provider_credential": null, "server": { "id": 4, "name": "db-01", "ipv4": "198.51.100.30", "status": "running", "hardware": { "vcpus": null, "ram_gb": null, "disk_gb": null, "plan": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": null, "sync_halt": null } } ```
## Cloud volumes ### List volumes {#get-organizations-organization-cloud-volumes}
`GET` `/organizations/{organization}/cloud-volumes` **Scope:** `cloud.read` · **Changes data:** no Every storage volume InfraNest tracks in the organization, a page at a time, with the server it is attached to. Filter with `search`, `attached`, `unattached`, `server_id`, `status`, `tag` or `credential`, and sort with `sort` and `dir`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-volumes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 6, "organization_id": 1, "cloud_server_id": null, "provider": "digitalocean", "provider_volume_id": "vol-6", "name": "analytics-scratch", "size_gb": 300, "status": "available", "datacenter": "nyc3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 9, "price_monthly": "30.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null, "server": null, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] }, { "id": 5, "organization_id": 1, "cloud_server_id": 11, "provider": "digitalocean", "provider_volume_id": "vol-5", "name": "staging-data", "size_gb": 100, "status": "attached", "datacenter": "ams3", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 9, "price_monthly": "10.00", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null, "server": { "id": 11, "name": "staging-api-01", "ipv4": "192.0.2.11", "status": "running", "hardware": { "vcpus": null, "ram_gb": null, "disk_gb": null, "plan": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": null, "sync_halt": null }, "provider_credential": { "id": 9, "organization_id": 1, "provider": "digitalocean", "label": "DigitalOcean — Staging", "enabled": true, "verified_at": "2025-10-15T12:00:00.000000Z", "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "refused_at": null }, "tags": [] } ], "first_page_url": "http://localhost/api/organizations/1/cloud-volumes?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/cloud-volumes?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/cloud-volumes?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/cloud-volumes", "per_page": 25, "prev_page_url": null, "to": 6, "total": 6, "meta": { "counts": { "total": 6, "attached": 4, "unattached": 2 }, "integrations": [ { "id": 9, "provider": "digitalocean", "label": "DigitalOcean — Staging" }, { "id": 1, "provider": "hetzner", "label": "Hetzner — Production" } ], "tags": [] } } ```
--- ### Get a volume {#get-organizations-organization-cloud-volumes-cloudvolume}
`GET` `/organizations/{organization}/cloud-volumes/{cloudVolume}` **Scope:** `cloud.read` · **Changes data:** no One volume with its server, tags, what its provider allows and the provider’s size limits where known. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `cloudVolume_id`
path · required | integer | The cloudVolume's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cloud-volumes/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "cloud_server_id": 4, "provider": "hetzner", "provider_volume_id": "vol-1", "name": "postgres-data", "size_gb": 100, "status": "attached", "datacenter": "hel1-dc2", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "provider_credential_id": 1, "price_monthly": "4.80", "price_source": "api", "price_currency": "eur", "delete_protection": false, "price_amount": null, "billing_interval": "monthly", "interval_count": 1, "native_delete_protection": false, "sync_halt": null, "server": { "id": 4, "name": "db-01", "ipv4": "198.51.100.30", "status": "running", "hardware": { "vcpus": null, "ram_gb": null, "disk_gb": null, "plan": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "machine": { "vcpus": null, "ram_gb": null, "disk_gb": null }, "mismatch": [], "disk_unallocated": false }, "live_status": null, "sync_halt": null }, "tags": [], "provider_credential": { "id": 1, "organization_id": 1, "provider": "hetzner", "label": "Hetzner — Production", "verified_at": "2025-10-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "enabled": true, "enabled_capabilities": null, "disabled_capabilities": null, "error_email_enabled": true, "disabled_operations": null, "alerts_muted_until": null, "spending_capacity": null, "spending_checked_at": null, "scope_check": null, "scope_checked_at": null, "removing_at": null, "last_synced_at": "2026-01-15T11:48:00.000000Z", "reconnect_required_at": null, "account_identity": null, "refused_at": null, "refusal_checked_at": null }, "capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "size_limits": { "min_gb": 10, "max_gb": 10240 } } ```
## Connect requests ### List connect requests {#get-organizations-organization-connect-requests}
`GET` `/organizations/{organization}/connect-requests` **Scope:** `cloud.read` · **Changes data:** no The 20 most recent links you created to ask someone else to connect a provider account, newest first, with who requested each one. Filter with `provider` or `kind`; the response also gives how many days a new link stays valid. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/connect-requests" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [], "ttl_days": 3 } ```
## Endpoints ### Get an address’s monitor {#get-organizations-organization-endpoints-endpoint-monitor}
`GET` `/organizations/{organization}/endpoints/{endpoint}/monitor` **Scope:** `monitoring.read` · **Changes data:** no What the monitor on this address knows: its status, 30-day uptime, response time, the certificate each probe location receives and where its alerts go. `monitor` is null when nothing watches the address. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `endpoint_id`
path · required | integer | The endpoint's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/endpoints/1/monitor" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "endpoint": { "id": 1, "host": "northwind.com", "port": 443, "proxied": true }, "monitor": { "monitor": { "id": 1, "name": "Marketing site", "type": "http", "enabled": true }, "status": "up", "last_checked_at": "2026-01-15T11:58:00+00:00", "uptime_30d": 100, "response_ms": 179, "chain_status": null, "locations": [ { "probe_id": null, "name": null, "region": null, "country_code": null, "status": "up", "response_ms": 180, "checked_at": "2026-01-15T12:00:00+00:00", "fingerprint": null, "certificate": null } ], "alerts": { "channels": [ { "id": 1, "name": "Ops team — email", "type": "email" }, { "id": 2, "name": "On-call — critical only", "type": "email" } ], "people": [ { "id": 2, "name": "Sam Okoye" }, { "id": 3, "name": "Priya Raghunathan" } ], "reaches_anyone": true }, "findings": [], "summary": { "status": "up", "uptime_30d": 100, "locations": 1, "locations_agree": true } } } ```
--- ### Track an address again {#post-organizations-organization-endpoints-endpointid-retrack}
`POST` `/organizations/{organization}/endpoints/{endpointId}/retrack` **Scope:** `cloud.write` · **Changes data:** yes — only where API writes are switched on for the organization Brings an untracked address back so it is checked and shown again. Its certificates and monitor were never changed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `endpointId`
path · required | string | The endpointId. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/endpoints/1/retrack" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "endpoint_id": 1, "untracked_at": null } ```
## Firewall templates ### List firewall templates {#get-organizations-organization-firewall-templates}
`GET` `/organizations/{organization}/firewall-templates` **Scope:** `cloud.read` · **Changes data:** no The built-in firewall templates and the custom templates your organization has saved, with their rules. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/firewall-templates" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "builtin": [ { "id": null, "slug": "web-server", "name": "Web Server", "description": "For a public website or app. Opens HTTP (80), HTTPS (443) and HTTP/3 over QUIC (UDP 443) to everyone. Add SSH restricted to your own IP separately for management.", "is_builtin": true, "rules": [ { "description": "HTTP", "direction": "in", "protocol": "tcp", "port": "80", "ips": [ "0.0.0.0/0", "::/0" ] }, { "description": "HTTPS", "direction": "in", "protocol": "tcp", "port": "443", "ips": [ "0.0.0.0/0", "::/0" ] } ] }, { "id": null, "slug": "mail-server", "name": "Mail Server", "description": "For a self-hosted mail server. Opens SMTP (25), submission (587), SMTPS (465) for delivery and sending, plus IMAP/IMAPS (143/993) and POP3/POP3S (110/995) for clients. Open to everyone.", "is_builtin": true, "rules": [ { "description": "SMTP", "direction": "in", "protocol": "tcp", "port": "25", "ips": [ "0.0.0.0/0", "::/0" ] }, { "description": "Submission", "direction": "in", "protocol": "tcp", "port": "587", "ips": [ "0.0.0.0/0", "::/0" ] } ] } ], "custom": [ { "id": 1, "organization_id": 1, "name": "Web server (80/443)", "description": "Allow HTTP and HTTPS from anywhere, plus SSH from the office network.", "slug": null, "is_builtin": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "rules": [ { "id": 1, "firewall_template_id": 1, "description": "HTTP", "direction": "in", "protocol": "tcp", "port": "80", "ips": [ "0.0.0.0/0", "::/0" ], "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" }, { "id": 2, "firewall_template_id": 1, "description": "HTTPS", "direction": "in", "protocol": "tcp", "port": "443", "ips": [ "0.0.0.0/0", "::/0" ], "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } ] } ] } ```
--- ### Preview a firewall template {#get-organizations-organization-firewall-templates-templatekey-preview-cloudfirewall}
`GET` `/organizations/{organization}/firewall-templates/{templateKey}/preview/{cloudFirewall}` **Scope:** `cloud.read` · **Changes data:** no The rules a template would add to the given firewall, alongside how many rules the firewall has now. `templateKey` is a built-in template’s slug or a custom template’s id; nothing is changed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `templateKey`
path · required | string | The templateKey. | | `cloudFirewall_id`
path · required | integer | The cloudFirewall's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/firewall-templates/1/preview/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "rules": [ { "description": "HTTP", "direction": "in", "protocol": "tcp", "port": "80", "source_ips": [ "0.0.0.0/0", "::/0" ], "destination_ips": [] }, { "description": "HTTPS", "direction": "in", "protocol": "tcp", "port": "443", "source_ips": [ "0.0.0.0/0", "::/0" ], "destination_ips": [] } ], "current_count": 3 } ```
--- # Monitoring Monitors, their results and uptime, and deploy markers. **Common tasks** - [Create a monitor](#post-organizations-organization-monitoring-checks) - [Pause a monitor during a deploy](#post-organizations-organization-monitoring-checks-monitoringcheck-pause) - [The monthly uptime report](#get-organizations-organization-uptime-report) **Only in the app:** Nothing. Monitors can be fully managed with a token. **How it works in the app:** [help article](https://infranest.io/docs/add-a-monitor). **Scopes:** Read with `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Attention summary](#attention-summary) 1 · [Deploy events](#deploy-events) 3 · [Deploy links](#deploy-links) 5 · [Deploy tokens](#deploy-tokens) 1 · [Email DNS monitors](#email-dns-monitors) 2 · [External services](#external-services) 1 · [Monitoring](#monitoring) 1 · [Monitoring checks](#monitoring-checks) 25 · [Status](#status) 1 · [Uptime report](#uptime-report) 2
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Attention summary ### Get your attention summary preview {#get-organizations-organization-attention-summary}
`GET` `/organizations/{organization}/attention-summary` **Scope:** `monitoring.read` · **Changes data:** no When your next “needs you” summary for this organization is due, how it is delivered, and a preview of up to 10 items it would contain. `receives` is false when you have turned the summary off, and `available` is false when it cannot be put together right now. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/attention-summary" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "hour": null, "organisation_hour": 10, "next_at": "2026-01-16T10:00:00+00:00", "via": "email", "telegram": true, "receives": true, "available": true, "count": 13, "new": 13, "will_send": true, "items": [ { "title": "northwind-labs.com will not renew itself", "detail": "Expires Jan 24, 2026 and auto-renew is off.", "route": "/domains/14", "severity": "warning", "new": true }, { "title": "northwind.shop will not renew itself", "detail": "Expires Jan 26, 2026 and auto-renew is off.", "route": "/domains/10", "severity": "warning", "new": true } ] } ```
## Deploy events ### List deploys for a monitor or server {#get-organizations-organization-deploy-events}
`GET` `/organizations/{organization}/deploy-events` **Scope:** `monitoring.read` · **Changes data:** no The deploys linked to one monitor (`monitor`) or server (`server`) between `from` and `to`; the window defaults to the 24 hours before `to`, which defaults to now. Also lists the repositories linked to it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitor`
body | integer | This field is required when server is not present. | | `server`
body | integer | This field is required when monitor is not present. | | `from`
body | string, nullable | Must be a valid date. | | `to`
body | string, nullable | Must be a valid date. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-events" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [], "links": [], "receives_deploys": true, "manage_path": "/integrations/github" } ```
--- ### Get what a deploy changed {#get-organizations-organization-deploy-events-deployevent-changes}
`GET` `/organizations/{organization}/deploy-events/{deployEvent}/changes` **Scope:** `monitoring.read` · **Changes data:** no The commits and pull requests since the previous successful deploy to the same place. Returns `first: true` when there is no earlier deploy to compare with, and `unavailable: true` when GitHub could not say. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `deployEvent_id`
path · required | integer | The deployEvent's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-events/1/changes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "first": true } ```
--- ### List repositories that have deployed {#get-organizations-organization-deploy-events-sources}
`GET` `/organizations/{organization}/deploy-events/sources` **Scope:** `monitoring.read` · **Changes data:** no Each repository and environment that has already reported a deploy to your organization, up to 500. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-events/sources" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "repository": "northwind-cloud/app", "environment": "production" }, { "repository": "northwind-cloud/storefront", "environment": "production" } ] } ```
## Deploy links ### List deploy links {#get-organizations-organization-deploy-links}
`GET` `/organizations/{organization}/deploy-links` **Scope:** `monitoring.read` · **Changes data:** no Your links between a repository and a monitor, server or tag, each with its newest deploy. Pass `monitor` to list only that monitor’s own links; `deploys_connected` says whether anything can report deploys to your organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-links" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": 1, "repository": "northwind-cloud/app", "kind": "deployment", "environment": "production", "workflow": null, "branch": null, "target_type": "monitor", "target_id": 2, "target_name": "App — northwind-app.dev", "last_deploy_at": "2026-01-15T11:18:00+00:00", "has_deployments": true, "checking": true }, { "id": 2, "repository": "northwind-cloud/storefront", "kind": "deployment", "environment": "production", "workflow": null, "branch": null, "target_type": "monitor", "target_id": 4, "target_name": "Checkout flow", "last_deploy_at": "2026-01-15T11:17:00+00:00", "has_deployments": true, "checking": true } ], "deploys_connected": true, "source": { "key": "github", "label": "GitHub" } } ```
--- ### Create a deploy link {#post-organizations-organization-deploy-links}
`POST` `/organizations/{organization}/deploy-links` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Links deploys of a `repository` to a monitor, server or tag (`target_type` and `target_id`). `kind` chooses what counts as a deploy: `deployment` (optionally for one `environment`), `workflow` (a `workflow` file, optionally on one `branch`) or `release`; earlier deploys are imported in the background. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `repository`
body · required | string | Must not be greater than 255 characters. | | `environment`
body | string, nullable | Must not be greater than 255 characters. | | `target_type`
body · required | string | | | `target_id`
body · required | integer | | | `kind`
body | string, nullable | What counts as a deploy. A workflow is named by its file, as GitHub's API addresses it. | | `workflow`
body | string, nullable | This field is required when kind is workflow. Must match the regex /^[\w.-]+\.ya?ml$/. Must not be greater than 255 characters. | | `branch`
body | string, nullable | Must not be greater than 255 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/deploy-links" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"repository":"northwind-cloud/app","target_type":"server","target_id":1}' ``` **Response** · 201 — Created ```json { "id": 3, "repository": "northwind-cloud/app", "kind": "deployment", "environment": null, "workflow": null, "branch": null, "target_type": "server", "target_id": 1, "target_name": "web-01", "last_deploy_at": "2026-01-15T11:18:00+00:00", "has_deployments": true, "checking": true } ```
--- ### Delete a deploy link {#delete-organizations-organization-deploy-links-deploylink}
`DELETE` `/organizations/{organization}/deploy-links/{deployLink}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes the link, so its deploys no longer appear on that monitor, server or tag. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `deployLink_id`
path · required | integer | The deployLink's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/deploy-links/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "ok": true } ```
--- ### Check whether a repository has deploys {#get-organizations-organization-deploy-links-check}
`GET` `/organizations/{organization}/deploy-links/check` **Scope:** `monitoring.read` · **Changes data:** no Whether GitHub has any deployment for `repository`, so you can tell before linking one that never deploys. `has_deployments` is null when GitHub could not be asked. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-links/check" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "has_deployments": null } ```
--- ### List deploy link targets {#get-organizations-organization-deploy-links-targets}
`GET` `/organizations/{organization}/deploy-links/targets` **Scope:** `monitoring.read` · **Changes data:** no The monitors, servers and tags a deploy link can point at, by ID and name. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-links/targets" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "monitors": [ { "id": 3, "name": "API health" }, { "id": 2, "name": "App — northwind-app.dev" } ], "servers": [ { "id": 13, "name": "analytics-01" }, { "id": 3, "name": "api-01" } ], "tags": [ { "id": 3, "name": "Customer-facing" }, { "id": 4, "name": "Internal" } ] } ```
## Deploy tokens ### List deploy tokens {#get-organizations-organization-deploy-tokens}
`GET` `/organizations/{organization}/deploy-tokens` **Scope:** `monitoring.read` · **Changes data:** no Your deploy tokens, newest first, with their name, creator, the monitors each token applies to, whether a signature is required and when each token was last used. Token secrets are never returned. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/deploy-tokens" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "name": "CI — deploy hook", "created_by": 1, "last_used_at": "2026-01-11T12:00:00.000000Z", "monitor_ids": [], "require_signature": true, "created_at": "2026-01-15T12:00:00.000000Z", "creator": { "id": 1, "name": "Alex Rivera", "analytics_id": null } }, { "id": 2, "name": "Staging redeploy", "created_by": 1, "last_used_at": "2025-12-27T12:00:00.000000Z", "monitor_ids": [], "require_signature": false, "created_at": "2026-01-15T12:00:00.000000Z", "creator": { "id": 1, "name": "Alex Rivera", "analytics_id": null } } ] ```
## Email DNS monitors ### Preview email DNS monitors for a domain {#get-organizations-organization-email-dns-monitors}
`GET` `/organizations/{organization}/email-dns-monitors` **Scope:** `monitoring.read` · **Changes data:** no Looks up the MX, SPF and DMARC records the domain in `domain` publishes and returns the DNS monitors that would watch those records. Nothing is created; `resolved` is false when the lookup failed. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/email-dns-monitors" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Create email DNS monitors for a domain {#post-organizations-organization-email-dns-monitors}
`POST` `/organizations/{organization}/email-dns-monitors` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates one DNS monitor for each MX, SPF and DMARC record the domain in `domain` publishes right now, and queues a first check of each. Either every monitor is created or none is: the request is refused when the domain publishes none of those records or the monitors would exceed your plan. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/email-dns-monitors" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
## External services ### List external services {#get-organizations-organization-external-services}
`GET` `/organizations/{organization}/external-services` **Scope:** `monitoring.read` · **Changes data:** no The third-party services you can watch: the curated catalogue plus any you added yourself. Each comes with its status page address, overall status, components and when it was last polled. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/external-services" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 16, "name": "Anthropic", "status_url": "https://status.anthropic.com", "schema": null, "overall_status": "unknown", "components": null, "is_curated": true, "last_polled_at": null }, { "id": 19, "name": "Atlassian", "status_url": "https://status.atlassian.com", "schema": null, "overall_status": "unknown", "components": null, "is_curated": true, "last_polled_at": null } ] ```
## Monitoring ### List active probe regions {#get-monitoring-active-regions}
`GET` `/monitoring/active-regions` **Scope:** `monitoring.read` · **Changes data:** no The regions that currently have at least one active probe, as a sorted list of region names. Use these values when choosing where a monitor runs. **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/monitoring/active-regions" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
## Monitoring checks ### List monitors {#get-organizations-organization-monitoring-checks}
`GET` `/organizations/{organization}/monitoring-checks` **Scope:** `monitoring.read` · **Changes data:** no A paginated list of your monitors with their type, target, interval, current status, 24-hour uptime and a 24-hour status chart, plus status counts in `meta`. Filter with `search`, `type`, `kind` (`own` or `external`), `status`, `enabled` and `tag`, and sort with `sort` and `dir`; pass `all=true` for a plain unpaginated list of every monitor. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 3, "organization_id": 1, "name": "API health", "type": "http", "target": "https://api.northwind.com/health", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "alert_channels_count": 0, "uptime_24h": "0.0", "uptime_chart_24h": [ "up", "up" ], "in_maintenance": false, "effective_interval_seconds": 300, "interval_minutes": 5, "tags": [ { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\MonitoringCheck", "taggable_id": 3, "tag_id": 5 } }, { "id": 1, "name": "Production", "color": "#ef4444", "pivot": { "taggable_type": "App\\Models\\MonitoringCheck", "taggable_id": 3, "tag_id": 1 } } ] }, { "id": 2, "organization_id": 1, "name": "App — northwind-app.dev", "type": "http", "target": "https://northwind-app.dev", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "alert_channels_count": 0, "uptime_24h": "0.0", "uptime_chart_24h": [ "up", "up" ], "in_maintenance": false, "effective_interval_seconds": 300, "interval_minutes": 5, "tags": [ { "id": 6, "name": "Northwind Platform", "color": "#14b8a6", "pivot": { "taggable_type": "App\\Models\\MonitoringCheck", "taggable_id": 2, "tag_id": 6 } }, { "id": 1, "name": "Production", "color": "#ef4444", "pivot": { "taggable_type": "App\\Models\\MonitoringCheck", "taggable_id": 2, "tag_id": 1 } } ] } ], "first_page_url": "http://localhost/api/organizations/1/monitoring-checks?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/monitoring-checks?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/monitoring-checks?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/monitoring-checks", "per_page": 25, "prev_page_url": null, "to": 13, "total": 13, "meta": { "locked_ids": [], "counts": { "total": 13, "up": 10, "down": 1, "degraded": 1, "paused": 1, "silenced": 0, "pending": 0 }, "tags": [ { "id": 6, "name": "Northwind Platform", "color": "#14b8a6" }, { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9" } ] } } ```
--- ### Create a monitor {#post-organizations-organization-monitoring-checks}
`POST` `/organizations/{organization}/monitoring-checks` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates a monitor and queues its first check. Pass `external_service_id` instead of a target to watch a third-party status page; a heartbeat monitor gets its own ping URL as its target. Refused when the monitor count, interval, check type or regions are beyond your plan. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `name`
body · required | string | Must not be greater than 255 characters. | | `type`
body · required | string | One of: `http`, `port`, `ssl`, `dns`, `keyword`, `redirect`, `ping`, `api`, `heartbeat`, `third_party`. | | `external_service_id`
body | integer, nullable | | | `external_component_ids`
body | array | Must not be greater than 255 characters. | | `external_alerts_enabled`
body | boolean, nullable | | | `target`
body · required | string | Must not be greater than 500 characters. | | `interval_seconds`
body | integer | One of: `30`, `60`, `120`, `300`, `600`, `900`, `1800`, `3600`, `7200`, `10800`, `21600`, `32400`, …. | | `enabled`
body | boolean | | | `pagerduty_down_severity`
body | string | One of: `critical`, `error`, `warning`, `info`. | | `pagerduty_degraded_severity`
body | string | One of: `critical`, `error`, `warning`, `info`. | | `flap_threshold`
body | integer | Must be at least 1. Must not be greater than 10. | | `region_consensus_threshold`
body | integer, nullable | Must be at least 1. Must not be greater than 10. | | `preferred_regions`
body | array | | | `probe_mode`
body | string | One of: `any`, `all`. | | `ip_version`
body | string | One of: `ipv4`, `ipv6`, `prefer_ipv4`, `prefer_ipv6`. | | `slow_response_ms`
body | integer, nullable | Must be at least 50. Must not be greater than 30000. | | `ssl_check_enabled`
body | boolean | | | `ssl_expiry_days`
body | integer, nullable | Must be at least 1. Must not be greater than 90. | | `settings`
body | object | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"Checkout API","type":"http","target":"https://example.com/health","interval":300}' ``` **Response** · 201 — Created ```json { "last_status": "unknown", "enabled": true, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "name": "Checkout API", "type": "http", "target": "https://example.com/health", "organization_id": 1, "server_id": null, "server_link": null, "settings": [], "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 15, "alert_channels_count": 0, "interval_minutes": 0, "tags": [] } ```
--- ### Get a monitor {#get-organizations-organization-monitoring-checks-monitoringcheck}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}` **Scope:** `monitoring.read` · **Changes data:** no One monitor with its settings, alert channels, tags, dependencies, maintenance windows, the status pages it appears on, its 30 most recent results and the interval it actually runs at under your plan. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Marketing site", "type": "http", "target": "https://northwind.com", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "results_count": 576, "alert_channels_count": 0, "status_pages": [ { "id": 1, "name": "Northwind Cloud Status", "slug": "northwind", "is_public": true, "display_name": "Website" } ], "recent_statuses": [ "up", "up" ], "recent_results": [ { "status": "up", "checked_at": "2026-01-15T09:35:00Z", "region": null }, { "status": "up", "checked_at": "2026-01-15T09:40:00Z", "region": null } ], "pagerduty_oncall": [], "released_address": null, "effective_interval_seconds": 300, "interval_minutes": 5, "alert_channels": [], "tags": [ { "id": 5, "name": "Northwind Retail", "color": "#0ea5e9", "pivot": { "taggable_type": "App\\Models\\MonitoringCheck", "taggable_id": 1, "tag_id": 5 } }, { "id": 1, "name": "Production", "color": "#ef4444", "pivot": { "taggable_type": "App\\Models\\MonitoringCheck", "taggable_id": 1, "tag_id": 1 } } ], "parent_checks": [], "child_checks": [], "maintenance_windows": [ { "id": 2, "organization_id": 1, "name": "Load balancer certificate rotation", "description": "Replaced the certificate on web-lb and reloaded the listeners.", "starts_at": "2026-01-06T01:00:00.000000Z", "ends_at": "2026-01-06T01:30:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "suppress_alerts": true, "status": "completed", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks": [], "tags": [] }, { "id": 1, "organization_id": 1, "name": "Database cluster upgrade", "description": "Rolling upgrade of the PostgreSQL primary and its replica. Brief API slowdowns are possible; no downtime is expected.", "starts_at": "2026-01-18T02:00:00.000000Z", "ends_at": "2026-01-18T03:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "suppress_alerts": true, "status": "scheduled", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks": [], "tags": [] } ], "latest_ssl_certificate": { "id": 1, "organization_id": 1, "source": "probe", "monitoring_check_id": 1, "domain_id": 1, "cloud_server_id": null, "provider_credential_id": null, "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9", "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "serial": "2395BA42", "sans": [ "northwind.com", "www.northwind.com" ], "not_before": "2025-12-30T12:00:00.000000Z", "not_after": "2026-03-30T12:00:00.000000Z", "purpose": "tls_server", "key_algo": "rsa", "key_bits": 2048, "sig_algo": "SHA256-RSA", "trust": "public", "validation": "DV", "is_wildcard": false, "is_multi_san": true, "managed": true, "renewal_source": "Let's Encrypt (R3)", "classification_override": null, "status": "valid", "superseded_by_id": null, "dismissed_at": null, "deployment_locations": [ { "endpoint": "northwind.com:443", "last_seen": "2026-01-15T11:50:00+00:00", "first_seen": "2026-01-08T12:00:00+00:00" } ], "owner": null, "notes": null, "first_seen_at": "2025-11-16T12:00:00.000000Z", "last_seen_at": "2026-01-15T06:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subject_org": null, "subject_location": null, "chain": [ { "subject": "northwind.com", "issuer": "Let's Encrypt (R3)", "is_ca": false, "self_signed": false, "not_after": "2026-03-30T12:00:00+00:00", "fingerprint": "6f6f72db300125c26f8b2c866124ffb57e60a201177144ed5e5b6711eba24cb9" }, { "subject": "Let's Encrypt (R3)", "issuer": "ISRG Root X1", "is_ca": true, "self_signed": false, "not_after": "2030-01-15T12:00:00+00:00", "fingerprint": "e04ca19ed66485fb479283c98fbfb4cda1ab67de1965304902bec3f6a55a2c8e" } ], "chain_status": "trusted", "sources": [ { "source": "probe", "last_seen": "2026-01-15T06:00:00+00:00", "first_seen": "2025-11-16T12:00:00+00:00" } ], "crl_url": null, "ocsp_url": null, "caa_status": "ok", "caissuer_url": "http://r3.i.lencr.org/", "no_auto_merge": false, "price_yearly": "0.00", "price_source": "manual", "price_currency": "EUR", "billing_interval": "yearly", "interval_count": 1, "renewal_type": "automatic", "renewal_type_basis": "guess", "served_status": "served", "served_checked_at": "2026-01-15T11:50:00.000000Z", "subscription_ends_at": null, "seller": null, "role": null, "ct_finding": null, "owner_user_id": null } } ```
--- ### Update a monitor {#put-organizations-organization-monitoring-checks-monitoringcheck}
`PUT` `/organizations/{organization}/monitoring-checks/{monitoringCheck}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes only the fields you send; keys inside `settings` are merged rather than replaced. Refused when a new interval, check type or set of regions is beyond your plan. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `name`
body | string | Must not be greater than 255 characters. | | `server_id`
body | integer, nullable | Must match an existing stored value. | | `server_link_auto`
body | boolean | | | `type`
body | string | One of: `http`, `port`, `ssl`, `dns`, `keyword`, `redirect`, `ping`, `api`, `heartbeat`, `third_party`. | | `external_component_ids`
body | array | Must not be greater than 255 characters. | | `external_alerts_enabled`
body | boolean | | | `target`
body | string | Must not be greater than 500 characters. | | `interval_seconds`
body | integer | One of: `30`, `60`, `120`, `300`, `600`, `900`, `1800`, `3600`, `7200`, `10800`, `21600`, `32400`, …. | | `enabled`
body | boolean | | | `pagerduty_down_severity`
body | string | One of: `critical`, `error`, `warning`, `info`. | | `pagerduty_degraded_severity`
body | string | One of: `critical`, `error`, `warning`, `info`. | | `flap_threshold`
body | integer | Must be at least 1. Must not be greater than 10. | | `region_consensus_threshold`
body | integer, nullable | Must be at least 1. Must not be greater than 10. | | `preferred_regions`
body | array | | | `probe_mode`
body | string | One of: `any`, `all`. | | `ip_version`
body | string | One of: `ipv4`, `ipv6`, `prefer_ipv4`, `prefer_ipv6`. | | `slow_response_ms`
body | integer, nullable | Must be at least 50. Must not be greater than 30000. | | `ssl_check_enabled`
body | boolean | | | `ssl_expiry_days`
body | integer, nullable | Must be at least 1. Must not be greater than 90. | | `settings`
body | object | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/monitoring-checks/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"interval_seconds":300}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Marketing site", "type": "http", "target": "https://northwind.com", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "alert_channels_count": 1, "interval_minutes": 5 } ```
--- ### Delete a monitor {#delete-organizations-organization-monitoring-checks-monitoringcheck}
`DELETE` `/organizations/{organization}/monitoring-checks/{monitoringCheck}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Permanently deletes the monitor. A monitor locked because it is over your plan’s limit cannot be deleted this way. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/monitoring-checks/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
--- ### List what an external service affects {#get-organizations-organization-monitoring-checks-monitoringcheck-affected}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/affected` **Scope:** `monitoring.read` · **Changes data:** no For a monitor that watches a third-party status page: the DNS zones, domains, monitors and servers of yours that depend on that service. Each has a `source` of `manual` (linked by you) or `auto` (zones and domains at the matching provider, detected by InfraNest). | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/affected" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "provider": null, "resources": [] } ```
--- ### Link a resource to an external service {#post-organizations-organization-monitoring-checks-monitoringcheck-affected}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/affected` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Records that one of your resources, given by `resource_type` and `resource_id`, depends on the third-party service this monitor watches. Only works on monitors that watch a third-party status page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `resource_type`
body · required | string | | | `resource_id`
body · required | integer | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/affected" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"resource_type":"","resource_id":16}' ```
--- ### Unlink a resource from an external service {#delete-organizations-organization-monitoring-checks-monitoringcheck-affected-type-resourceid}
`DELETE` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/affected/{type}/{resourceId}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes a resource you linked to the third-party service this monitor watches. Automatically detected links cannot be removed this way. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `type`
path · required | string | The type. | | `resourceId`
path · required | string | The resourceId. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/monitoring-checks/1/affected/1/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- ### Get a monitor’s uptime and response times for a period {#get-organizations-organization-monitoring-checks-monitoringcheck-aggregates}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/aggregates` **Scope:** `monitoring.read` · **Changes data:** no Totals for one window: `uptime_percent` (the share of the time no confirmed outage covered, the figure the app shows), `up_percent` (the share of checks that came back up), checks by outcome, p50 and p95 response time in milliseconds, and the number and minutes of unplanned incidents. Pick `period` (`24h`, `7d`, `30d` or `90d`, default `24h`) or set `from` and `to` (ISO 8601, at most 366 days apart; longer is refused with `range_too_large`); the window starts on the hour `from` falls in. Pass `region` for one probe region, where `uptime_percent` is the share of checks that were up. Response times over more than one hour or probe are averaged from hourly figures, weighted by the number of checks, so they are close rather than exact. `uptime_percent` is null when nothing was measured. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `period`
query | string | `24h`, `7d`, `30d` or `90d`. Default `24h`; ignored when `from` or `to` is sent. | | `from`
query | string | Start of the window, ISO 8601; rounded down to the hour. | | `to`
query | string | End of the window, ISO 8601; default now. | | `region`
query | string | Only checks from probes in this region. | | `period`
body | string | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/aggregates" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "monitoring_check_id": 1, "period": "24h", "from": "2026-01-14T12:00:00Z", "to": "2026-01-15T12:00:00Z", "region": null, "uptime_percent": null, "up_percent": 100, "checks": { "total": 288, "up": 288, "degraded": 0, "down": 0 }, "response_time_ms": { "p50": 180, "p95": 288 }, "incidents": { "count": 0, "down_minutes": 0 } } ```
--- ### Attach an alert channel to a monitor {#post-organizations-organization-monitoring-checks-monitoringcheck-alerts}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/alerts` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Sends this monitor’s alerts to the alert channel in `alert_channel_id`, which must belong to your organization. Refused if the channel is already attached. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `alert_channel_id`
body · required | integer | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/alerts" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"alert_channel_id":1}' ``` **Response** · 201 — Created ```json { "id": 1, "organization_id": 1, "name": "Ops team — email", "type": "email", "settings": { "email": "ops@northwind.example" }, "enabled": true, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subscriptions": [ "monitoring_down", "monitoring_recovered" ], "is_personal": false, "user_id": null, "format": "html", "last_sent_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "consecutive_failures": 0, "last_failure_at": null, "last_error": null, "last_error_type": null, "broken_since": null, "broken_notified_at": null, "broken_notify_count": 0, "awaiting_confirmation_at": null, "categories": null, "telegram_link_id": null, "telegram_chat_id": null, "slack_installation_id": null, "slack_channel_id": null } ```
--- ### Detach an alert channel from a monitor {#delete-organizations-organization-monitoring-checks-monitoringcheck-alerts-alertchannel}
`DELETE` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/alerts/{alertChannel}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Stops sending this monitor’s alerts to the channel. A monitor with no channels left keeps running, but no channel receives its alerts. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `alertChannel_id`
path · required | integer | The alertChannel's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/monitoring-checks/1/alerts/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
--- ### Copy a monitor {#post-organizations-organization-monitoring-checks-monitoringcheck-clone}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/clone` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates a copy named with “(copy)” appended, with the same settings but no history. The copy starts paused, and counts against your plan like any new monitor. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/clone" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 201 — Created ```json { "organization_id": 1, "name": "Marketing site (copy)", "type": "http", "target": "https://northwind.com", "enabled": false, "settings": [], "deleted_at": null, "flap_threshold": 2, "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": 1, "server_link": "inferred", "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 16, "alert_channels_count": 0, "interval_minutes": 5 } ```
--- ### List a monitor’s dependencies {#get-organizations-organization-monitoring-checks-monitoringcheck-dependencies}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/dependencies` **Scope:** `monitoring.read` · **Changes data:** no The monitors this one depends on (`parents`) and the monitors that depend on it (`children`), each with its name, type, target and current status. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/dependencies" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "parents": [], "children": [] } ```
--- ### Add a monitor dependency {#post-organizations-organization-monitoring-checks-monitoringcheck-dependencies}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/dependencies` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Pass `parent_check_id` to make this monitor depend on another, or `child_check_id` to make another depend on it. Both monitors must be yours, and a dependency on itself or one that would form a loop is refused. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `parent_check_id`
body | integer, nullable | This field is required when child_check_id is not present. Must match an existing stored value. | | `child_check_id`
body | integer, nullable | This field is required when parent_check_id is not present. Must match an existing stored value. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/dependencies" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"child_check_id":3}' ``` **Response** · 201 — Created ```json { "parent_check_id": 1, "child_check_id": 3, "organization_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 2 } ```
--- ### Remove a monitor dependency {#delete-organizations-organization-monitoring-checks-monitoringcheck-dependencies-othercheck}
`DELETE` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/dependencies/{otherCheck}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes the dependency between the two monitors, in whichever direction it runs. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `otherCheck_id`
path · required | integer | The otherCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/monitoring-checks/1/dependencies/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Deleted." } ```
--- ### List a monitor’s incidents {#get-organizations-organization-monitoring-checks-monitoringcheck-incidents}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/incidents` **Scope:** `monitoring.read` · **Changes data:** no The monitor’s 20 most recent incidents, newest first, with their status, impact, start and end times and duration. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/incidents" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 2, "source": "monitor", "title": "Brief marketing-site blip", "status": "resolved", "impact": "minor", "started_at": "2026-01-09T11:45:00.000000Z", "resolved_at": "2026-01-09T12:00:00.000000Z", "duration_seconds": 900, "cause": null, "description": null, "external_status": null, "external_url": null, "is_planned": false, "acknowledged_at": null } ] ```
--- ### Edit a monitor incident {#patch-organizations-organization-monitoring-checks-monitoringcheck-incidents-incident}
`PATCH` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/incidents/{incident}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes an incident’s `description`, or marks it as planned with `is_planned`, which takes it out of uptime figures and the status page’s incident history. Incidents imported from a third-party status page cannot be edited. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `incident_id`
path · required | integer | The incident's ID. | | `description`
body | string, nullable | Must not be greater than 2000 characters. | | `is_planned`
body | boolean, nullable | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/monitoring-checks/1/incidents/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"description":"Caused by an expired upstream certificate.","is_planned":false}' ``` **Response** · 200 ```json { "id": 2, "monitoring_check_id": 1, "started_at": "2026-01-09T11:45:00.000000Z", "resolved_at": "2026-01-09T12:00:00.000000Z", "duration_seconds": 900, "cause": null, "affected_probe_ids": null, "acknowledged_at": null, "acknowledged_by": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "description": "Caused by an expired upstream certificate.", "is_planned": false, "title": "Brief marketing-site blip", "status": "resolved", "impact": "minor", "source": "monitor", "server_id": null, "cloud_server_alert_rule_id": null, "external_status": null, "external_url": null, "alert_rule_state_id": null, "folded_into": null, "unstable_at": null, "unstable_ended_at": null, "outage_count": 1, "down_since": null, "downtime_seconds": 0, "outages": null, "acknowledged_by_name": null } ```
--- ### Pause a monitor {#post-organizations-organization-monitoring-checks-monitoringcheck-pause}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/pause` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Stops the monitor from running until you resume it, and returns the updated monitor. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/pause" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Marketing site", "type": "http", "target": "https://northwind.com", "enabled": false, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "interval_minutes": 5 } ```
--- ### Get who an outage alert would reach {#get-organizations-organization-monitoring-checks-monitoringcheck-reach}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/reach` **Scope:** `monitoring.read` · **Changes data:** no Whether an outage on this monitor would reach anybody right now: `state` is `ok`, `nobody` or `failing`, with the alert channels it would use, how many members get the status email, and whether it would escalate. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/reach" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "state": "ok", "channels": [ { "id": 1, "name": "Ops team — email", "type": "email", "failing": false }, { "id": 2, "name": "On-call — critical only", "type": "email", "failing": false } ], "members": 4, "escalation": { "via": "pagerduty" } } ```
--- ### List a monitor’s check results {#get-organizations-organization-monitoring-checks-monitoringcheck-results}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/results` **Scope:** `monitoring.read` · **Changes data:** no The monitor’s check results, newest first. With `resolution=raw` (the default) each result is one check by one probe, filterable by `status`; individual results are kept for 3 days. With `resolution=hour` or `day` each row sums one hour or one day across probes: checks by outcome (`checks_up`, `checks_degraded` for slow checks, `checks_down` for failures), `up_percent` and the p50 and p95 response times, reaching back as far as your plan keeps history. Set the window with `from` and `to` (ISO 8601; default: everything kept for raw, the last 24 hours for hours, the last 30 days for days), narrow it to one probe `region`, and page with `page` and `per_page` (up to 500, default 50). A range longer than the resolution allows — 3 days raw, 31 days by hour, 366 days by day — is refused with `range_too_large`. `window` repeats the range that was applied. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `resolution`
query | string | `raw` (each check, the default), `hour` or `day` (summed across probes). | | `from`
query | string | Start of the window, ISO 8601. | | `to`
query | string | End of the window, ISO 8601; default now. | | `status`
query | string | Raw results only: only this status (`up`, `down`, `degraded`). | | `region`
query | string | Only results from probes in this region. | | `per_page`
query | integer | Rows per page, 1 to 500. Default 50. | | `page`
query | integer | Page number, from 1. | | `per_page`
body | string | | | `page`
body | integer | Must be at least 1. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/results" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 1, "monitoring_check_id": 1, "status": "up", "response_time_ms": 180, "status_code": 200, "message": "HTTP 200 — OK", "checked_at": "2026-01-15T12:00:00.000000Z", "meta": null, "probe_id": null, "is_failover": false, "failover_from_region": null, "ssl_fingerprint": null, "ssl_expires_at": null, "scheduled_for": null, "suppressed_by_probe": false, "reason_code": null, "reason_params": null }, { "id": 2, "monitoring_check_id": 1, "status": "up", "response_time_ms": 191, "status_code": 200, "message": "HTTP 200 — OK", "checked_at": "2026-01-15T11:55:00.000000Z", "meta": null, "probe_id": null, "is_failover": false, "failover_from_region": null, "ssl_fingerprint": null, "ssl_expires_at": null, "scheduled_for": null, "suppressed_by_probe": false, "reason_code": null, "reason_params": null } ], "first_page_url": "http://localhost/api/organizations/1/monitoring-checks/1/results?page=1", "from": 1, "last_page": 12, "last_page_url": "http://localhost/api/organizations/1/monitoring-checks/1/results?page=12", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/monitoring-checks/1/results?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": "http://localhost/api/organizations/1/monitoring-checks/1/results?page=2", "path": "http://localhost/api/organizations/1/monitoring-checks/1/results", "per_page": 50, "prev_page_url": null, "to": 50, "total": 576, "window": { "from": null, "to": null, "resolution": "raw" } } ```
--- ### Resume a monitor {#post-organizations-organization-monitoring-checks-monitoringcheck-resume}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/resume` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Starts a paused monitor again, queues a check straight away and returns the updated monitor. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/resume" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Marketing site", "type": "http", "target": "https://northwind.com", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "interval_minutes": 5 } ```
--- ### Run a monitor now {#post-organizations-organization-monitoring-checks-monitoringcheck-run}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/run` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Queues an immediate check of the monitor and answers 202 straight away; the result appears in its results once the check has run. Limited by your plan’s hourly allowance of manual checks. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/run" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 202 ```json { "message": "Check queued." } ```
--- ### Silence a monitor’s alerts {#post-organizations-organization-monitoring-checks-monitoringcheck-silence}
`POST` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/silence` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Mutes the monitor’s alerts for `duration_minutes` (1 to 10,080), or until you unsilence it when you leave that out. The monitor keeps running. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | | `duration_minutes`
body | integer, nullable | Must be at least 1. Must not be greater than 10080. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/monitoring-checks/1/silence" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"duration_minutes":60}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Marketing site", "type": "http", "target": "https://northwind.com", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": "2026-01-15T13:00:00.000000Z", "silenced_by": 1, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "interval_minutes": 5 } ```
--- ### Unsilence a monitor’s alerts {#delete-organizations-organization-monitoring-checks-monitoringcheck-silence}
`DELETE` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/silence` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Lifts a silence straight away so the monitor’s alerts are sent again, and returns the updated monitor. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/monitoring-checks/1/silence" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Marketing site", "type": "http", "target": "https://northwind.com", "enabled": true, "settings": [], "last_status": "up", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 200 — OK", "consecutive_failures": 0, "flap_threshold": 2, "last_status_changed_at": "2026-01-14T12:00:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "interval_minutes": 5 } ```
--- ### Get a monitor’s uptime and response-time stats {#get-organizations-organization-monitoring-checks-monitoringcheck-stats}
`GET` `/organizations/{organization}/monitoring-checks/{monitoringCheck}/stats` **Scope:** `monitoring.read` · **Changes data:** no Uptime for the last 24 hours, 7 days and 30 days, incident counts, response-time figures and chart series for the chosen `period` (`24h`, `7d` or `30d`, default `24h`). Pass `region` to narrow the figures to one probe region; the regions with data are listed in `regions`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `monitoringCheck_id`
path · required | integer | The monitoringCheck's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/monitoring-checks/1/stats" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "uptime": { "24h": null, "7d": null, "30d": null }, "chart_data": [ { "time": "2026-01-14T12:00:00+00:00", "avg": 176, "min": 164, "max": 186 }, { "time": "2026-01-14T13:00:00+00:00", "avg": 167, "min": 154, "max": 178 } ], "uptime_chart_data": [ { "time": "2026-01-14T12:00:00+00:00", "up": 12, "total": 12, "degraded": 0, "down_seconds": 0, "status": "up" }, { "time": "2026-01-14T13:00:00+00:00", "up": 12, "total": 12, "degraded": 0, "down_seconds": 0, "status": "up" } ], "period": "24h", "response_time": { "avg": 180, "min": 137, "max": 223 }, "incident_stats": { "24h": { "count": 0, "down_minutes": 0 }, "7d": { "count": 1, "down_minutes": 15 }, "30d": { "count": 1, "down_minutes": 15 } }, "regions": [], "region": null, "chart_by_region": null } ```
## Status ### Get the current status of everything {#get-organizations-organization-status}
`GET` `/organizations/{organization}/status` **Scope:** `monitoring.read` · **Changes data:** no Everything in your organization as it stands right now, in one call: every monitor with its `status` (`up`, `down`, `degraded`, `paused` or `unknown`), when it was last checked and its 24-hour uptime; every server with its state, whether its agent is reporting (null when no agent is installed) and how many of its alert rules are breaching; the open incidents; the maintenance windows running now; and counts of each in `summary`. Pass `tag` (a tag ID) to narrow it to monitors and servers with that tag, their incidents, and maintenance that carries the tag or covers them. `servers` is null when the token has no Cloud scope or your role cannot see servers. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `tag`
query | integer | Only monitors and servers with this tag, and what concerns them. | | `tag`
body | integer, nullable | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "generated_at": "2026-01-15T12:00:00Z", "tag": null, "summary": { "monitors": { "total": 13, "up": 10, "down": 1, "degraded": 1, "paused": 1, "unknown": 0 }, "servers": { "total": 14, "running": 12, "agent_not_reporting": 0, "breaching": 0 }, "open_incidents": 1, "active_maintenance": 0 }, "monitors": [ { "id": 3, "name": "API health", "type": "http", "status": "up", "enabled": true, "silenced": false, "last_checked_at": "2026-01-15T11:58:00Z", "uptime_24h": 0 }, { "id": 2, "name": "App — northwind-app.dev", "type": "http", "status": "up", "enabled": true, "silenced": false, "last_checked_at": "2026-01-15T11:58:00Z", "uptime_24h": 0 } ], "servers": [ { "id": 13, "name": "analytics-01", "status": "stopped", "monitoring_enabled": false, "agent_reporting": null, "breaching_alerts": 0 }, { "id": 3, "name": "api-01", "status": "running", "monitoring_enabled": true, "agent_reporting": true, "breaching_alerts": 0 } ], "incidents": [ { "id": 1, "title": "Checkout returning 503", "status": "investigating", "impact": "major", "started_at": "2026-01-15T11:20:00Z", "acknowledged_at": null, "monitoring_check_id": 4, "server_id": null } ], "maintenance": [] } ```
## Uptime report ### Get the monthly uptime report {#get-organizations-organization-uptime-report}
`GET` `/organizations/{organization}/uptime-report` **Scope:** `monitoring.read` · **Changes data:** no Uptime per monitor for one calendar month, with downtime, incidents, longest outage and the basis of each figure. Pass `month` as `YYYY-MM` (default: the last full month) and `tag` to limit it to monitors with that tag. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/uptime-report" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "month": "2025-12", "closed": true, "days_in_month": 31, "checks": [ { "id": 3, "name": "API health", "type": "http", "target": "https://api.northwind.com/health", "uptime_pct": 99.951, "downtime_minutes": 22, "incidents": 1, "longest_outage_minutes": 22, "monitored_minutes": 44640, "days_with_data": 31, "planned_minutes_excluded": 0, "probes": [], "started_monitoring": "2026-01-15" }, { "id": 2, "name": "App — northwind-app.dev", "type": "http", "target": "https://northwind-app.dev", "uptime_pct": 100, "downtime_minutes": 0, "incidents": 0, "longest_outage_minutes": 0, "monitored_minutes": 44640, "days_with_data": 31, "planned_minutes_excluded": 0, "probes": [], "started_monitoring": "2026-01-15" } ], "summary": { "checks": 13, "checks_measured": 12, "worst_uptime_pct": 99.951, "mean_uptime_pct": 99.996, "incidents": 1, "downtime_minutes": 22 }, "findings": [ { "key": "uptimeReport.findings.worst", "params": { "check": "API health", "percent": 99.951, "minutes": 22, "n": 1 }, "tone": "warning" }, { "key": "uptimeReport.findings.unmeasured", "params": { "n": 1, "total": 13 }, "tone": "warning" } ], "generated_at": "2026-01-15T12:00:00+00:00", "months": [ { "month": "2025-02", "closed": true, "checks": 13, "checks_measured": 0, "worst_uptime_pct": null, "mean_uptime_pct": null, "incidents": 0, "downtime_minutes": 0 }, { "month": "2025-03", "closed": true, "checks": 13, "checks_measured": 0, "worst_uptime_pct": null, "mean_uptime_pct": null, "incidents": 0, "downtime_minutes": 0 } ], "applied_filters": [] } ```
--- ### Download the monthly uptime report as CSV {#get-organizations-organization-uptime-report-export}
`GET` `/organizations/{organization}/uptime-report/export` **Scope:** `monitoring.read` · **Changes data:** no The same report as a CSV file, one row per monitor. Takes the same `month` and `tag` parameters, and every download is recorded in your audit log. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/uptime-report/export" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 — The month's uptime report as CSV. (`text/csv`)
--- # Incidents Incidents: open, update and resolve them. **Common tasks** - [List incidents](#get-organizations-organization-incidents) - [Post an update](#post-organizations-organization-incidents-incident-updates) - [Acknowledge an incident](#post-organizations-organization-incidents-incident-acknowledge) **Only in the app:** Nothing. Monitors open incidents; a token can update, annotate and acknowledge them, as the app can. **How it works in the app:** [help article](https://infranest.io/docs/incidents). **Scopes:** Read with `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Incidents](#incidents) 7
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Incidents ### List incidents {#get-organizations-organization-incidents}
`GET` `/organizations/{organization}/incidents` **Scope:** `monitoring.read` · **Changes data:** no A paginated list of your monitors’ incidents with their monitor, status, impact, latest update and who acknowledged each one, plus counts in `meta`. `period` is `24h`, `7d` (default), `30d` or `all`; filter with `status` (`open`, `acked` or `resolved`) and `search`, or pass `incident` to fetch a single incident whatever its age. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/incidents" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 2, "monitoring_check_id": 1, "source": "monitor", "title": "Brief marketing-site blip", "status": "resolved", "impact": "minor", "started_at": "2026-01-09T11:45:00.000000Z", "resolved_at": "2026-01-09T12:00:00.000000Z", "duration_seconds": 900, "cause": null, "description": null, "external_status": null, "external_url": null, "is_planned": false, "acknowledged_at": null, "acknowledged_by": null, "check": { "id": 1, "name": "Marketing site", "type": "http", "last_status": "up", "interval_minutes": 0 }, "updates": [ { "id": 5, "monitoring_incident_id": 2, "status": "resolved", "message": "A CDN configuration change was rolled back and the site recovered. No other services were affected.", "user_id": null, "created_at": "2026-01-09T12:00:00.000000Z", "user": null } ] }, { "id": 1, "monitoring_check_id": 4, "source": "monitor", "title": "Checkout returning 503", "status": "investigating", "impact": "major", "started_at": "2026-01-15T11:20:00.000000Z", "resolved_at": null, "duration_seconds": null, "cause": "Upstream payment gateway timeout", "description": null, "external_status": null, "external_url": null, "is_planned": false, "acknowledged_at": null, "acknowledged_by": null, "check": { "id": 4, "name": "Checkout flow", "type": "http", "last_status": "down", "interval_minutes": 0 }, "updates": [ { "id": 3, "monitoring_incident_id": 1, "status": "monitoring", "message": "The failover is live and checkout is succeeding again. Watching error rates before we call this resolved.", "user_id": null, "created_at": "2026-01-15T11:54:00.000000Z", "user": null } ] } ], "first_page_url": "http://localhost/api/organizations/1/incidents?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/incidents?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/incidents?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/incidents", "per_page": 25, "prev_page_url": null, "to": 2, "total": 2, "meta": { "counts": { "total": 2, "open": 1, "acked": 0, "resolved": 1 } } } ```
--- ### Update an incident {#patch-organizations-organization-incidents-incident}
`PATCH` `/organizations/{organization}/incidents/{incident}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes an incident’s `title`, `status`, `impact`, `description` or `is_planned`. Setting `status` to `resolved` resolves an open incident, and any other status reopens a resolved one; incidents imported from a third-party status page cannot be edited. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `incident_id`
path · required | integer | The incident's ID. | | `title`
body | string, nullable | Must not be greater than 255 characters. | | `status`
body | string | One of: `investigating`, `identified`, `monitoring`, `resolved`. | | `impact`
body | string | One of: `critical`, `major`, `minor`, `none`. | | `description`
body | string, nullable | Must not be greater than 5000 characters. | | `is_planned`
body | boolean | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/incidents/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"status":"identified","impact":"major"}' ``` **Response** · 200 ```json { "id": 1, "monitoring_check_id": 4, "started_at": "2026-01-15T11:20:00.000000Z", "resolved_at": null, "duration_seconds": null, "cause": "Upstream payment gateway timeout", "affected_probe_ids": null, "acknowledged_at": null, "acknowledged_by": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "description": null, "is_planned": false, "title": "Checkout returning 503", "status": "identified", "impact": "major", "source": "monitor", "server_id": null, "cloud_server_alert_rule_id": null, "external_status": null, "external_url": null, "alert_rule_state_id": null, "folded_into": null, "unstable_at": null, "unstable_ended_at": null, "outage_count": 1, "down_since": null, "downtime_seconds": 0, "outages": null, "acknowledged_by_name": null } ```
--- ### Acknowledge an incident {#post-organizations-organization-incidents-incident-acknowledge}
`POST` `/organizations/{organization}/incidents/{incident}/acknowledge` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Marks the incident as acknowledged by you, so your team knows someone is on it, and returns it with who acknowledged it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `incident_id`
path · required | integer | The incident's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/incidents/1/acknowledge" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "monitoring_check_id": 4, "started_at": "2026-01-15T11:20:00.000000Z", "resolved_at": null, "duration_seconds": null, "cause": "Upstream payment gateway timeout", "affected_probe_ids": null, "acknowledged_at": "2026-01-15T12:00:00.000000Z", "acknowledged_by": { "id": 1, "first_name": null, "last_name": null, "analytics_id": null }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "description": null, "is_planned": false, "title": "Checkout returning 503", "status": "investigating", "impact": "major", "source": "monitor", "server_id": null, "cloud_server_alert_rule_id": null, "external_status": null, "external_url": null, "alert_rule_state_id": null, "folded_into": null, "unstable_at": null, "unstable_ended_at": null, "outage_count": 1, "down_since": null, "downtime_seconds": 0, "outages": null, "acknowledged_by_name": null, "check": { "id": 4, "organization_id": 1, "name": "Checkout flow", "type": "http", "target": "https://northwind.com/checkout", "enabled": true, "settings": [], "last_status": "down", "last_checked_at": "2026-01-15T11:58:00.000000Z", "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "last_message": "HTTP 503 — Service Unavailable", "consecutive_failures": 8, "flap_threshold": 2, "last_status_changed_at": "2026-01-15T11:20:00.000000Z", "consensus_threshold": 1, "preferred_regions": [ "EU" ], "probe_mode": "any", "ip_version": "prefer_ipv4", "slow_response_ms": null, "ssl_check_enabled": true, "ssl_expiry_days": 21, "heartbeat_uuid": null, "last_ping_at": null, "silenced_until": null, "silenced_by": null, "is_suppressed_by_parent": false, "external_service_id": null, "external_component_ids": null, "external_alerts_enabled": false, "region_checked_at": null, "region_consensus_threshold": null, "last_reason_code": null, "last_reason_params": null, "interval_seconds": 300, "badge_enabled": false, "badge_token": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "server_id": null, "server_link": null, "pagerduty_down_severity": "critical", "pagerduty_degraded_severity": "warning", "interval_minutes": 5 } } ```
--- ### Add a team note to an incident {#post-organizations-organization-incidents-incident-notes}
`POST` `/organizations/{organization}/incidents/{incident}/notes` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Adds an internal note in `body` for your team. Notes never appear on a status page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `incident_id`
path · required | integer | The incident's ID. | | `body`
body · required | string | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/incidents/1/notes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"body":"Rolled back release 2026.01.15 — error rate back to normal."}' ``` **Response** · 201 — Created ```json { "id": 1, "body": "Rolled back release 2026.01.15 — error rate back to normal.", "via": "app", "by": "Alex Rivera", "created_at": "2026-01-15T12:00:00+00:00" } ```
--- ### Remove an incident acknowledgement {#post-organizations-organization-incidents-incident-unacknowledge}
`POST` `/organizations/{organization}/incidents/{incident}/unacknowledge` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Clears the acknowledgement so the incident reads as unhandled again. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `incident_id`
path · required | integer | The incident's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/incidents/1/unacknowledge" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "monitoring_check_id": 4, "started_at": "2026-01-15T11:20:00.000000Z", "resolved_at": null, "duration_seconds": null, "cause": "Upstream payment gateway timeout", "affected_probe_ids": null, "acknowledged_at": null, "acknowledged_by": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "description": null, "is_planned": false, "title": "Checkout returning 503", "status": "investigating", "impact": "major", "source": "monitor", "server_id": null, "cloud_server_alert_rule_id": null, "external_status": null, "external_url": null, "alert_rule_state_id": null, "folded_into": null, "unstable_at": null, "unstable_ended_at": null, "outage_count": 1, "down_since": null, "downtime_seconds": 0, "outages": null, "acknowledged_by_name": null } ```
--- ### Get an incident’s timeline {#get-organizations-organization-incidents-incident-updates}
`GET` `/organizations/{organization}/incidents/{incident}/updates` **Scope:** `monitoring.read` · **Changes data:** no The incident’s public status updates and internal team notes, plus the alert channels that were notified, deploys shortly before it began, who PagerDuty is paging, notes from PagerDuty and tickets opened for it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `incident_id`
path · required | integer | The incident's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/incidents/1/updates" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "updates": [ { "id": 3, "monitoring_incident_id": 1, "status": "monitoring", "message": "The failover is live and checkout is succeeding again. Watching error rates before we call this resolved.", "user_id": null, "created_at": "2026-01-15T11:54:00.000000Z", "user": null }, { "id": 8, "monitoring_incident_id": 1, "status": "investigating", "message": "The provider has acknowledged an incident on their side. We are holding while they work.", "user_id": 1, "created_at": "2026-01-15T11:51:00.000000Z", "user": { "id": 1, "first_name": null, "last_name": null, "analytics_id": null } } ], "team_notes": [], "notified": [ "email", "slack" ], "deploys": [ { "id": 9, "source": "github", "kind": "deployment", "workflow": null, "repository": "northwind-cloud/storefront", "environment": "production", "state": "success", "sha": "f19e6c7000000000000000000000000000000000", "ref": "main", "actor": "jordan-k", "url": "https://github.com/northwind-cloud/storefront/deployments", "deployed_at": "2026-01-15T11:17:00+00:00" } ], "oncall": [], "pagerduty_notes": [ { "id": 2, "author_name": "Alex Rivera", "content": "Secondary payment route is warmed up — Sam is switching checkout over to it now.", "created_at": "2026-01-15T11:45:00+00:00" }, { "id": 1, "author_name": "Sam Okoye", "content": "Payment gateway status page shows degraded API performance in eu-west. Opening a ticket with them.", "created_at": "2026-01-15T11:26:00+00:00" } ], "tickets": [] } ```
--- ### Post an incident update {#post-organizations-organization-incidents-incident-updates}
`POST` `/organizations/{organization}/incidents/{incident}/updates` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Posts a public status update with a `status` and `message`, and moves the incident to that status (`resolved` resolves it). Status page subscribers are notified unless you send `notify_subscribers: false`; incidents imported from a third-party status page cannot be updated. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `incident_id`
path · required | integer | The incident's ID. | | `status`
body · required | string | One of: `investigating`, `identified`, `monitoring`, `resolved`. | | `message`
body · required | string | Must not be greater than 5000 characters. | | `notify_subscribers`
body | boolean | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/incidents/1/updates" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"status":"monitoring","message":"A fix is out; we are watching checkout recover."}' ``` **Response** · 201 — Created ```json { "monitoring_incident_id": 1, "status": "monitoring", "message": "A fix is out; we are watching checkout recover.", "user_id": 1, "created_at": "2026-01-15T12:00:00.000000Z", "id": 9, "incident": { "id": 1, "monitoring_check_id": 4, "started_at": "2026-01-15T11:20:00.000000Z", "resolved_at": null, "duration_seconds": null, "cause": "Upstream payment gateway timeout", "affected_probe_ids": null, "acknowledged_at": null, "acknowledged_by": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "description": null, "is_planned": false, "title": "Checkout returning 503", "status": "investigating", "impact": "major", "source": "monitor", "server_id": null, "cloud_server_alert_rule_id": null, "external_status": null, "external_url": null, "alert_rule_state_id": null, "folded_into": null, "unstable_at": null, "unstable_ended_at": null, "outage_count": 1, "down_since": null, "downtime_seconds": 0, "outages": null, "acknowledged_by_name": null }, "user": { "id": 1, "first_name": null, "last_name": null, "analytics_id": null } } ```
--- # Maintenance Planned maintenance windows, which pause alerts and show on status pages. **Common tasks** - [Plan a maintenance window](#post-organizations-organization-maintenance-windows) - [Mark it complete](#patch-organizations-organization-maintenance-windows-maintenancewindow-complete) **Only in the app:** Nothing. Maintenance windows can be fully managed with a token. **How it works in the app:** [help article](https://infranest.io/docs/maintenance). **Scopes:** Read with `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Maintenance windows](#maintenance-windows) 6
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Maintenance windows ### List maintenance windows {#get-organizations-organization-maintenance-windows}
`GET` `/organizations/{organization}/maintenance-windows` **Scope:** `monitoring.read` · **Changes data:** no A paginated list of your maintenance windows with the monitors, servers and tags each window covers, whether each window is in effect and its current or next occurrence, plus counts in `meta`. Filter with `search`, `tag` and `status` (`active`, `scheduled`, `recurring` or `completed`), and sort with `sort` (`schedule`, `name`, `monitors` or `status`) and `dir`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/maintenance-windows" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 3, "organization_id": 1, "name": "Nightly backup window", "description": "Alerts are suppressed while backups run.", "starts_at": null, "ends_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "daily", "recurrence_days": null, "start_time": "03:00", "duration_minutes": 45, "suppress_alerts": true, "status": "scheduled", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks_count": 0, "checks": [], "servers": [], "tags": [], "is_active": false, "occurrence_starts_at": "2026-01-16T03:00:00Z", "occurrence_ends_at": "2026-01-16T03:45:00Z", "schedule_timezone": "UTC" }, { "id": 2, "organization_id": 1, "name": "Load balancer certificate rotation", "description": "Replaced the certificate on web-lb and reloaded the listeners.", "starts_at": "2026-01-06T01:00:00.000000Z", "ends_at": "2026-01-06T01:30:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "suppress_alerts": true, "status": "completed", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks_count": 0, "checks": [], "servers": [], "tags": [], "is_active": false, "occurrence_starts_at": null, "occurrence_ends_at": null, "schedule_timezone": "UTC" } ], "first_page_url": "http://localhost/api/organizations/1/maintenance-windows?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/maintenance-windows?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/maintenance-windows?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/maintenance-windows", "per_page": 25, "prev_page_url": null, "to": 3, "total": 3, "meta": { "counts": { "total": 3, "active": 0, "scheduled": 1, "recurring": 1, "completed": 1 }, "tags": [] } } ```
--- ### Create a maintenance window {#post-organizations-organization-maintenance-windows}
`POST` `/organizations/{organization}/maintenance-windows` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Schedules maintenance for the monitors in `check_ids` and servers in `server_ids`; a window with no monitors applies to every monitor. A one-time window needs `starts_at` and `ends_at`, a `daily` or `weekly` one needs `start_time` and `duration_minutes`, and alerts are suppressed during it unless `suppress_alerts` is false. Subscribers of the affected status pages are told about the new window. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `name`
body · required | string | Must not be greater than 255 characters. | | `description`
body | string, nullable | Must not be greater than 2000 characters. | | `recurrence_type`
body | string, nullable | One of: `none`, `daily`, `weekly`. | | `recurrence_days`
body | array | Must be at least 0. Must not be greater than 6. | | `start_time`
body | string, nullable | Must be a valid date in the format H:i. | | `duration_minutes`
body | integer, nullable | Must be at least 1. Must not be greater than 1440. | | `starts_at`
body | string, nullable | Must be a valid date. | | `ends_at`
body | string, nullable | Must be a valid date. Must be a date after starts_at. | | `suppress_alerts`
body | boolean, nullable | | | `check_ids`
body | array | Must match an existing stored value. | | `server_ids`
body | array | Must match an existing stored value. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/maintenance-windows" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"Load balancer upgrade","starts_at":"2026-01-17T22:00:00Z","ends_at":"2026-01-17T23:00:00Z","suppress_alerts":true,"check_ids":[1]}' ``` **Response** · 201 — Created ```json { "name": "Load balancer upgrade", "description": null, "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "starts_at": "2026-01-17T22:00:00.000000Z", "ends_at": "2026-01-17T23:00:00.000000Z", "suppress_alerts": true, "organization_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 6, "checks": [ { "id": 1, "name": "Marketing site", "type": "http", "interval_minutes": 0, "pivot": { "maintenance_window_id": 6, "monitoring_check_id": 1 } } ], "servers": [], "tags": [], "is_active": false, "occurrence_starts_at": "2026-01-17T22:00:00Z", "occurrence_ends_at": "2026-01-17T23:00:00Z", "schedule_timezone": "UTC" } ```
--- ### Update a maintenance window {#patch-organizations-organization-maintenance-windows-maintenancewindow}
`PATCH` `/organizations/{organization}/maintenance-windows/{maintenanceWindow}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes the fields you send. Sending `check_ids` or `server_ids` replaces what the window covers, and an empty list clears it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `maintenanceWindow_id`
path · required | integer | The maintenanceWindow's ID. | | `name`
body | string | Must not be greater than 255 characters. | | `description`
body | string, nullable | Must not be greater than 2000 characters. | | `recurrence_type`
body | string, nullable | One of: `none`, `daily`, `weekly`. | | `recurrence_days`
body | array | Must be at least 0. Must not be greater than 6. | | `start_time`
body | string, nullable | Must be a valid date in the format H:i. | | `duration_minutes`
body | integer, nullable | Must be at least 1. Must not be greater than 1440. | | `starts_at`
body | string, nullable | Must be a valid date. | | `ends_at`
body | string, nullable | Must be a valid date. Must be a date after starts_at. | | `suppress_alerts`
body | boolean, nullable | | | `check_ids`
body | array | Must match an existing stored value. | | `server_ids`
body | array | Must match an existing stored value. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/maintenance-windows/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"description":"Primary database moves to PostgreSQL 17. Expect two minutes of read-only mode."}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Database cluster upgrade", "description": "Primary database moves to PostgreSQL 17. Expect two minutes of read-only mode.", "starts_at": "2026-01-18T02:00:00.000000Z", "ends_at": "2026-01-18T03:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "suppress_alerts": true, "status": "scheduled", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks": [], "servers": [], "tags": [], "is_active": false, "occurrence_starts_at": "2026-01-18T02:00:00Z", "occurrence_ends_at": "2026-01-18T03:00:00Z", "schedule_timezone": "UTC" } ```
--- ### Delete a maintenance window {#delete-organizations-organization-maintenance-windows-maintenancewindow}
`DELETE` `/organizations/{organization}/maintenance-windows/{maintenanceWindow}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Permanently deletes the maintenance window. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `maintenanceWindow_id`
path · required | integer | The maintenanceWindow's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/maintenance-windows/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Deleted." } ```
--- ### Complete a maintenance window {#patch-organizations-organization-maintenance-windows-maintenancewindow-complete}
`PATCH` `/organizations/{organization}/maintenance-windows/{maintenanceWindow}/complete` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Marks a window that is in progress or verifying as completed. Refused for a window in any other state. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `maintenanceWindow_id`
path · required | integer | The maintenanceWindow's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/maintenance-windows/1/complete" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 4, "organization_id": 1, "name": "Database upgrade", "description": null, "starts_at": "2026-01-15T11:50:00.000000Z", "ends_at": "2026-01-15T13:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "suppress_alerts": true, "status": "completed", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks": [], "servers": [], "tags": [], "is_active": false, "occurrence_starts_at": null, "occurrence_ends_at": null, "schedule_timezone": "UTC" } ```
--- ### Mark a maintenance window as verifying {#patch-organizations-organization-maintenance-windows-maintenancewindow-verifying}
`PATCH` `/organizations/{organization}/maintenance-windows/{maintenanceWindow}/verifying` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Moves a window that is in progress to `verifying`, while you check that everything works again. Refused for a window in any other state. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `maintenanceWindow_id`
path · required | integer | The maintenanceWindow's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/maintenance-windows/1/verifying" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 4, "organization_id": 1, "name": "Database upgrade", "description": null, "starts_at": "2026-01-15T11:50:00.000000Z", "ends_at": "2026-01-15T13:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "recurrence_type": "none", "recurrence_days": null, "start_time": null, "duration_minutes": null, "suppress_alerts": true, "status": "verifying", "notified_at": null, "source": "manual", "deploy_id": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "created_by": null, "checks": [], "servers": [], "tags": [], "is_active": true, "occurrence_starts_at": "2026-01-15T11:50:00Z", "occurrence_ends_at": "2026-01-15T13:00:00Z", "schedule_timezone": "UTC" } ```
--- # Status pages Public status pages and what they show. **Common tasks** - [List status pages](#get-organizations-organization-status-pages) - [Post an announcement](#post-organizations-organization-status-pages-statuspage-announcements) - [Add services from your monitors](#post-organizations-organization-status-pages-statuspage-components-bulk) **Only in the app:** Creating a status page, its custom domain and its look. **How it works in the app:** [help article](https://infranest.io/docs/status-pages). **Scopes:** Read with `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Status pages](#status-pages) 24
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Status pages ### List status pages {#get-organizations-organization-status-pages}
`GET` `/organizations/{organization}/status-pages` **Scope:** `monitoring.read` · **Changes data:** no Every status page in your organization with its component and subscriber counts, how many components are operational or degraded, which are down, and the page’s 90-day uptime. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "name": "Northwind Cloud Status", "slug": "northwind", "custom_domain": "status.northwind.example", "is_public": true, "password_hash": null, "description": "Live status of Northwind Cloud services.", "logo_url": null, "show_incident_history": true, "show_uptime_percentage": true, "subscriber_notifications_enabled": true, "theme": "light", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "overall_status": "major_outage", "custom_domain_status": "pending", "allow_indexing": true, "show_group_labels": true, "footer_text": null, "footer_url": null, "show_tags": false, "show_badge_branding": true, "edge_published": false, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "header_image_url": null, "density": "comfortable", "history_days": 90, "show_aggregate_bar": true, "show_maintenance": true, "show_third_party": true, "template": "default", "on_call_note": "Primary: Sam Okoye · +31 6 12 34 56 78 · #northwind-oncall on Slack. Escalation after 15m: Alex Rivera.", "header_style": "band", "accent_color": null, "header_gradient": true, "links": null, "subscriber_reply_to": null, "meta_title": null, "show_name_with_logo": true, "custom_domain_pending_since": null, "components_count": 8, "subscribers_count": 3, "components_up": 6, "down_components": [ "Checkout & payments" ], "degraded_count": 1, "uptime_90d": 100, "edge_publish_eligible": false } ] ```
--- ### Get a status page {#get-organizations-organization-status-pages-statuspage}
`GET` `/organizations/{organization}/status-pages/{statusPage}` **Scope:** `monitoring.read` · **Changes data:** no One status page with its settings, its components with the monitor and group behind each, and counts of components, announcements and subscribers. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Northwind Cloud Status", "slug": "northwind", "custom_domain": "status.northwind.example", "is_public": true, "password_hash": null, "description": "Live status of Northwind Cloud services.", "logo_url": null, "show_incident_history": true, "show_uptime_percentage": true, "subscriber_notifications_enabled": true, "theme": "light", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "overall_status": "major_outage", "custom_domain_status": "pending", "allow_indexing": true, "show_group_labels": true, "footer_text": null, "footer_url": null, "show_tags": false, "show_badge_branding": true, "edge_published": false, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "header_image_url": null, "density": "comfortable", "history_days": 90, "show_aggregate_bar": true, "show_maintenance": true, "show_third_party": true, "template": "default", "on_call_note": "Primary: Sam Okoye · +31 6 12 34 56 78 · #northwind-oncall on Slack. Escalation after 15m: Alex Rivera.", "header_style": "band", "accent_color": null, "header_gradient": true, "links": null, "subscriber_reply_to": null, "meta_title": null, "show_name_with_logo": true, "custom_domain_pending_since": null, "components_count": 8, "announcements_count": 2, "subscribers_count": 3, "edge_publish_eligible": false, "components": [ { "id": 1, "status_page_id": 1, "monitoring_check_id": 1, "display_name": "Website", "sort_order": 0, "status": "operational", "status_override": false, "show_uptime_bar": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "runbook_url": null, "status_page_component_group_id": 1, "description": null, "affects_overall_status": true, "group_name": "Public", "monitoring_check": { "id": 1, "name": "Marketing site", "type": "http", "interval_minutes": 0 }, "group": { "id": 1, "name": "Public" } }, { "id": 2, "status_page_id": 1, "monitoring_check_id": 2, "display_name": "Web app", "sort_order": 1, "status": "operational", "status_override": false, "show_uptime_bar": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "runbook_url": null, "status_page_component_group_id": 1, "description": null, "affects_overall_status": true, "group_name": "Public", "monitoring_check": { "id": 2, "name": "App — northwind-app.dev", "type": "http", "interval_minutes": 0 }, "group": { "id": 1, "name": "Public" } } ], "link_card_url": "http://localhost/api/status/northwind/card.png?v=66f618c615aff8ba" } ```
--- ### Get status page traffic {#get-organizations-organization-status-pages-statuspage-analytics}
`GET` `/organizations/{organization}/status-pages/{statusPage}/analytics` **Scope:** `monitoring.read` · **Changes data:** no Views, unique visitors and subscribe clicks over the last `days` (7, 30 or 90; default 30), compared with the period before, with the busiest day and a daily series. Counts only; no individual visits are recorded. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/analytics" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "days": 30, "totals": { "views": 0, "unique_visitors": 0, "subscribe_clicks": 0 }, "previous": { "views": 0, "unique_visitors": 0, "subscribe_clicks": 0 }, "change_pct": null, "busiest": null, "series": [ { "d": "2025-12-17", "views": 0, "unique_visitors": 0, "subscribe_clicks": 0 }, { "d": "2025-12-18", "views": 0, "unique_visitors": 0, "subscribe_clicks": 0 } ] } ```
--- ### List status page announcements {#get-organizations-organization-status-pages-statuspage-announcements}
`GET` `/organizations/{organization}/status-pages/{statusPage}/announcements` **Scope:** `monitoring.read` · **Changes data:** no The announcements you posted on this page, newest first, including ones scheduled for later or no longer shown. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/announcements" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 2, "status_page_id": 1, "title": "Released: v2.14.0", "body": "Faster search and a clearer billing page.", "show_from": "2026-01-15T09:00:00.000000Z", "show_until": "2026-01-22T12:00:00.000000Z", "created_at": "2026-01-15T09:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "kind": "information" }, { "id": 1, "status_page_id": 1, "title": "Scheduled maintenance this weekend", "body": "We'll be upgrading our database cluster on Saturday 02:00–03:00 UTC. Brief API slowdowns are possible; no downtime is expected.", "show_from": "2026-01-14T12:00:00.000000Z", "show_until": "2026-01-21T12:00:00.000000Z", "created_at": "2026-01-14T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "kind": "information" } ] ```
--- ### Post a status page announcement {#post-organizations-organization-status-pages-statuspage-announcements}
`POST` `/organizations/{organization}/status-pages/{statusPage}/announcements` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Publishes a notice with a `title` and `body` on the public page. `kind` is `information` or `warning`, and `show_from` and `show_until` limit when it is shown. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `title`
body · required | string | Must not be greater than 255 characters. | | `body`
body · required | string | Must not be greater than 10000 characters. | | `kind`
body | string, nullable | Two, on purpose: anything louder than a warning is an incident. One of: `information`, `warning`. | | `show_from`
body | string, nullable | Must be a valid date. | | `show_until`
body | string, nullable | Must be a valid date. Must be a date after or equal to show_from. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/status-pages/1/announcements" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"title":"New status page address","body":"This page now also answers at status.northwind.example."}' ``` **Response** · 201 — Created ```json { "title": "New status page address", "body": "This page now also answers at status.northwind.example.", "status_page_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 3 } ```
--- ### Update a status page announcement {#patch-organizations-organization-status-pages-statuspage-announcements-announcement}
`PATCH` `/organizations/{organization}/status-pages/{statusPage}/announcements/{announcement}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes the announcement’s title, body, kind or display window. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `announcement_id`
path · required | integer | The announcement's ID. | | `title`
body | string | Must not be greater than 255 characters. | | `body`
body | string | Must not be greater than 10000 characters. | | `kind`
body | string, nullable | One of: `information`, `warning`. | | `show_from`
body | string, nullable | Must be a valid date. | | `show_until`
body | string, nullable | Must be a valid date. Must be a date after or equal to show_from. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/status-pages/1/announcements/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"title":"Status page has moved","kind":"information"}' ``` **Response** · 200 ```json { "id": 1, "status_page_id": 1, "title": "Status page has moved", "body": "We'll be upgrading our database cluster on Saturday 02:00–03:00 UTC. Brief API slowdowns are possible; no downtime is expected.", "show_from": "2026-01-14T12:00:00.000000Z", "show_until": "2026-01-21T12:00:00.000000Z", "created_at": "2026-01-14T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "kind": "information" } ```
--- ### Delete a status page announcement {#delete-organizations-organization-status-pages-statuspage-announcements-announcement}
`DELETE` `/organizations/{organization}/status-pages/{statusPage}/announcements/{announcement}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes the announcement from the page permanently. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `announcement_id`
path · required | integer | The announcement's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/status-pages/1/announcements/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Deleted." } ```
--- ### Get custom domain CNAME setup {#get-organizations-organization-status-pages-statuspage-cname-setup}
`GET` `/organizations/{organization}/status-pages/{statusPage}/cname-setup` **Scope:** `monitoring.read` · **Changes data:** no Whether the page’s custom domain, or the one in `domain`, sits in a DNS zone you manage in InfraNest, whether InfraNest can create the CNAME record there for you, and whether it is already published. Includes the CNAME target to point the domain at. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/cname-setup" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "manageable": false, "matched": false, "published": false, "cname_target": "pages.infranest.io" } ```
--- ### List status page groups {#get-organizations-organization-status-pages-statuspage-component-groups}
`GET` `/organizations/{organization}/status-pages/{statusPage}/component-groups` **Scope:** `monitoring.read` · **Changes data:** no The page’s component groups in display order, each with how many components it holds. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/component-groups" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "status_page_id": 1, "name": "Public", "sort_order": 0, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "shows_platform_status": false, "components_count": 5 }, { "id": 2, "status_page_id": 1, "name": "Developer", "sort_order": 1, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "shows_platform_status": false, "components_count": 3 } ] ```
--- ### Create a status page group {#post-organizations-organization-status-pages-statuspage-component-groups}
`POST` `/organizations/{organization}/status-pages/{statusPage}/component-groups` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Adds a group with a `name` at the end of the page. Names must be unique within the page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `name`
body · required | string | Must not be greater than 255 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/status-pages/1/component-groups" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"Customer apps"}' ``` **Response** · 201 — Created ```json { "name": "Customer apps", "sort_order": 2, "status_page_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 3 } ```
--- ### Update a status page group {#patch-organizations-organization-status-pages-statuspage-component-groups-componentgroup}
`PATCH` `/organizations/{organization}/status-pages/{statusPage}/component-groups/{componentGroup}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Renames the group for every component in it. Setting `shows_platform_status` to true on one group clears it from every other group on the page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `componentGroup_id`
path · required | integer | The componentGroup's ID. | | `name`
body | string | Must not be greater than 255 characters. | | `shows_platform_status`
body | boolean | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/status-pages/1/component-groups/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"Customer-facing apps"}' ``` **Response** · 200 ```json { "id": 1, "status_page_id": 1, "name": "Customer-facing apps", "sort_order": 0, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "shows_platform_status": false } ```
--- ### Delete a status page group {#delete-organizations-organization-status-pages-statuspage-component-groups-componentgroup}
`DELETE` `/organizations/{organization}/status-pages/{statusPage}/component-groups/{componentGroup}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Removes the group but keeps its components, which become ungrouped. The response says how many were ungrouped. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `componentGroup_id`
path · required | integer | The componentGroup's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/status-pages/1/component-groups/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Deleted.", "components_ungrouped": 5 } ```
--- ### Reorder status page groups {#post-organizations-organization-status-pages-statuspage-component-groups-sort}
`POST` `/organizations/{organization}/status-pages/{statusPage}/component-groups/sort` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets the display order from `order`, a list of group IDs. Every ID must belong to this page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `order`
body | array | Must match an existing stored value. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/status-pages/1/component-groups/sort" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"order":[2,1]}' ``` **Response** · 200 ```json { "message": "Order updated." } ```
--- ### List status page components {#get-organizations-organization-status-pages-statuspage-components}
`GET` `/organizations/{organization}/status-pages/{statusPage}/components` **Scope:** `monitoring.read` · **Changes data:** no The page’s components in display order, each with its group, linked monitor and uptime bars over the page’s history window. A component with no monitor has no bars. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/components" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "status_page_id": 1, "monitoring_check_id": 1, "display_name": "Website", "sort_order": 0, "status": "operational", "status_override": false, "show_uptime_bar": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "runbook_url": null, "status_page_component_group_id": 1, "description": null, "affects_overall_status": true, "group_name": "Public", "monitoring_check": { "id": 1, "name": "Marketing site", "type": "http", "last_status": "up", "external_service_id": null, "interval_minutes": 0 }, "group": { "id": 1, "name": "Public", "sort_order": 0 }, "uptime_bars": [ { "date": "2025-10-17", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false }, { "date": "2025-10-18", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false } ] }, { "id": 2, "status_page_id": 1, "monitoring_check_id": 2, "display_name": "Web app", "sort_order": 1, "status": "operational", "status_override": false, "show_uptime_bar": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "runbook_url": null, "status_page_component_group_id": 1, "description": null, "affects_overall_status": true, "group_name": "Public", "monitoring_check": { "id": 2, "name": "App — northwind-app.dev", "type": "http", "last_status": "up", "external_service_id": null, "interval_minutes": 0 }, "group": { "id": 1, "name": "Public", "sort_order": 0 }, "uptime_bars": [ { "date": "2025-10-17", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false }, { "date": "2025-10-18", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false } ] } ] ```
--- ### Add a component to a status page {#post-organizations-organization-status-pages-statuspage-components}
`POST` `/organizations/{organization}/status-pages/{statusPage}/components` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Adds a component with a `display_name`, optionally linked to one of your monitors with `monitoring_check_id`. A `group_name` that does not exist yet creates that group, and a `runbook_url` must start with https:// and is only shown internally. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `display_name`
body · required | string | Must not be greater than 255 characters. | | `description`
body | string, nullable | What this service IS, for the reader who does not already know. Must not be greater than 255 characters. | | `group_name`
body | string, nullable | Must not be greater than 255 characters. | | `sort_order`
body | integer | Must be at least 0. | | `monitoring_check_id`
body | integer, nullable | Must match an existing stored value. | | `status`
body | string, nullable | | | `status_override`
body | boolean | | | `show_uptime_bar`
body | boolean | | | `affects_overall_status`
body | boolean | | | `runbook_url`
body | string, nullable | Followed by somebody in a hurry who will not read the scheme first. Must be a valid URL. Must start with one of https:// Must not be greater than 500 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/status-pages/1/components" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"display_name":"Payments API"}' ``` **Response** · 201 — Created ```json { "display_name": "Payments API", "description": null, "status_page_component_group_id": null, "sort_order": 8, "monitoring_check_id": null, "status": "operational", "status_override": false, "show_uptime_bar": true, "affects_overall_status": true, "runbook_url": null, "status_page_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 9, "group_name": null, "monitoring_check": null, "group": null } ```
--- ### Update a status page component {#patch-organizations-organization-status-pages-statuspage-components-component}
`PATCH` `/organizations/{organization}/status-pages/{statusPage}/components/{component}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes the fields you send, such as the name, description, linked monitor, status override, group or runbook link. A new `group_name` creates that group. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `component_id`
path · required | integer | The component's ID. | | `display_name`
body | string | Must not be greater than 255 characters. | | `description`
body | string, nullable | Must not be greater than 255 characters. | | `group_name`
body | string, nullable | Must not be greater than 255 characters. | | `sort_order`
body | integer | Must be at least 0. | | `monitoring_check_id`
body | integer, nullable | Must match an existing stored value. | | `status`
body | string, nullable | | | `status_override`
body | boolean | | | `show_uptime_bar`
body | boolean | | | `affects_overall_status`
body | boolean | | | `runbook_url`
body | string, nullable | Followed by somebody in a hurry who will not read the scheme first. Must be a valid URL. Must start with one of https:// Must not be greater than 500 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/status-pages/1/components/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"display_name":"Checkout","description":"Payments and order confirmation"}' ``` **Response** · 200 ```json { "id": 1, "status_page_id": 1, "monitoring_check_id": 1, "display_name": "Checkout", "sort_order": 0, "status": "operational", "status_override": false, "show_uptime_bar": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "runbook_url": null, "status_page_component_group_id": 1, "description": "Payments and order confirmation", "affects_overall_status": true, "group_name": "Public", "monitoring_check": { "id": 1, "name": "Marketing site", "type": "http", "last_status": "up", "external_service_id": null, "interval_minutes": 0 }, "group": { "id": 1, "name": "Public", "sort_order": 0 } } ```
--- ### Remove a component from a status page {#delete-organizations-organization-status-pages-statuspage-components-component}
`DELETE` `/organizations/{organization}/status-pages/{statusPage}/components/{component}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Deletes the component from the page. The monitor behind it is not affected. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `component_id`
path · required | integer | The component's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/status-pages/1/components/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Deleted." } ```
--- ### Add several monitors to a status page {#post-organizations-organization-status-pages-statuspage-components-bulk}
`POST` `/organizations/{organization}/status-pages/{statusPage}/components/bulk` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Adds a component for each of up to 50 monitors in `monitoring_check_ids`, named after the monitor and optionally placed in `group_name`. Monitors already on the page are skipped and listed in `skipped`; third-party monitors are added ungrouped and do not affect the page’s overall status. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `monitoring_check_ids`
body | array | Must match an existing stored value. | | `group_name`
body | string, nullable | Must not be greater than 255 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/status-pages/1/components/bulk" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"monitoring_check_ids":[1,2,3]}' ``` **Response** · 201 — Created ```json { "created": [], "skipped": [ 1, 2 ] } ```
--- ### Reorder status page components {#post-organizations-organization-status-pages-statuspage-components-sort}
`POST` `/organizations/{organization}/status-pages/{statusPage}/components/sort` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets the display order from `order`, a list of component IDs. Every ID must belong to this page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | | `order`
body | array | Must match an existing stored value. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/status-pages/1/components/sort" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"order":[8,7,6,5,4,3,2,1]}' ``` **Response** · 200 ```json { "message": "Order updated." } ```
--- ### Get the internal view of a status page {#get-organizations-organization-status-pages-statuspage-internal}
`GET` `/organizations/{organization}/status-pages/{statusPage}/internal` **Scope:** `monitoring.read` · **Changes data:** no The team-only view of the page: each component with its monitor, runbook link, regions reporting and 30 days of uptime, plus active incidents, the next maintenance and the on-call note. These internal fields are never shown on the public page, and this view needs a plan that includes private status pages. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/internal" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "page": { "id": 1, "name": "Northwind Cloud Status", "slug": "northwind", "logo_url": null, "is_public": true, "on_call_note": "Primary: Sam Okoye · +31 6 12 34 56 78 · #northwind-oncall on Slack. Escalation after 15m: Alex Rivera." }, "metrics": { "overall_status": "major_outage", "monitors_up": 6, "monitors_total": 8, "uptime_30d": 100, "next_maintenance": null }, "groups": [ { "name": "Public", "components": [ { "id": 1, "display_name": "Website", "status": "operational", "monitoring_check_id": 1, "monitor_name": "Marketing site", "runbook_url": null, "last_checked_at": "2026-01-15T11:58:00+00:00", "regions_reporting": null, "regions_total": null, "uptime_bars": [ { "date": "2025-12-16", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false }, { "date": "2025-12-17", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false } ] }, { "id": 2, "display_name": "Web app", "status": "operational", "monitoring_check_id": 2, "monitor_name": "App — northwind-app.dev", "runbook_url": null, "last_checked_at": "2026-01-15T11:58:00+00:00", "regions_reporting": null, "regions_total": null, "uptime_bars": [ { "date": "2025-12-16", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false }, { "date": "2025-12-17", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false } ] } ] }, { "name": "Developer", "components": [ { "id": 6, "display_name": "API", "status": "operational", "monitoring_check_id": 3, "monitor_name": "API health", "runbook_url": "https://wiki.northwind.example/runbooks/api", "last_checked_at": "2026-01-15T11:58:00+00:00", "regions_reporting": null, "regions_total": null, "uptime_bars": [ { "date": "2025-12-16", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false }, { "date": "2025-12-17", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false } ] }, { "id": 7, "display_name": "Search API", "status": "degraded", "monitoring_check_id": 6, "monitor_name": "Search API", "runbook_url": null, "last_checked_at": "2026-01-15T11:58:00+00:00", "regions_reporting": null, "regions_total": null, "uptime_bars": [ { "date": "2025-12-16", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false }, { "date": "2025-12-17", "uptime_pct": 100, "colour": "green", "incidents": [], "imported": false } ] } ] } ], "active_incidents": [ { "id": 1, "monitoring_check_id": 4, "title": "Checkout returning 503", "status": "investigating", "impact": "major", "started_at": "2026-01-15T11:20:00+00:00", "is_planned": false, "public_url": "/status/northwind/incidents/1" } ], "matrix_days": 30 } ```
--- ### List incidents and maintenance on a status page {#get-organizations-organization-status-pages-statuspage-other-notices}
`GET` `/organizations/{organization}/status-pages/{statusPage}/other-notices` **Scope:** `monitoring.read` · **Changes data:** no The incidents and maintenance windows that appear on this page automatically from its monitors, from the last 90 days or still open. Read-only; edit each one on the incident or maintenance window itself. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/other-notices" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "notices": [ { "kind": "incident", "id": 1, "title": "Checkout returning 503", "starts_at": "2026-01-15T11:20:00+00:00", "ends_at": null, "open": true, "href": "/monitoring/incidents" }, { "kind": "incident", "id": 2, "title": "Brief marketing-site blip", "starts_at": "2026-01-09T11:45:00+00:00", "ends_at": "2026-01-09T12:00:00+00:00", "open": false, "href": "/monitoring/incidents" } ] } ```
--- ### Get a status page overview {#get-organizations-organization-status-pages-statuspage-overview}
`GET` `/organizations/{organization}/status-pages/{statusPage}/overview` **Scope:** `monitoring.read` · **Changes data:** no The page at a glance: setup (components, custom domain, subscribers, whether it is public), uptime over the page’s history window, current status with active incidents, and the next maintenance and announcement due to end. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/overview" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "setup": { "components": { "count": 8, "linked": 8, "groups": 2 }, "domain": { "hostname": "status.northwind.example", "status": "pending" }, "subscribers": { "enabled": true, "count": 3 }, "is_public": true }, "uptime": { "days": 90, "pct": 100, "bars": [ { "d": "2025-10-17", "p": 100, "c": "green" }, { "d": "2025-10-18", "p": 100, "c": "green" } ] }, "now": { "overall_status": "major_outage", "components_total": 8, "by_status": { "operational": 6, "major_outage": 1, "degraded": 1 }, "incidents": [ { "id": 1, "monitoring_check_id": 4, "title": "Checkout returning 503", "status": "investigating", "impact": "major", "started_at": "2026-01-15T11:20:00+00:00", "is_planned": false, "public_url": "/status/northwind/incidents/1" } ] }, "upcoming": { "maintenance": null, "announcement_ends": { "id": 1, "title": "Scheduled maintenance this weekend", "ends_at": "2026-01-21T12:00:00+00:00" } } } ```
--- ### Get status page subscriber counts {#get-organizations-organization-status-pages-statuspage-subscribers}
`GET` `/organizations/{organization}/status-pages/{statusPage}/subscribers` **Scope:** `monitoring.read` · **Changes data:** no How many people subscribe to the page, how many have confirmed or are pending, a count per channel, and a 90-day growth curve. Counts only; use the subscriber list for individual entries. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/subscribers" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "total": 3, "confirmed": 3, "pending": 0, "channels": { "email": 3, "slack": 0, "teams": 0, "discord": 0 }, "growth": { "days": 90, "points": [ { "d": "2025-10-18", "total": 0, "confirmed": 0 }, { "d": "2025-10-19", "total": 0, "confirmed": 0 } ] } } ```
--- ### List status page subscribers {#get-organizations-organization-status-pages-statuspage-subscribers-list}
`GET` `/organizations/{organization}/status-pages/{statusPage}/subscribers/list` **Scope:** `monitoring.read` · **Changes data:** no The page’s subscribers, newest first, with their channel, label, confirmation date and whether delivery is failing. Filter with `q` (searches email addresses) and `status` (`confirmed` or `pending`), and page with `per_page` (10 to 100); reading the list is recorded in your audit log, and webhook addresses are never shown in full. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `statusPage_id`
path · required | integer | The statusPage's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/status-pages/1/subscribers/list" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": 3, "channel": "email", "label": "oncall@northwind.example", "confirmed_at": "2026-01-10T12:00:00+00:00", "subscribed_at": "2026-01-15T12:00:00+00:00", "undeliverable": null }, { "id": 2, "channel": "email", "label": "sre@northwind.example", "confirmed_at": "2025-12-29T12:00:00+00:00", "subscribed_at": "2026-01-15T12:00:00+00:00", "undeliverable": null } ], "total": 3, "per_page": 25, "current_page": 1, "last_page": 1 } ```
--- # Alerts and notifications Alert channels, alert rules and notification settings. Read only: changing who gets alerted stays in the app. **Common tasks** - [List alert channels](#get-organizations-organization-alert-channels) **Only in the app:** Adding or changing alert channels and who gets alerted. **How it works in the app:** [help article](https://infranest.io/docs/alerts). **Scopes:** Read with `monitoring.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Alert channels](#alert-channels) 2
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Alert channels ### List alert destinations {#get-organizations-organization-alert-channels}
`GET` `/organizations/{organization}/alert-channels` **Scope:** `monitoring.read` · **Changes data:** no Every place the organization sends alerts, such as email, Slack or Telegram, with how many checks use each and how many alerts it missed while it refused messages. Pass `personal=1` for only your own personal destinations. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/alert-channels" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "name": "Ops team — email", "type": "email", "settings": { "email": "ops@northwind.example" }, "enabled": true, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subscriptions": [ "monitoring_down", "monitoring_recovered" ], "is_personal": false, "user_id": null, "format": "html", "last_sent_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "consecutive_failures": 0, "last_failure_at": null, "last_error": null, "last_error_type": null, "broken_since": null, "broken_notified_at": null, "broken_notify_count": 0, "awaiting_confirmation_at": null, "categories": null, "telegram_link_id": null, "telegram_chat_id": null, "slack_installation_id": null, "slack_channel_id": null, "checks_count": 0, "missed_alerts_count": 0, "missed_alerts_min_created_at": null, "needs_telegram_link": false }, { "id": 2, "organization_id": 1, "name": "On-call — critical only", "type": "email", "settings": { "email": "oncall@northwind.example", "critical_only": true }, "enabled": true, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subscriptions": [ "monitoring_down", "domain_expiry_critical" ], "is_personal": false, "user_id": null, "format": "html", "last_sent_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "consecutive_failures": 0, "last_failure_at": null, "last_error": null, "last_error_type": null, "broken_since": null, "broken_notified_at": null, "broken_notify_count": 0, "awaiting_confirmation_at": null, "categories": null, "telegram_link_id": null, "telegram_chat_id": null, "slack_installation_id": null, "slack_channel_id": null, "checks_count": 0, "missed_alerts_count": 0, "missed_alerts_min_created_at": null, "needs_telegram_link": false } ] ```
--- ### Get an alert destination {#get-organizations-organization-alert-channels-alertchannel}
`GET` `/organizations/{organization}/alert-channels/{alertChannel}` **Scope:** `monitoring.read` · **Changes data:** no One alert destination with the checks that send to it. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `alertChannel_id`
path · required | integer | The alertChannel's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/alert-channels/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Ops team — email", "type": "email", "settings": { "email": "ops@northwind.example" }, "enabled": true, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "subscriptions": [ "monitoring_down", "monitoring_recovered" ], "is_personal": false, "user_id": null, "format": "html", "last_sent_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "consecutive_failures": 0, "last_failure_at": null, "last_error": null, "last_error_type": null, "broken_since": null, "broken_notified_at": null, "broken_notify_count": 0, "awaiting_confirmation_at": null, "categories": null, "telegram_link_id": null, "telegram_chat_id": null, "slack_installation_id": null, "slack_channel_id": null, "checks_count": 0, "checks": [] } ```
--- # Automations Automations and the connections they use. Read only. **Common tasks** - [List automations](#get-organizations-organization-automations) **Only in the app:** Creating and changing automations. **How it works in the app:** [help article](https://infranest.io/docs/automations). **Scopes:** Read with `monitoring.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Automation connections](#automation-connections) 1 · [Automations](#automations) 3
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Automation connections ### List automation connections {#get-organizations-organization-automation-connections}
`GET` `/organizations/{organization}/automation-connections` **Scope:** `monitoring.read` · **Changes data:** no The accounts automations can act through, such as GitHub, Jira or PagerDuty, with each one’s status, whether it is paused and whether its triggers can fire. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/automation-connections" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "provider": "pagerduty", "kind": "oauth", "label": "Northwind on-call", "meta": { "service_name": "Platform — production", "account": "northwind", "auth": "oauth", "region": "us", "authorized_by": { "name": "Alex Rivera", "email": "demo@example.com" }, "webhook_subscription_id": "PD3WHS1" }, "status": "active", "paused": false, "triggers_on": false, "created_at": "2026-01-15T12:00:00+00:00" }, { "id": 2, "provider": "github", "kind": "github_app", "label": "northwind-cloud", "meta": { "installation_id": "20000001", "account": "northwind-cloud" }, "status": "active", "paused": false, "triggers_on": false, "created_at": "2026-01-15T12:00:00+00:00" } ] ```
## Automations ### List automations {#get-organizations-organization-automations}
`GET` `/organizations/{organization}/automations` **Scope:** `monitoring.read` · **Changes data:** no Every automation, newest first, with its tags and run statistics: runs in the last 30 and 7 days, success rate, average duration and the last run’s status. Test runs are not counted. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/automations" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "name": "Release published → announce on status page", "trigger_event": "github.release", "conditions": [ { "op": "eq", "field": "action", "value": "published" }, { "op": "eq", "field": "prerelease", "value": "false" } ], "actions": [ { "action": "create_status_announcement", "config": { "body": "{{name}}.", "title": "Released: {{tag}}", "status_page_id": 1 } } ], "enabled": true, "cooldown_seconds": 0, "consecutive_failures": 0, "disabled_reason": null, "last_triggered_at": null, "created_by": 1, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "condition_match": "all", "run_as": "owner", "trigger_kind": "event", "schedule": null, "trigger_events": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "disabled_detail": null, "template_key": null, "tags": [], "runs_30d": 0, "runs_7d": 0, "success_rate": null, "avg_duration_ms": null, "last_status": null, "last_run_at": null, "spark": [ { "total": 0, "failed": 0 }, { "total": 0, "failed": 0 } ] }, { "id": 2, "organization_id": 1, "name": "PR merged to main → run deploy workflow", "trigger_event": "github.pull_request", "conditions": [ { "op": "eq", "field": "merged", "value": "true" }, { "op": "eq", "field": "base_branch", "value": "main" } ], "actions": [ { "action": "require_approval", "config": { "note": "Production deploy" } }, { "action": "github_trigger_workflow", "config": { "ref": "main", "repo": "northwind-cloud/app", "workflow": "deploy.yml", "connection_id": 2 } } ], "enabled": true, "cooldown_seconds": 0, "consecutive_failures": 0, "disabled_reason": null, "last_triggered_at": null, "created_by": 1, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "condition_match": "all", "run_as": "owner", "trigger_kind": "event", "schedule": null, "trigger_events": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "disabled_detail": null, "template_key": null, "tags": [], "runs_30d": 5, "runs_7d": 5, "success_rate": 40, "avg_duration_ms": 3000, "last_status": "awaiting_approval", "last_run_at": "2026-01-15T09:00:00.000000Z", "spark": [ { "total": 1, "failed": 1 }, { "total": 0, "failed": 0 } ] } ] ```
--- ### Get an automation {#get-organizations-organization-automations-automationrule}
`GET` `/organizations/{organization}/automations/{automationRule}` **Scope:** `monitoring.read` · **Changes data:** no One automation with its trigger, conditions, actions and tags, plus its 20 most recent runs and their steps. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `automationRule_id`
path · required | integer | The automationRule's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/automations/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Release published → announce on status page", "trigger_event": "github.release", "conditions": [ { "op": "eq", "field": "action", "value": "published" }, { "op": "eq", "field": "prerelease", "value": "false" } ], "actions": [ { "action": "create_status_announcement", "config": { "body": "{{name}}.", "title": "Released: {{tag}}", "status_page_id": 1 } } ], "enabled": true, "cooldown_seconds": 0, "consecutive_failures": 0, "disabled_reason": null, "last_triggered_at": null, "created_by": 1, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "condition_match": "all", "run_as": "owner", "trigger_kind": "event", "schedule": null, "trigger_events": null, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "disabled_detail": null, "template_key": null, "tags": [], "runs": [] } ```
--- ### List an automation’s runs {#get-organizations-organization-automations-automationrule-runs}
`GET` `/organizations/{organization}/automations/{automationRule}/runs` **Scope:** `monitoring.read` · **Changes data:** no The automation’s run history with each run’s steps, newest first, 25 per page. Filter with `status` and the dates `from` and `to`. Unless you may edit automations, what triggered each run is left out and `details_hidden` is true. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `automationRule_id`
path · required | integer | The automationRule's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/automations/1/runs" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "details_hidden": false, "current_page": 1, "data": [], "first_page_url": "http://localhost/api/organizations/1/automations/1/runs?page=1", "from": null, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/automations/1/runs?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/automations/1/runs?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/automations/1/runs", "per_page": 25, "prev_page_url": null, "to": null, "total": 0 } ```
--- # Integrations Connected providers and messaging apps. Read only: credentials are managed in the app. **Common tasks** - [List connected providers](#get-organizations-organization-integrations) **Only in the app:** Connecting, reconnecting and removing providers and their credentials. **How it works in the app:** [help article](https://infranest.io/docs/integrations). **Scopes:** Read with `cloud.read` or `monitoring.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Integrations](#integrations) 5 · [Provider credentials](#provider-credentials) 4 · [Webhook endpoints](#webhook-endpoints) 1
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Integrations ### List connected integrations {#get-organizations-organization-integrations}
`GET` `/organizations/{organization}/integrations` **Scope:** `cloud.read` · **Changes data:** no Everything the organization has connected — cloud, DNS, registrar and certificate accounts, alert destinations, outbound webhooks, automation connections and calendars — each with its status and health. Also returns summary counts and the integrations your plan does not include (`locks`). | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/integrations" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "connections": [ { "id": 1, "kind": "provider", "provider": "hetzner", "integration_key": "hetzner", "name": "Hetzner — Production", "categories": [ "cloud", "dns" ], "capabilities": [ "cloud", "dns" ], "counts": { "servers": 6, "zones": 2, "domains": 0, "certs": 1 }, "suspended_capabilities": [], "incident": null, "auth_kind": "api_token", "enabled": true, "status": "connected", "locked": false, "health": { "status": "ok", "success_rate": 100, "total_calls": 97, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" }, "rate_limited": null, "last_activity_at": "2026-01-15T12:00:00Z" }, { "id": 2, "kind": "provider", "provider": "cloudflare", "integration_key": "cloudflare", "name": "Cloudflare DNS", "categories": [ "dns", "domains" ], "capabilities": [ "dns", "registrar" ], "counts": { "servers": 0, "zones": 5, "domains": 0, "certs": 1 }, "suspended_capabilities": [], "incident": null, "auth_kind": "api_token", "enabled": true, "status": "connected", "locked": false, "health": { "status": "ok", "success_rate": 100, "total_calls": 49, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" }, "rate_limited": null, "last_activity_at": "2026-01-15T12:00:00Z" } ], "summary": { "total": 22, "connected": 16, "degraded": 3, "failing": 2, "disabled": 1, "issues": 5, "locked": 0 }, "locks": [] } ```
--- ### Get an integration {#get-organizations-organization-integrations-provider}
`GET` `/organizations/{organization}/integrations/{provider}` **Scope:** `cloud.read` · **Changes data:** no What the integration offers, the organization’s connections to it, and whether your plan allows connecting it. Returns 404 for an unknown integration. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `provider`
path · required | string | The provider. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/integrations/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "provider": { "key": "hetzner", "label": "Hetzner", "logo": "hetzner", "capabilities": [ "cloud", "dns" ], "categories": [ "cloud", "dns" ], "dns_scope": "any", "features": [ "zone_discovery", "record_ids" ], "environments": [], "oauth": false, "one_click": false, "needs_allowlist": false, "featured": true, "premium": false, "min_plan": null, "max_connections": null, "credentials": [ { "key": "api_token", "label": "integrations.providers.hetzner.apiToken.label", "secret": true, "multiline": false, "help": "integrations.providers.hetzner.apiToken.help", "help_params": { "aws_account_id": "InfraNest's AWS account (set AWS_INFRANEST_ACCOUNT_ID)" }, "placeholder": "LRK9mXv2QpT8sYw4…", "prefill": null, "type": "text", "options": null, "depends_on": null, "show_when": null, "default": null } ], "kind": "provider", "content": null }, "connections": [ { "id": 1, "kind": "provider", "provider": "hetzner", "integration_key": "hetzner", "name": "Hetzner — Production", "categories": [ "cloud", "dns" ], "capabilities": [ "cloud", "dns" ], "counts": { "servers": 6, "zones": 2, "domains": 0, "certs": 1 }, "suspended_capabilities": [], "incident": null, "auth_kind": "api_token", "enabled": true, "status": "connected", "locked": false, "health": { "status": "ok", "success_rate": 100, "total_calls": 97, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" }, "rate_limited": null, "last_activity_at": "2026-01-15T12:00:00Z" } ], "gate": { "locked": false, "reason": null, "premium": false, "min_plan": null, "min_plan_name": null, "current_plan": "pro", "current_plan_name": "Pro", "max_connections": null, "used": 1, "grandfathered": false, "included": false } } ```
--- ### Get an integration account {#get-organizations-organization-integrations-provider-accounts-id}
`GET` `/organizations/{organization}/integrations/{provider}/accounts/{id}` **Scope:** `cloud.read` · **Changes data:** no One connected account of an integration, whatever kind it is, with its status and health. Returns 404 when the account does not exist or does not belong to this integration and organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `provider`
path · required | string | The provider. | | `id`
path · required | integer | The id's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/integrations/1/accounts/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "removing": false, "is_oauth": false, "auth_kind": "api_token", "oauth": null, "error_email_enabled": true, "alerts_muted_until": null, "incident": null, "notices": [], "spending": null, "capabilities": [ "cloud", "dns" ], "supported_capabilities": [ "cloud", "dns" ], "cloud_capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "dns_capabilities": { "record_writes": true, "bulk_replace": false, "zone_create": true, "zone_delete": true, "record_proxying": false, "signing": false }, "dns_scope": "any", "cloud_firewall_protocols": [ "tcp", "udp" ], "enabled_capabilities": null, "suspended_capabilities": [], "resource_counts": { "dns": 2, "certificates": 1, "cloud": 6 }, "verified_at": "2025-10-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "fields": [ "api_token" ], "environment": null, "field_values": { "api_token": { "set": true, "secret": true, "display": "••••oken" } }, "context": [], "health": { "status": "ok", "success_rate": 100, "total_calls": 97, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" }, "registrar_operations": null, "scope_check": null, "scope_checked_at": null, "disabled_operations": [], "api_base": "/organizations/1/provider-credentials/1" } ```
--- ### List integration activity {#get-organizations-organization-integrations-provider-activity}
`GET` `/organizations/{organization}/integrations/{provider}/activity` **Scope:** `cloud.read` · **Changes data:** no The calls made to and received from this integration, newest first, paginated with `per_page` (5 to 100, default 20). Filter with `status` (`success` or `error`), `capability`, `direction`, `credential_id` and `search`; `meta` lists the accounts and filter values available. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `provider`
path · required | string | The provider. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/integrations/1/activity" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": 9, "credential_id": 1, "capability": "cloud", "direction": "outbound", "method": "GET", "endpoint": "/v1/servers", "action": "listServers", "success": true, "http_status": 200, "duration_ms": 60, "error_type": null, "error_message": null, "created_at": "2026-01-15T12:00:00.000000Z" }, { "id": 10, "credential_id": 1, "capability": "cloud", "direction": "outbound", "method": "GET", "endpoint": "/v1/volumes", "action": "listVolumes", "success": true, "http_status": 200, "duration_ms": 112, "error_type": null, "error_message": null, "created_at": "2026-01-15T11:57:00.000000Z" } ], "total": 672, "last_page": 34, "meta": { "accounts": [ { "id": 1, "label": "Hetzner — Production", "context": [] } ], "capabilities": [ "cloud" ], "directions": [ "outbound" ], "rate_limit": null } } ```
--- ### Export integration activity as CSV {#get-organizations-organization-integrations-provider-activity-export}
`GET` `/organizations/{organization}/integrations/{provider}/activity/export` **Scope:** `cloud.read` · **Changes data:** no Downloads every call logged for this integration as a CSV file, newest first. Takes the same `status`, `capability`, `direction`, `credential_id` and `search` filters as the activity list. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `provider`
path · required | string | The provider. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/integrations/1/activity/export" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 — The integration's activity log as CSV. (`text/csv`)
## Provider credentials ### List provider accounts {#get-organizations-organization-provider-credentials}
`GET` `/organizations/{organization}/provider-credentials` **Scope:** `cloud.read` · **Changes data:** no Every connected cloud, DNS, registrar and certificate account, with its health and how many zones, domains, certificates and servers it holds. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/provider-credentials" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "removing": false, "is_oauth": false, "auth_kind": "api_token", "oauth": null, "error_email_enabled": true, "alerts_muted_until": null, "incident": null, "notices": [], "spending": null, "capabilities": [ "cloud", "dns" ], "supported_capabilities": [ "cloud", "dns" ], "cloud_capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "dns_capabilities": { "record_writes": true, "bulk_replace": false, "zone_create": true, "zone_delete": true, "record_proxying": false, "signing": false }, "dns_scope": "any", "cloud_firewall_protocols": [ "tcp", "udp" ], "enabled_capabilities": null, "suspended_capabilities": [], "resource_counts": { "dns": 2, "certificates": 1, "cloud": 6 }, "verified_at": "2025-10-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "fields": [ "api_token" ], "environment": null, "field_values": { "api_token": { "set": true, "secret": true, "display": "••••oken" } }, "context": [], "health": { "status": "ok", "success_rate": 100, "total_calls": 97, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" } }, { "id": 2, "provider": "cloudflare", "label": "Cloudflare DNS", "enabled": true, "removing": false, "is_oauth": false, "auth_kind": "api_token", "oauth": null, "error_email_enabled": true, "alerts_muted_until": null, "incident": null, "notices": [], "spending": null, "capabilities": [ "dns", "registrar" ], "supported_capabilities": [ "dns", "registrar" ], "cloud_capabilities": [], "dns_capabilities": { "record_writes": true, "bulk_replace": false, "zone_create": true, "zone_delete": true, "record_proxying": true, "signing": true }, "dns_scope": "any", "cloud_firewall_protocols": [], "enabled_capabilities": null, "suspended_capabilities": [], "resource_counts": { "dns": 5, "registrar": 0, "certificates": 1 }, "verified_at": "2025-10-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "fields": [ "api_token", "account_id" ], "environment": null, "field_values": { "api_token": { "set": true, "secret": true, "display": "••••oken" }, "account_id": { "set": true, "secret": false, "display": "demoaccount0000000000000000cf01" } }, "context": [], "health": { "status": "ok", "success_rate": 100, "total_calls": 49, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" } } ] ```
--- ### Get a provider account {#get-organizations-organization-provider-credentials-providercredential}
`GET` `/organizations/{organization}/provider-credentials/{providerCredential}` **Scope:** `cloud.read` · **Changes data:** no One connected provider account with its health, how many zones, domains, certificates and servers it holds, the registrar operations it supports and the result of its last permission check. Secrets are never returned. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `providerCredential_id`
path · required | integer | The providerCredential's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/provider-credentials/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "provider": "hetzner", "label": "Hetzner — Production", "enabled": true, "removing": false, "is_oauth": false, "auth_kind": "api_token", "oauth": null, "error_email_enabled": true, "alerts_muted_until": null, "incident": null, "notices": [], "spending": null, "capabilities": [ "cloud", "dns" ], "supported_capabilities": [ "cloud", "dns" ], "cloud_capabilities": { "native_delete_protection": true, "server_destroy": true, "server_destroy_is_cancellation": false, "server_create": true, "server_rename": true, "console": true, "snapshots": true, "backups": true, "isos": true, "rescue": true, "placement_groups": true, "public_network_toggle": true, "scheduled_backups": true, "private_networks": true, "private_network_attach": true, "network_join_on_create": false, "network_attach_ip_choice": true, "private_network_unfiltered": true, "floating_ips": true, "networks_creatable": true, "network_cidr": true, "network_subnets": true, "network_routes": true, "network_rename": true, "network_ip_range_extendable": true, "firewalls_creatable": true, "firewall_multi_server": true, "firewall_renamable": true, "firewall_outbound": true, "firewall_ipv6": true, "firewall_dynamic_ip": true, "server_firewall": false, "load_balancers_creatable": true, "load_balancer_rename": true, "load_balancer_algorithm": true, "load_balancer_services": true, "load_balancer_targets": true, "volumes_creatable": true, "volume_resize": true, "volume_rename": true, "volume_native_delete_protection": true, "network_native_delete_protection": true, "user_data": true, "install_flavour": false, "images_per_plan": false, "metrics": true, "metrics_disk_throughput": true, "rdns_subnet": true, "rdns_address_list": false, "reverse_dns": true, "snapshot_description": true, "snapshot_protection": true }, "dns_capabilities": { "record_writes": true, "bulk_replace": false, "zone_create": true, "zone_delete": true, "record_proxying": false, "signing": false }, "dns_scope": "any", "cloud_firewall_protocols": [ "tcp", "udp" ], "enabled_capabilities": null, "suspended_capabilities": [], "resource_counts": { "dns": 2, "certificates": 1, "cloud": 6 }, "verified_at": "2025-10-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "fields": [ "api_token" ], "environment": null, "field_values": { "api_token": { "set": true, "secret": true, "display": "••••oken" } }, "context": [], "health": { "status": "ok", "success_rate": 100, "total_calls": 97, "last_error_at": null, "last_success_at": "2026-01-15T12:00:00Z" }, "registrar_operations": null, "scope_check": null, "scope_checked_at": null, "disabled_operations": [] } ```
--- ### List a provider account’s call log {#get-organizations-organization-provider-credentials-providercredential-logs}
`GET` `/organizations/{organization}/provider-credentials/{providerCredential}/logs` **Scope:** `cloud.read` · **Changes data:** no The most recent 200 calls InfraNest made with this account, newest first. Filter with `status` (`success` or `failure`) and the dates `from` and `to`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `providerCredential_id`
path · required | integer | The providerCredential's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/provider-credentials/1/logs" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 9, "provider": "hetzner", "capability": "cloud", "method": "GET", "endpoint": "/v1/servers", "success": true, "http_status": 200, "duration_ms": 60, "error_type": null, "error_message": null, "provider_request_id": null, "request_summary": null, "response_summary": null, "created_at": "2026-01-15T12:00:00.000000Z" }, { "id": 10, "provider": "hetzner", "capability": "cloud", "method": "GET", "endpoint": "/v1/volumes", "success": true, "http_status": 200, "duration_ms": 112, "error_type": null, "error_message": null, "provider_request_id": null, "request_summary": null, "response_summary": null, "created_at": "2026-01-15T11:57:00.000000Z" } ] ```
--- ### Get provider account health {#get-organizations-organization-provider-credentials-health}
`GET` `/organizations/{organization}/provider-credentials/health` **Scope:** `cloud.read` · **Changes data:** no How many enabled provider accounts there are (`total`) and how many are currently degraded or down (`unhealthy`). | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/provider-credentials/health" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "unhealthy": 2, "total": 10 } ```
## Webhook endpoints ### Get the outbound webhook JSON Schema {#get-organizations-organization-webhook-endpoints-schema}
`GET` `/organizations/{organization}/webhook-endpoints/schema` **Scope:** `monitoring.read` · **Changes data:** no The JSON Schema of the payload InfraNest sends to your webhook endpoints, for tools that type a payload from a schema. Narrow it with `events[]`; with none, it covers every event. `events` lists every event you can name. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `events`
body | array | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/webhook-endpoints/schema" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "schema": { "type": "object", "properties": { "event": { "title": "Event", "type": "string" }, "created_at": { "title": "Created at", "type": "string", "format": "date-time" }, "data": { "type": "object", "properties": { "acknowledged_by": { "title": "Acknowledged by", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "action": { "title": "Action", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "actor": { "title": "Actor", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "agent": { "title": "Agent", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "asn_org": { "title": "ASN org", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "assignee": { "title": "Assignee", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "assignees": { "title": "Assignees", "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "null" } ] }, "auction": { "title": "Auction", "anyOf": [ { "type": "object" }, { "type": "null" } ] }, "author": { "title": "Author", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "base_branch": { "title": "Base branch", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "branch": { "title": "Branch", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "change": { "title": "Change", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "changed_fields": { "title": "Changed fields", "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "null" } ] }, "check_name": { "title": "Check name", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "comment": { "title": "Comment", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "commits": { "title": "Commits", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "conclusion": { "title": "Conclusion", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "context": { "title": "Context", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "country_code": { "title": "Country code", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "created": { "title": "Created", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "creator": { "title": "Creator", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "current_ipv4": { "title": "Current IPv4", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "current_ipv6": { "title": "Current IPv6", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "custom_fields": { "title": "Custom fields", "anyOf": [ { "type": "object" }, { "type": "null" } ] }, "days_left": { "title": "Days left", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "deleted": { "title": "Deleted", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "deleted_in": { "title": "Deleted in", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "deployment_id": { "title": "Deployment ID", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "description": { "title": "Description", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "domain": { "title": "Domain", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "draft": { "title": "Draft", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "drop_eta": { "title": "Drop ETA", "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ] }, "duration_minutes": { "title": "Duration minutes", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "email": { "title": "Email", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "endpoint": { "title": "Endpoint", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "environment": { "title": "Environment", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "environment_url": { "title": "Environment URL", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "escalation_policy": { "title": "Escalation policy", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "expires_at": { "title": "Expires at", "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ] }, "expiry_may_be_stale": { "title": "Expiry may be stale", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "forced": { "title": "Forced", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "head_branch": { "title": "Head branch", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "head_sha": { "title": "Head SHA", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "host": { "title": "Host", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "identifier": { "title": "Identifier", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "incident_title": { "title": "Incident title", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "incident_type": { "title": "Incident type", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "incident_url": { "title": "Incident URL", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "ip": { "title": "IP", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "ip_version": { "title": "IP version", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "is_hosting": { "title": "Is hosting", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "is_pull_request": { "title": "Is pull request", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "issue_type": { "title": "Issue type", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "issuer": { "title": "Issuer", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "issues": { "title": "Issues", "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "null" } ] }, "label": { "title": "Label", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "labels": { "title": "Labels", "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "null" } ] }, "log_url": { "title": "Log URL", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "merged": { "title": "Merged", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "message": { "title": "Message", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "monitor_type": { "title": "Monitor type", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "name": { "title": "Name", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "new_nameservers": { "title": "New nameservers", "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "null" } ] }, "note": { "title": "Note", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "note_trimmed": { "title": "Note trimmed", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "number": { "title": "Number", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "old_nameservers": { "title": "Old nameservers", "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "null" } ] }, "owner": { "title": "Owner", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "owner_email": { "title": "Owner email", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "owner_name": { "title": "Owner name", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "pd_connection_id": { "title": "PagerDuty connection ID", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "pd_incident_id": { "title": "PagerDuty incident ID", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "prerelease": { "title": "Prerelease", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "previous_state": { "title": "Previous state", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "priority": { "title": "Priority" }, "production_environment": { "title": "Production environment", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "project": { "title": "Project", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "provider": { "title": "Provider", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "pusher": { "title": "Pusher", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "record_action": { "title": "Record action", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "record_name": { "title": "Record name", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "record_type": { "title": "Record type", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "records": { "title": "Records", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "ref": { "title": "Ref", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "ref_type": { "title": "Ref type", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "registrar": { "title": "Registrar", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "repo": { "title": "Repo", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "responder": { "title": "Responder", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "responder_message": { "title": "Responder message", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "responder_state": { "title": "Responder state", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "reviewer": { "title": "Reviewer", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "run_attempt": { "title": "Run attempt", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "run_id": { "title": "Run ID", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "run_number": { "title": "Run number", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "server_name": { "title": "Server name", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "service": { "title": "Service", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "sha": { "title": "SHA", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "size_gb": { "title": "Size GB", "anyOf": [ { "type": "integer" }, { "type": "null" } ] }, "state": { "title": "State", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "status": { "title": "Status", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "status_page": { "title": "Status page", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "subject": { "title": "Subject", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "tag": { "title": "Tag", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "task": { "title": "Task", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "team": { "title": "Team", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "title": { "title": "Title", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "tld": { "title": "TLD", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "transient_environment": { "title": "Transient environment", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "trigger_event": { "title": "Triggering event", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "triggering_actor": { "title": "Triggering actor", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": { "title": "Type", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "unused": { "title": "Unused", "anyOf": [ { "type": "boolean" }, { "type": "null" } ] }, "update_status": { "title": "Update status", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "urgency": { "title": "Urgency", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "url": { "title": "URL", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "via": { "title": "Via", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "workflow": { "title": "Workflow", "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "zone": { "title": "Zone", "anyOf": [ { "type": "string" }, { "type": "null" } ] } } } }, "required": [ "event", "created_at" ] }, "events": [ "domain.registered", "domain.expiring" ] } ```
--- # Drop catch Watched domains. **Common tasks** - [List watched domains](#get-organizations-organization-watched-domains) **Only in the app:** Adding watched domains and placing backorders. **How it works in the app:** [help article](https://infranest.io/docs/drop-catch). **Scopes:** Read with `drop-catch.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Watched domains](#watched-domains) 8 · [Whois servers](#whois-servers) 1
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Watched domains ### List watched domains {#get-organizations-organization-watched-domains}
`GET` `/organizations/{organization}/watched-domains` **Scope:** `drop-catch.read` · **Changes data:** no Your watched domains, paginated with `per_page` (default 25, at most 200) and sorted with `sort` and `dir`. Filter with `search`, `tag` and flags such as `active`, `available`, `registered`, `failed`, `paused` and `in_auction`; `meta` carries counts per status. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 3, "organization_id": 1, "name": "northwind", "tld": "app", "priority": 5, "status": "available", "auto_register": true, "registrar": null, "consecutive_available_count": 0, "notes": null, "last_checked_at": "2026-01-15T09:00:00.000000Z", "available_since": null, "registered_at": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "next_check_at": "2026-01-15T13:00:00.000000Z", "observed_expires_at": null, "observed_status": null, "drop_eta": null, "date_visibility": null, "score": 88, "score_breakdown": null, "scored_at": "2026-01-15T12:00:00.000000Z", "registrar_preferences": null, "lifecycle_stage": "available", "observed_created_at": null, "contact_id": null, "extended_attributes": null, "catchability": 90, "catchability_signals": { "verdict": "available", "factors": [] }, "dns_signals": null, "observed_registrar": null, "observed_updated_at": null, "observed_nameservers": null, "lifecycle_progress": null, "http_signals": null, "enrichment_signals": null, "burst_until": null, "parallel_race": false, "in_auction": false, "auction": null, "backorder": false, "backorder_credential_id": null, "backorder_status": null, "whois_raw_at": null, "drop_eta_uncertainty_days": null, "lifecycle_source": null, "lifecycle_stage_since": "2026-01-10T12:00:00.000000Z", "published_drop_at": null, "deleted_at_registry": null, "dropping_soon_alerted_at": null, "last_successful_check_at": null, "consecutive_check_errors": 0, "check_failing_alerted_at": null, "funding": null, "funding_alerted_at": null, "full_name": "northwind.app", "drop_score": 5, "tags": [] }, { "id": 13, "organization_id": 1, "name": "northwind-app", "tld": "com", "priority": 5, "status": "failed", "auto_register": true, "registrar": null, "consecutive_available_count": 0, "notes": null, "last_checked_at": "2026-01-15T09:00:00.000000Z", "available_since": null, "registered_at": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "next_check_at": "2026-01-15T13:00:00.000000Z", "observed_expires_at": null, "observed_status": null, "drop_eta": null, "date_visibility": null, "score": 87, "score_breakdown": null, "scored_at": "2026-01-15T12:00:00.000000Z", "registrar_preferences": null, "lifecycle_stage": "active", "observed_created_at": null, "contact_id": null, "extended_attributes": null, "catchability": 30, "catchability_signals": { "verdict": "active", "factors": [] }, "dns_signals": null, "observed_registrar": null, "observed_updated_at": null, "observed_nameservers": null, "lifecycle_progress": null, "http_signals": null, "enrichment_signals": null, "burst_until": null, "parallel_race": false, "in_auction": false, "auction": null, "backorder": false, "backorder_credential_id": null, "backorder_status": null, "whois_raw_at": null, "drop_eta_uncertainty_days": null, "lifecycle_source": null, "lifecycle_stage_since": "2026-01-09T12:00:00.000000Z", "published_drop_at": null, "deleted_at_registry": null, "dropping_soon_alerted_at": null, "last_successful_check_at": null, "consecutive_check_errors": 0, "check_failing_alerted_at": null, "funding": null, "funding_alerted_at": null, "full_name": "northwind-app.com", "drop_score": 5, "tags": [] } ], "first_page_url": "http://localhost/api/organizations/1/watched-domains?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/watched-domains?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/watched-domains?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/watched-domains", "per_page": 25, "prev_page_url": null, "to": 16, "total": 16, "meta": { "counts": { "total": 16, "active": 13, "available": 2, "registered": 1, "failed": 1, "paused": 1, "in_auction": 1, "not_armed": 8 }, "tags": [] } } ```
--- ### Get a watched domain {#get-organizations-organization-watched-domains-watcheddomain}
`GET` `/organizations/{organization}/watched-domains/{watchedDomain}` **Scope:** `drop-catch.read` · **Changes data:** no One watched domain with its stored WHOIS record, lifecycle dates, estimated drop date, the check schedule still ahead and drop statistics for its extension. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `watchedDomain_id`
path · required | integer | The watchedDomain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "northwind-labs", "tld": "com", "priority": 5, "status": "watching", "auto_register": false, "registrar": null, "consecutive_available_count": 0, "notes": null, "last_checked_at": "2026-01-15T09:00:00.000000Z", "available_since": null, "registered_at": null, "deleted_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "next_check_at": "2026-01-15T13:00:00.000000Z", "observed_expires_at": "2025-11-13T12:00:00.000000Z", "observed_status": null, "drop_eta": "2026-01-27T12:00:00.000000Z", "date_visibility": null, "score": 84, "score_breakdown": null, "scored_at": "2026-01-15T12:00:00.000000Z", "registrar_preferences": null, "lifecycle_stage": "redemption", "observed_created_at": null, "contact_id": null, "extended_attributes": null, "catchability": 78, "catchability_signals": { "verdict": "likely", "factors": [] }, "dns_signals": null, "observed_registrar": null, "observed_updated_at": null, "observed_nameservers": null, "lifecycle_progress": null, "http_signals": null, "enrichment_signals": null, "burst_until": null, "parallel_race": false, "in_auction": false, "auction": null, "backorder": false, "backorder_credential_id": null, "backorder_status": null, "whois_raw": null, "whois_raw_at": null, "drop_eta_uncertainty_days": null, "lifecycle_source": null, "lifecycle_stage_since": "2026-01-12T12:00:00.000000Z", "published_drop_at": null, "deleted_at_registry": null, "dropping_soon_alerted_at": null, "last_successful_check_at": null, "consecutive_check_errors": 0, "check_failing_alerted_at": null, "funding": null, "funding_alerted_at": null, "check_logs_count": 0, "registration_logs_count": 0, "full_name": "northwind-labs.com", "drop_score": 5, "tags": [], "drop_stats": { "heuristic": 78, "probability": null, "basis_n": 0, "scope": null, "phase": "redemption", "tld": "com" }, "lifecycle_anchors": { "registered": null, "expired": "2025-11-13T12:00:00+00:00", "redemption": "2025-12-28T12:00:00+00:00", "pending_delete": "2026-01-27T12:00:00+00:00", "drop": "2026-02-01T12:00:00+00:00" }, "measured_samples": null, "release": null, "cadence": [ { "starts_at": null, "every_seconds": 21600 }, { "starts_at": "2026-01-25T12:00:00+00:00", "every_seconds": 3600 } ] } ```
--- ### List availability checks {#get-organizations-organization-watched-domains-watcheddomain-check-logs}
`GET` `/organizations/{organization}/watched-domains/{watchedDomain}/check-logs` **Scope:** `drop-catch.read` · **Changes data:** no Every availability check run on the watched domain, newest first, 100 per page. Filter with `status`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `watchedDomain_id`
path · required | integer | The watchedDomain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/1/check-logs" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [], "first_page_url": "http://localhost/api/organizations/1/watched-domains/1/check-logs?page=1", "from": null, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/watched-domains/1/check-logs?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/watched-domains/1/check-logs?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/watched-domains/1/check-logs", "per_page": 100, "prev_page_url": null, "to": null, "total": 0 } ```
--- ### List registration attempts {#get-organizations-organization-watched-domains-watcheddomain-registration-logs}
`GET` `/organizations/{organization}/watched-domains/{watchedDomain}/registration-logs` **Scope:** `drop-catch.read` · **Changes data:** no Every attempt to register the watched domain, newest first, 50 per page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `watchedDomain_id`
path · required | integer | The watchedDomain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/1/registration-logs" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [], "first_page_url": "http://localhost/api/organizations/1/watched-domains/1/registration-logs?page=1", "from": null, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/watched-domains/1/registration-logs?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/watched-domains/1/registration-logs?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/watched-domains/1/registration-logs", "per_page": 50, "prev_page_url": null, "to": null, "total": 0 } ```
--- ### List WHOIS changes {#get-organizations-organization-watched-domains-watcheddomain-whois-changes}
`GET` `/organizations/{organization}/watched-domains/{watchedDomain}/whois-changes` **Scope:** `drop-catch.read` · **Changes data:** no Changes detected in the watched domain’s WHOIS record, newest first, 50 per page. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `watchedDomain_id`
path · required | integer | The watchedDomain's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/1/whois-changes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 1, "watched_domain_id": 1, "field": "status", "old_value": "clientTransferProhibited", "new_value": "redemptionPeriod", "changed_at": "2026-01-11T12:00:00.000000Z" }, { "id": 2, "watched_domain_id": 1, "field": "expiry", "old_value": "2026-08-14", "new_value": "2026-08-14", "changed_at": "2025-12-20T12:00:00.000000Z" } ], "first_page_url": "http://localhost/api/organizations/1/watched-domains/1/whois-changes?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/watched-domains/1/whois-changes?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/watched-domains/1/whois-changes?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/watched-domains/1/whois-changes", "per_page": 50, "prev_page_url": null, "to": 3, "total": 3 } ```
--- ### Check whether you already hold a domain {#get-organizations-organization-watched-domains-held}
`GET` `/organizations/{organization}/watched-domains/held` **Scope:** `drop-catch.read` · **Changes data:** no The domain in your organization matching `name` and `tld` (both required) that you hold or are registering, or null when there is none. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `name`
body · required | string | Must not be greater than 63 characters. | | `tld`
body · required | string | Must not be greater than 63 characters. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/held?name=www&tld=%3Ctld%3E" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "domain": { "id": 1, "full_name": "northwind.com", "status": "active", "tenure": "held", "registrar": "Namecheap", "expires_at": "2026-02-02T00:00:00+00:00", "auto_renew": true } } ```
--- ### List registrars for an extension {#get-organizations-organization-watched-domains-registrars}
`GET` `/organizations/{organization}/watched-domains/registrars` **Scope:** `drop-catch.read` · **Changes data:** no The connected registrar accounts that can register a caught domain. With `tld`, each says whether it carries that extension and what it charges, and `requirements` lists what the extension asks of a registrant, or null when nothing is on file. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/registrars" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "registrars": [ { "credential_id": 2, "provider": "cloudflare", "provider_name": "Cloudflare", "provider_label": "Cloudflare DNS", "supports_tld": null, "price": null, "can_race": false }, { "credential_id": 3, "provider": "namecheap", "provider_name": "Namecheap", "provider_label": "Namecheap Registrar", "supports_tld": null, "price": null, "can_race": false } ], "requirements": null } ```
--- ### List registrars available for catching {#get-organizations-organization-watched-domains-registrars-identities}
`GET` `/organizations/{organization}/watched-domains/registrars/identities` **Scope:** `drop-catch.read` · **Changes data:** no The connected registrar accounts that can register a caught domain, by name, and whether each may race for it. Makes no calls to the registrars. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/watched-domains/registrars/identities" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "registrars": [ { "credential_id": 2, "provider": "cloudflare", "provider_name": "Cloudflare", "provider_label": "Cloudflare DNS", "can_race": false }, { "credential_id": 3, "provider": "namecheap", "provider_name": "Namecheap", "provider_label": "Namecheap Registrar", "can_race": false } ] } ```
## Whois servers ### List custom WHOIS servers {#get-organizations-organization-whois-servers}
`GET` `/organizations/{organization}/whois-servers` **Scope:** `drop-catch.read` · **Changes data:** no The WHOIS servers you set per extension to use instead of the default ones, sorted by extension. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/whois-servers" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "tld": "dev", "server": "whois.nic.google", "note": "Override for .dev lookups.", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } ] ```
--- # Organization and members The organizations a token can reach, their members, roles, single sign-on and dashboards. Read only. **Common tasks** - [List members](#get-organizations-organization-members) **Only in the app:** Inviting and removing members, roles and single sign-on. **How it works in the app:** [help article](https://infranest.io/docs/organizations). **Scopes:** Read with `members.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Invitations](#invitations) 1 · [Members](#members) 1 · [Organizations](#organizations) 2 · [Roles](#roles) 2 · [Sandbox](#sandbox) 1
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Invitations ### List pending invitations {#get-organizations-organization-invitations}
`GET` `/organizations/{organization}/invitations` **Scope:** `members.read` · **Changes data:** no Invitations that have not been accepted yet, with who sent each one and the role it grants. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/invitations" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "email": "newcolleague@northwind.example", "invited_by": 1, "accepted_at": null, "expires_at": "2026-01-20T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "org_role_id": 3, "origin": null, "inviter": { "id": 1, "name": "Alex Rivera", "email": "demo@example.com", "analytics_id": null }, "org_role": { "id": 3, "organization_id": null, "name": "Member", "slug": "member", "is_system": true, "permissions": { "dns.read": true, "dns.write": false, "cloud.read": true, "cloud.write": false, "billing.read": false, "domains.read": true, "roles.manage": false, "billing.write": false, "domains.write": false, "domains.delete": false, "members.manage": false, "monitoring.read": true, "monitoring.write": false }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null } } ] ```
## Members ### List members {#get-organizations-organization-members}
`GET` `/organizations/{organization}/members` **Scope:** `members.read` · **Changes data:** no Every member of the organization with their role. `email_undeliverable` is true when email to that address can no longer be delivered. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/members" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 2, "name": "Sam Okoye", "email": "sam@example.com", "email_undeliverable": false, "org_role": { "id": 2, "organization_id": null, "name": "Admin", "slug": "admin", "is_system": true, "permissions": { "dns.read": true, "dns.write": true, "cloud.read": true, "cloud.write": true, "billing.read": true, "domains.read": true, "roles.manage": false, "billing.write": false, "domains.write": true, "domains.delete": false, "members.manage": true, "monitoring.read": true, "monitoring.write": true }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null }, "sso_external": false }, { "id": 3, "name": "Priya Raghunathan", "email": "priya@example.com", "email_undeliverable": false, "org_role": { "id": 3, "organization_id": null, "name": "Member", "slug": "member", "is_system": true, "permissions": { "dns.read": true, "dns.write": false, "cloud.read": true, "cloud.write": false, "billing.read": false, "domains.read": true, "roles.manage": false, "billing.write": false, "domains.write": false, "domains.delete": false, "members.manage": false, "monitoring.read": true, "monitoring.write": false }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null }, "sso_external": false } ] ```
## Organizations ### List your organizations {#get-organizations}
`GET` `/organizations` **Scope:** `members.read` · **Changes data:** no Every organization you are a member of, with your role and its member count. An organization that requires its own single sign-on, which this session has not used, is listed by name only. **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "name": "Northwind Cloud", "slug": "northwind-demo", "deleted_at": null, "created_at": "2025-10-17T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "avatar_url": null, "reseller_id": null, "entitlement_overrides": { "api": true, "cloud": true, "api.mcp": true, "domains": null, "servers": null, "api.read": true, "monitors": null, "webhooks": true, "api.write": true, "dns.drift": true, "dns_zones": null, "drop_catch": true, "module.api": true, "module.dns": true, "automations": null, "data_import": true, "dynamic_ips": null, "white_label": true, "certificates": null, "module.cloud": true, "security.sso": true, "status_pages": null, "team_members": null, "cloud.backups": true, "cloud.volumes": true, "custom_fields": true, "dns.templates": true, "organizations": null, "security.scim": true, "api.tokens_max": null, "cloud.networks": true, "cost_reporting": true, "email_branding": true, "module.domains": true, "cloud.firewalls": true, "cloud.providers": "all", "cloud.snapshots": true, "monitor_regions": true, "watched_domains": null, "module.dropcatch": true, "cloud.volumes_max": null, "external_monitors": null, "module.monitoring": true, "monitored_servers": null, "cloud.networks_max": null, "dropcatch.register": true, "module.automations": true, "monitoring.regions": "all", "cloud.firewalls_max": null, "dropcatch.backorder": true, "module.certificates": true, "module.integrations": true, "module.status_pages": true, "status_page_private": true, "cloud.load_balancers": true, "data_export_advanced": true, "domains.registration": true, "monitoring.incidents": true, "security.ip_allowlist": true, "integrations.providers": "all", "monitoring.check_types": "all", "monitoring.maintenance": true, "status_page_whitelabel": true, "dropcatch.parallel_race": true, "automations.action_types": "all", "cloud.load_balancers_max": null, "monitoring.calendar_feed": true, "status_page_custom_domain": true, "automations.premium_actions": true, "integrations.connections_max": null, "integrations.chat_destinations": null }, "stripe_customer_id": null, "require_reauth_destructive": false, "require_reauth_power": false, "dynamic_ip_default_strict": false, "dynamic_ip_default_fail_closed": false, "require_2fa": false, "password_min_length": null, "password_require_symbols": null, "password_max_age_days": null, "entitlement_overrides_meta": null, "base_currency": "EUR", "price_display": "net", "vat_rate": "21.00", "country": null, "is_demo": true, "timezone": null, "sso_required": false, "suspended_at": null, "suspended_reason": null, "ip_allowlist": null, "session_lifetime_days": null, "pagerduty_webhook_token": null, "vat_number": null, "vat_status": null, "vat_trader_name": null, "vat_country": null, "vat_consultation_number": null, "vat_checked_at": null, "billing_email": null, "billing_cc": null, "billing_reference": null, "purge_after": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "purge_reminder_sent_at": null, "alerts_quiet_until": null, "billing_country": null, "trial_used_at": null, "recap_cadence": "monthly", "brand_logo_url": "http://localhost/storage/org-brand-logos/docs-demo-northwind.svg", "brand_accent_color": "#0F766E", "brand_email_from_name": "Northwind Cloud", "acquisition_src": null, "acquisition_plan": null, "acquisition_cadence": null, "acquisition_code": null, "customer_number": "4200017", "size_band": null, "presignup_first_page": null, "presignup_sessions": null, "presignup_days": null, "sso_required_since": null, "is_internal": false, "client_field_id": null, "onboarding_report_sent_at": null, "notification_defaults": null, "api_writes_enabled_at": null, "is_sandbox": false, "referral_code": "northwind-cloud", "acquisition_ref": null, "users_count": 4, "analytics_id": "api.infranest.io-4200017", "pivot": { "user_id": 1, "organization_id": 1, "org_role_id": 1, "sso_external": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } } ] ```
--- ### Get an organization {#get-organizations-id}
`GET` `/organizations/{id}` **Scope:** `members.read` · **Changes data:** no The organization’s settings, with the name and email address of each member. | Field | Type | Notes | |---|---|---| | `id`
path · required | integer | The id's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "name": "Northwind Cloud", "slug": "northwind-demo", "deleted_at": null, "created_at": "2025-10-17T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "avatar_url": null, "reseller_id": null, "entitlement_overrides": { "api": true, "cloud": true, "api.mcp": true, "domains": null, "servers": null, "api.read": true, "monitors": null, "webhooks": true, "api.write": true, "dns.drift": true, "dns_zones": null, "drop_catch": true, "module.api": true, "module.dns": true, "automations": null, "data_import": true, "dynamic_ips": null, "white_label": true, "certificates": null, "module.cloud": true, "security.sso": true, "status_pages": null, "team_members": null, "cloud.backups": true, "cloud.volumes": true, "custom_fields": true, "dns.templates": true, "organizations": null, "security.scim": true, "api.tokens_max": null, "cloud.networks": true, "cost_reporting": true, "email_branding": true, "module.domains": true, "cloud.firewalls": true, "cloud.providers": "all", "cloud.snapshots": true, "monitor_regions": true, "watched_domains": null, "module.dropcatch": true, "cloud.volumes_max": null, "external_monitors": null, "module.monitoring": true, "monitored_servers": null, "cloud.networks_max": null, "dropcatch.register": true, "module.automations": true, "monitoring.regions": "all", "cloud.firewalls_max": null, "dropcatch.backorder": true, "module.certificates": true, "module.integrations": true, "module.status_pages": true, "status_page_private": true, "cloud.load_balancers": true, "data_export_advanced": true, "domains.registration": true, "monitoring.incidents": true, "security.ip_allowlist": true, "integrations.providers": "all", "monitoring.check_types": "all", "monitoring.maintenance": true, "status_page_whitelabel": true, "dropcatch.parallel_race": true, "automations.action_types": "all", "cloud.load_balancers_max": null, "monitoring.calendar_feed": true, "status_page_custom_domain": true, "automations.premium_actions": true, "integrations.connections_max": null, "integrations.chat_destinations": null }, "stripe_customer_id": null, "require_reauth_destructive": false, "require_reauth_power": false, "dynamic_ip_default_strict": false, "dynamic_ip_default_fail_closed": false, "require_2fa": false, "password_min_length": null, "password_require_symbols": null, "password_max_age_days": null, "entitlement_overrides_meta": null, "base_currency": "EUR", "price_display": "net", "vat_rate": "21.00", "country": null, "is_demo": true, "timezone": null, "sso_required": false, "suspended_at": null, "suspended_reason": null, "ip_allowlist": null, "session_lifetime_days": null, "pagerduty_webhook_token": null, "vat_number": null, "vat_status": null, "vat_trader_name": null, "vat_country": null, "vat_consultation_number": null, "vat_checked_at": null, "billing_email": null, "billing_cc": null, "billing_reference": null, "purge_after": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "purge_reminder_sent_at": null, "alerts_quiet_until": null, "billing_country": null, "trial_used_at": null, "recap_cadence": "monthly", "brand_logo_url": "http://localhost/storage/org-brand-logos/docs-demo-northwind.svg", "brand_accent_color": "#0F766E", "brand_email_from_name": "Northwind Cloud", "acquisition_src": null, "acquisition_plan": null, "acquisition_cadence": null, "acquisition_code": null, "customer_number": "4200017", "size_band": null, "presignup_first_page": null, "presignup_sessions": null, "presignup_days": null, "sso_required_since": null, "is_internal": false, "client_field_id": null, "onboarding_report_sent_at": null, "notification_defaults": null, "api_writes_enabled_at": null, "is_sandbox": false, "referral_code": "northwind-cloud", "acquisition_ref": null, "analytics_id": "api.infranest.io-4200017", "subscription": { "id": 1, "organization_id": 1, "plan_id": 2, "status": "active", "provider": "stripe", "provider_subscription_id": "sub_demo_northwind", "trial_ends_at": null, "current_period_start": "2026-01-01T00:00:00.000000Z", "current_period_end": "2026-02-15T12:00:00.000000Z", "canceled_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "cancel_at_period_end": false, "stripe_price_id": null, "currency": "eur", "trial_reminder_sent_at": null, "renewal_reminder_sent_at": null, "card_expiry_reminder_sent_at": null, "addons": [], "card_on_file": false, "trial_recap_sent_at": null, "trial_last_call_sent_at": null, "trial_first_steps_sent_at": null, "trial_ended_at": null, "trial_reactivation_sent_at": null, "recap_period_start": null, "renewal_recap_sent_for": null, "discount_code": null, "discount_percent": null, "discount_amount_off": null, "discount_duration": null, "discount_starts_at": null, "discount_ends_at": null, "discount_label": null, "plan": { "id": 2, "name": "Pro", "slug": "pro", "description": "For freelancers & solo devs — faster checks, cloud, API and automations.", "limits": { "api": true, "rate": { "manual_sync_per_hour": 30, "domain_checks_per_hour": 100, "monitor_checks_per_hour": 120, "provider_api_per_minute": 300 }, "cloud": true, "api.mcp": true, "domains": 100, "servers": 10, "api.read": true, "monitors": 50, "webhooks": true, "api.write": true, "dns.drift": true, "dns_zones": 100, "drop_catch": true, "automations": 25, "data_import": true, "dynamic_ips": 10, "certificates": 100, "status_pages": 3, "team_members": 3, "cloud.backups": true, "cloud.volumes": true, "dns.templates": true, "organizations": 1, "api.tokens_max": 5, "cloud.firewalls": true, "cloud.snapshots": true, "watched_domains": 50, "cloud.volumes_max": 10, "external_monitors": 5, "dropcatch.register": true, "cloud.firewalls_max": 5, "audit.retention_days": 30, "domains.registration": true, "monitor_min_interval": 60, "monitoring.incidents": true, "monitoring.maintenance": true, "cloud.load_balancers_max": 0, "monitoring.calendar_feed": true, "monitoring.retention_days": 90, "status_page_custom_domain": true, "dns.history_retention_days": 90, "automations.premium_actions": true, "integrations.connections_max": 5 }, "price_monthly": 2250, "price_yearly": 22800, "active": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "is_public": true, "sort_order": 2, "stripe_product_id": null, "stripe_prices": null, "prices": { "eur": { "yearly": 22800, "monthly": 2250 } }, "trial_days": 0, "recommended": false, "name_i18n": null, "description_i18n": null, "badge": null, "tagline_i18n": { "de": "Freelancer & Solo-Entwickler", "en": "Freelancers & solo devs", "fr": "Freelances & développeurs solo", "nl": "Freelancers & solo-ontwikkelaars" }, "highlights_i18n": { "de": [ "Alles in Free", "100 Domains" ], "en": [ "Everything in Free", "100 domains" ], "fr": [ "Tout dans Free", "100 domaines" ], "nl": [ "Alles in Free", "100 domeinen" ] }, "contact_sales": false, "contact_url": null } }, "users": [ { "id": 2, "name": "Sam Okoye", "email": "sam@example.com", "analytics_id": null, "pivot": { "organization_id": 1, "user_id": 2, "org_role_id": 2, "sso_role_managed": false, "sso_external": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } }, { "id": 3, "name": "Priya Raghunathan", "email": "priya@example.com", "analytics_id": null, "pivot": { "organization_id": 1, "user_id": 3, "org_role_id": 3, "sso_role_managed": false, "sso_external": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } } ] } ```
## Roles ### List roles {#get-organizations-organization-roles}
`GET` `/organizations/{organization}/roles` **Scope:** `members.read` · **Changes data:** no The built-in roles followed by the organization’s custom roles, each with its permissions. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/roles" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": [ { "id": 2, "organization_id": null, "name": "Admin", "slug": "admin", "is_system": true, "permissions": { "dns.read": true, "dns.write": true, "cloud.read": true, "cloud.write": true, "billing.read": true, "domains.read": true, "roles.manage": false, "billing.write": false, "domains.write": true, "domains.delete": false, "members.manage": true, "monitoring.read": true, "monitoring.write": true }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null }, { "id": 3, "organization_id": null, "name": "Member", "slug": "member", "is_system": true, "permissions": { "dns.read": true, "dns.write": false, "cloud.read": true, "cloud.write": false, "billing.read": false, "domains.read": true, "roles.manage": false, "billing.write": false, "domains.write": false, "domains.delete": false, "members.manage": false, "monitoring.read": true, "monitoring.write": false }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null } ] } ```
--- ### Get a role {#get-organizations-organization-roles-id}
`GET` `/organizations/{organization}/roles/{id}` **Scope:** `members.read` · **Changes data:** no One role and its permissions. Either a built-in role or one of this organization’s custom roles. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `id`
path · required | integer | The id's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/roles/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "data": { "id": 1, "organization_id": null, "name": "Owner", "slug": "owner", "is_system": true, "permissions": { "dns.read": true, "dns.write": true, "cloud.read": true, "cloud.write": true, "billing.read": true, "domains.read": true, "roles.manage": true, "billing.write": true, "domains.write": true, "domains.delete": true, "members.manage": true, "monitoring.read": true, "monitoring.write": true }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null } } ```
## Sandbox ### Reset a sandbox {#post-organizations-organization-sandbox-reset}
`POST` `/organizations/{organization}/sandbox/reset` **Scope:** any token · **Changes data:** yes — only where API writes are switched on for the organization Removes everything in your sandbox organization and restores its sample zones, records, monitors, server and tags. Works only on a sandbox, with its own test token or from the app; any other organization answers 404. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/sandbox/reset" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ```
--- # Tags and custom fields Tags and custom fields, which work across every module. **Common tasks** - [List tags](#get-organizations-organization-tags) - [Create a tag](#post-organizations-organization-tags) **Only in the app:** Nothing. Tags and custom fields can be managed with a token. **How it works in the app:** [help article](https://infranest.io/docs/tags). **Scopes:** Read with `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Custom field values](#custom-field-values) 2 · [Custom fields](#custom-fields) 4 · [Tags](#tags) 4
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Custom field values ### List custom field values for a record {#get-organizations-organization-custom-field-values}
`GET` `/organizations/{organization}/custom-field-values` **Scope:** `monitoring.read` · **Changes data:** no The custom field values set on one record. `entity_type` and `entity_id` are required; returns 404 when the record is not in this organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body · required | string | | | `entity_id`
body · required | integer | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/custom-field-values?entity_type=%3Centity_type%3E&entity_id=16" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "custom_field_definition_id": 1, "value": "Platform" }, { "id": 2, "custom_field_definition_id": 2, "value": "Web platform" } ] ```
--- ### Set a custom field value {#put-organizations-organization-custom-field-values}
`PUT` `/organizations/{organization}/custom-field-values` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Sets the `value` of one field (`definition_id`) on one record (`entity_type`, `entity_id`), checked against the field’s type and options. A null value clears it and returns 204. Requires a plan that includes custom fields. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body · required | string | | | `entity_id`
body · required | integer | | | `definition_id`
body · required | integer | | | `value`
body | string | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PUT "https://api.infranest.io/api/organizations/42/custom-field-values" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"entity_type":"server","entity_id":1,"definition_id":1,"value":"Platform"}' ``` **Response** · 200 ```json { "id": 1, "custom_field_definition_id": 1, "value": "Platform" } ```
## Custom fields ### List custom fields {#get-organizations-organization-custom-fields}
`GET` `/organizations/{organization}/custom-fields` **Scope:** `monitoring.read` · **Changes data:** no The organization’s custom field definitions in display order. Pass `entity_type` to list only the fields that apply to that kind of record. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body | string | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/custom-fields" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 1, "organization_id": 1, "entity_types": [ "server", "domain" ], "key": "cost_centre", "label": "Cost centre", "type": "select", "options": [ "Platform", "Marketing" ], "required": false, "sort": 0, "show_on_detail": true, "show_as_column": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null }, { "id": 2, "organization_id": 1, "entity_types": [ "server" ], "key": "owner_team", "label": "Owning team", "type": "text", "options": null, "required": false, "sort": 1, "show_on_detail": true, "show_as_column": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null } ] ```
--- ### Create a custom field {#post-organizations-organization-custom-fields}
`POST` `/organizations/{organization}/custom-fields` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Defines a custom field with `entity_types`, a unique `key`, a `label` and a `type`; select and multi-select fields also need `options`. Requires a plan that includes custom fields. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/custom-fields" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"entity_types":["server"],"key":"cost_center","label":"Cost center","type":"text"}' ``` **Response** · 201 — Created ```json { "entity_types": [ "server" ], "key": "cost_center", "label": "Cost center", "type": "text", "options": null, "required": false, "sort": 0, "show_on_detail": true, "show_as_column": false, "organization_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 4 } ```
--- ### Update a custom field {#patch-organizations-organization-custom-fields-customfielddefinition}
`PATCH` `/organizations/{organization}/custom-fields/{customFieldDefinition}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Replaces the field’s definition; send the full set of fields, as on create. Requires a plan that includes custom fields. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `customFieldDefinition_id`
path · required | integer | The customFieldDefinition's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/custom-fields/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"entity_types":["server","domain"],"key":"cost_centre","label":"Responsible team","type":"select","options":["Platform","Marketing","Data","Internal tools"]}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "entity_types": [ "server", "domain" ], "key": "cost_centre", "label": "Responsible team", "type": "select", "options": [ "Platform", "Marketing" ], "required": false, "sort": 0, "show_on_detail": true, "show_as_column": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null } ```
--- ### Delete a custom field {#delete-organizations-organization-custom-fields-customfielddefinition}
`DELETE` `/organizations/{organization}/custom-fields/{customFieldDefinition}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Deletes the field definition together with every value stored for it. Requires a plan that includes custom fields. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `customFieldDefinition_id`
path · required | integer | The customFieldDefinition's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/custom-fields/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
## Tags ### List tags {#get-organizations-organization-tags}
`GET` `/organizations/{organization}/tags` **Scope:** `monitoring.read` · **Changes data:** no Every tag in the organization, with its colour, whether it names a client, its uptime target and how many resources carry it (`usage_count`). | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/tags" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 3, "organization_id": 1, "name": "Customer-facing", "color": "#3b82f6", "uptime_target_pct": null, "is_client": false, "created_at": "2026-01-15T12:00:00.000000Z", "usage_count": 12 }, { "id": 4, "organization_id": 1, "name": "Internal", "color": "#8b5cf6", "uptime_target_pct": null, "is_client": false, "created_at": "2026-01-15T12:00:00.000000Z", "usage_count": 11 } ] ```
--- ### Create a tag {#post-organizations-organization-tags}
`POST` `/organizations/{organization}/tags` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Creates a tag from a `name` (up to 50 characters) and a hex `color` such as `#ff8800`; `is_client` is optional. Refused with 422 when a tag with that name already exists. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `name`
body · required | string | Must not be greater than 50 characters. | | `color`
body · required | string | Must match the regex /^#[0-9A-Fa-f]{6}$/. | | `is_client`
body | boolean | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/tags" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"Payments","color":"#3B82F6"}' ``` **Response** · 201 — Created ```json { "name": "Payments", "color": "#3B82F6", "organization_id": 1, "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 7 } ```
--- ### Update a tag {#patch-organizations-organization-tags-tag}
`PATCH` `/organizations/{organization}/tags/{tag}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes any of `name`, `color`, `is_client` and `uptime_target_pct` (0 to 100, or null to clear it). Refused with 422 when the new name is already taken. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `tag_id`
path · required | integer | The tag's ID. | | `name`
body | string | Must not be greater than 50 characters. | | `color`
body | string | Must match the regex /^#[0-9A-Fa-f]{6}$/. | | `uptime_target_pct`
body | number, nullable | The availability agreed with this client. Null clears it, and a cleared target is reported as "none agreed" rather than as 100%. Must be at least 0. Must not be greater than 100. | | `is_client`
body | boolean | This tag names a client: attention items on what it tags are grouped under it. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/tags/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"color":"#22C55E","uptime_target_pct":99.9}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "name": "Production", "color": "#22C55E", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "uptime_target_pct": "99.900", "is_client": false } ```
--- ### Delete a tag {#delete-organizations-organization-tags-tag}
`DELETE` `/organizations/{organization}/tags/{tag}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Deletes the tag and removes it from every resource that carried it. The resources themselves are kept. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `tag_id`
path · required | integer | The tag's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/tags/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
--- # Costs Cost items, cost changes and provider price changes. **Common tasks** - [Record a cost](#post-organizations-organization-cost-items) **Only in the app:** Nothing. Cost items can be managed with a token. **How it works in the app:** [help article](https://infranest.io/docs/itemised-costs). **Scopes:** Read with `monitoring.read`, change with `monitoring.write`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Cost events](#cost-events) 1 · [Cost items](#cost-items) 4 · [Price changes](#price-changes) 1
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Cost events ### List cost events {#get-organizations-organization-cost-events}
`GET` `/organizations/{organization}/cost-events` **Scope:** `monitoring.read` · **Changes data:** no Money spent on a date, such as registrations, renewals, transfers and purchases, newest first, up to 500 entries. Narrow with `entity_type`, `entity_id`, `from` and `to`. Requires a plan that includes cost reporting. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body | string | | | `entity_id`
body | integer | | | `from`
body | string | Must be a valid date. | | `to`
body | string | Must be a valid date. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cost-events" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
## Cost items ### List cost items {#get-organizations-organization-cost-items}
`GET` `/organizations/{organization}/cost-items` **Scope:** `monitoring.read` · **Changes data:** no The recurring costs you recorded on one resource. `entity_type` is required, and `entity_id` too unless `entity_type` is `organization`, which lists costs that belong to the whole organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body · required | string | | | `entity_id`
body | integer | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/cost-items?entity_type=%3Centity_type%3E" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
--- ### Create a cost item {#post-organizations-organization-cost-items}
`POST` `/organizations/{organization}/cost-items` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Records a recurring cost with a `label`, `amount`, `currency` and optional `period` (default monthly) on a resource (`entity_type`, `entity_id`), or on the whole organization with `entity_type` `organization`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body · required | string | | | `entity_id`
body | integer | Absent for an org-level cost, which belongs to no object. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/organizations/42/cost-items" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"entity_type":"organization","label":"Uptime SMS credits","amount":9,"currency":"EUR"}' ``` **Response** · 201 — Created ```json { "organization_id": 1, "label": "Uptime SMS credits", "amount": "9.00", "currency": "EUR", "period": "monthly", "updated_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "id": 2 } ```
--- ### Update a cost item {#patch-organizations-organization-cost-items-costitem}
`PATCH` `/organizations/{organization}/cost-items/{costItem}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Changes the cost’s `label`, `amount`, `currency` and `period`. `label`, `amount` and `currency` are required on every update; an omitted `period` resets to monthly. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `costItem_id`
path · required | integer | The costItem's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`validation_failed`](../errors.md#validation_failed), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X PATCH "https://api.infranest.io/api/organizations/42/cost-items/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"label":"Status page SMS credits","amount":15,"currency":"EUR"}' ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "cost_itemable_type": null, "cost_itemable_id": null, "label": "Status page SMS credits", "amount": "15.00", "currency": "EUR", "period": "monthly", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } ```
--- ### Delete a cost item {#delete-organizations-organization-cost-items-costitem}
`DELETE` `/organizations/{organization}/cost-items/{costItem}` **Scope:** `monitoring.write` · **Changes data:** yes — only where API writes are switched on for the organization Deletes the recorded cost. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `costItem_id`
path · required | integer | The costItem's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X DELETE "https://api.infranest.io/api/organizations/42/cost-items/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 204 — no content
## Price changes ### List price changes for a resource {#get-organizations-organization-price-changes}
`GET` `/organizations/{organization}/price-changes` **Scope:** `monitoring.read` · **Changes data:** no How one resource’s price has changed over time, newest first, up to 100 entries. `entity_type` and `entity_id` are required. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `entity_type`
body · required | string | | | `entity_id`
body · required | integer | | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/price-changes?entity_type=%3Centity_type%3E&entity_id=16" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 2, "organization_id": 1, "priceable_type": "App\\Models\\Domain", "priceable_id": 1, "label": "northwind.com", "field": "renewal_price", "old_amount": "13.5000", "old_currency": "EUR", "old_interval": "yearly", "old_interval_count": 1, "new_amount": "12.5000", "new_currency": "EUR", "new_interval": "yearly", "new_interval_count": 1, "source": "api", "changed_by_user_id": null, "observed_at": "2025-12-02T12:00:00.000000Z", "effective_from": "2025-12-02T00:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "changed_by": null } ] ```
--- # Billing Subscription, invoices and usage. Read only. **Common tasks** - [The current subscription](#get-organizations-organization-subscription) **Only in the app:** Changing the plan, payment methods and invoices details. **How it works in the app:** [help article](https://infranest.io/docs/subscription). **Scopes:** Read with `billing.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Account credit](#account-credit) 1 · [Credit notes](#credit-notes) 1 · [Invoices](#invoices) 1 · [Referrals](#referrals) 1 · [Subscription](#subscription) 2
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Account credit ### Get account credit {#get-organizations-organization-account-credit}
`GET` `/organizations/{organization}/account-credit` **Scope:** `billing.read` · **Changes data:** no Money InfraNest holds for you — for example after a downgrade or a refund — which comes off your next invoice. One balance per currency, in the currency’s smallest unit; empty when you have none. Read-only. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/account-credit" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "credit": [] } ```
## Credit notes ### List credit notes {#get-organizations-organization-credit-notes}
`GET` `/organizations/{organization}/credit-notes` **Scope:** `billing.read` · **Changes data:** no Credit notes issued against your invoices, with the invoice each one credits, its amounts (negative, in the currency’s smallest unit), how it was settled and a PDF link. Read-only. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/credit-notes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
## Invoices ### List invoices {#get-organizations-organization-invoices}
`GET` `/organizations/{organization}/invoices` **Scope:** `billing.read` · **Changes data:** no Your most recent 100 invoices, newest first, with their status, totals, billing period and links to view or download each one. Read-only. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/invoices" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [ { "id": 6, "number": "INF-2026-0100", "status": "paid", "currency": "eur", "total": 4719, "tax": 819, "paid_at": "2026-01-02T00:00:00.000000Z", "hosted_invoice_url": null, "invoice_pdf": null, "period_start": "2026-01-01T00:00:00.000000Z", "period_end": "2026-01-31T23:59:59.000000Z", "created_at": "2026-01-01T00:00:00.000000Z" }, { "id": 5, "number": "INF-2025-0101", "status": "paid", "currency": "eur", "total": 4719, "tax": 819, "paid_at": "2025-12-02T00:00:00.000000Z", "hosted_invoice_url": null, "invoice_pdf": null, "period_start": "2025-12-01T00:00:00.000000Z", "period_end": "2025-12-31T23:59:59.000000Z", "created_at": "2025-12-01T00:00:00.000000Z" } ] ```
## Referrals ### Get your referrals {#get-organizations-organization-referrals}
`GET` `/organizations/{organization}/referrals` **Scope:** `billing.read` · **Changes data:** no Your referral link, what it has earned and how much discount is left, and the workspaces that signed up with it — names partly hidden, with where each referral has got to. `enabled` is false when the referral programme is switched off. Read-only. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/referrals" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "enabled": true, "code": "northwind-cloud", "link": "http://localhost/r/northwind-cloud", "can_change": true, "currency": "eur", "stats": { "signed_up": 5, "paying": 3, "rewards_this_year": 2, "max_rewards_per_year": 12, "earned": 12800, "on_its_way": 1, "remaining": 7000 }, "referrals": [ { "id": 4, "name": "Ve•••• Labs", "joined_at": "2026-01-11T12:00:00+00:00", "status": "pending", "void_reason": null, "credited_from": null, "reward_amount": null, "reward_currency": null, "reward_remaining": null }, { "id": 3, "name": "Ha•••• IT", "joined_at": "2025-12-26T12:00:00+00:00", "status": "qualifying", "void_reason": null, "credited_from": "2026-01-28T12:00:00+00:00", "reward_amount": null, "reward_currency": null, "reward_remaining": null } ] } ```
## Subscription ### Get the subscription {#get-organizations-organization-subscription}
`GET` `/organizations/{organization}/subscription` **Scope:** `billing.read` · **Changes data:** no The organization’s subscription and plan, the plan whose limits currently apply, whether a free trial is still available and any discount in force. Read-only; returns 404 when there is no subscription. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/subscription" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "id": 1, "organization_id": 1, "plan_id": 2, "status": "active", "provider": "stripe", "provider_subscription_id": "sub_demo_northwind", "trial_ends_at": null, "current_period_start": "2026-01-01T00:00:00.000000Z", "current_period_end": "2026-02-15T12:00:00.000000Z", "canceled_at": null, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "cancel_at_period_end": false, "stripe_price_id": null, "currency": "eur", "trial_reminder_sent_at": null, "renewal_reminder_sent_at": null, "card_expiry_reminder_sent_at": null, "addons": [], "card_on_file": false, "trial_recap_sent_at": null, "trial_last_call_sent_at": null, "trial_first_steps_sent_at": null, "trial_ended_at": null, "trial_reactivation_sent_at": null, "recap_period_start": null, "renewal_recap_sent_for": null, "discount_code": null, "discount_percent": null, "discount_amount_off": null, "discount_duration": null, "discount_starts_at": null, "discount_ends_at": null, "discount_label": null, "plan": { "id": 2, "name": "Pro", "slug": "pro", "description": "For freelancers & solo devs — faster checks, cloud, API and automations.", "limits": { "api": true, "rate": { "manual_sync_per_hour": 30, "domain_checks_per_hour": 100, "monitor_checks_per_hour": 120, "provider_api_per_minute": 300 }, "cloud": true, "api.mcp": true, "domains": 100, "servers": 10, "api.read": true, "monitors": 50, "webhooks": true, "api.write": true, "dns.drift": true, "dns_zones": 100, "drop_catch": true, "automations": 25, "data_import": true, "dynamic_ips": 10, "certificates": 100, "status_pages": 3, "team_members": 3, "cloud.backups": true, "cloud.volumes": true, "dns.templates": true, "organizations": 1, "api.tokens_max": 5, "cloud.firewalls": true, "cloud.snapshots": true, "watched_domains": 50, "cloud.volumes_max": 10, "external_monitors": 5, "dropcatch.register": true, "cloud.firewalls_max": 5, "audit.retention_days": 30, "domains.registration": true, "monitor_min_interval": 60, "monitoring.incidents": true, "monitoring.maintenance": true, "cloud.load_balancers_max": 0, "monitoring.calendar_feed": true, "monitoring.retention_days": 90, "status_page_custom_domain": true, "dns.history_retention_days": 90, "automations.premium_actions": true, "integrations.connections_max": 5 }, "price_monthly": 2250, "price_yearly": 22800, "active": true, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "is_public": true, "sort_order": 2, "stripe_product_id": null, "stripe_prices": null, "prices": { "eur": { "yearly": 22800, "monthly": 2250 } }, "trial_days": 0, "recommended": false, "name_i18n": null, "description_i18n": null, "badge": null, "tagline_i18n": { "de": "Freelancer & Solo-Entwickler", "en": "Freelancers & solo devs", "fr": "Freelances & développeurs solo", "nl": "Freelancers & solo-ontwikkelaars" }, "highlights_i18n": { "de": [ "Alles in Free", "100 Domains" ], "en": [ "Everything in Free", "100 domains" ], "fr": [ "Tout dans Free", "100 domaines" ], "nl": [ "Alles in Free", "100 domeinen" ] }, "contact_sales": false, "contact_url": null }, "effective_plan_id": 2, "effective_plan_slug": "pro", "trial_available": true, "referral_welcome": null, "suggested_promotion_code": null, "discount": null } ```
--- ### List add-ons {#get-organizations-organization-subscription-addons}
`GET` `/organizations/{organization}/subscription/addons` **Scope:** `billing.read` · **Changes data:** no The add-ons on offer with their prices, each marked `owned` when the organization already has it. Read-only. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/subscription/addons" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json [] ```
--- # Audit log Every change in the organization, including those made with tokens. Read only. **Common tasks** - [Read the audit log](#get-organizations-organization-audit-logs) **Only in the app:** Nothing. The audit log is read only, in the app too. **How it works in the app:** [help article](https://infranest.io/docs/audit-log). **Scopes:** Read with `audit-log.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Audit logs](#audit-logs) 2
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Audit logs ### List audit log entries {#get-organizations-organization-audit-logs}
`GET` `/organizations/{organization}/audit-logs` **Scope:** `audit-log.read` · **Changes data:** no Who changed what in the organization, newest first. Filter with the dates `from` and `to`, `action` (matches part of the name), `resource_type` and `token_id` for changes made through one API token; page with `per_page` (default 25, at most 100) and `page`. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/audit-logs" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "current_page": 1, "data": [ { "id": 25, "organization_id": 1, "user_id": 1, "action": "status_page_subscribers.viewed", "resource_type": "StatusPage", "resource_id": 1, "meta": { "count": 3, "status_page": "Northwind Cloud Status" }, "ip_address": "127.0.0.1", "user_agent": "Symfony", "created_at": "2026-01-15T12:00:00.000000Z", "impersonator_id": null, "via_token_id": 5, "via_token_name": "api-samples", "user": { "id": 1, "name": "Alex Rivera", "email": "demo@example.com", "analytics_id": null }, "impersonator": null }, { "id": 24, "organization_id": 1, "user_id": 1, "action": "data.exported", "resource_type": null, "resource_id": null, "meta": { "month": "2025-12", "format": "csv", "filters": [], "resource": "uptime-report" }, "ip_address": "127.0.0.1", "user_agent": "Symfony", "created_at": "2026-01-15T12:00:00.000000Z", "impersonator_id": null, "via_token_id": 5, "via_token_name": "api-samples", "user": { "id": 1, "name": "Alex Rivera", "email": "demo@example.com", "analytics_id": null }, "impersonator": null } ], "first_page_url": "http://localhost/api/organizations/1/audit-logs?page=1", "from": 1, "last_page": 1, "last_page_url": "http://localhost/api/organizations/1/audit-logs?page=1", "links": [ { "url": null, "label": "« Previous", "page": null, "active": false }, { "url": "http://localhost/api/organizations/1/audit-logs?page=1", "label": "1", "page": 1, "active": true } ], "next_page_url": null, "path": "http://localhost/api/organizations/1/audit-logs", "per_page": 25, "prev_page_url": null, "to": 24, "total": 24 } ```
--- ### Export the audit log as CSV {#get-organizations-organization-audit-logs-export}
`GET` `/organizations/{organization}/audit-logs/export` **Scope:** `audit-log.read` · **Changes data:** no Downloads every matching entry as a CSV file, with the same filters as the list and no paging. The export is itself recorded in the audit log. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/audit-logs/export" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 — The matching audit log entries as CSV. (`text/csv`)
--- # Data export Export your data as CSV or JSON. **Common tasks** - [Export a resource as CSV or JSON](#get-organizations-organization-export-resource) **Only in the app:** Importing data. **How it works in the app:** [help article](https://infranest.io/docs/data-export-import). **Scopes:** Read with `data.read`. See [Tokens and scopes](../tokens-and-scopes.md#scopes).
[Export](#export) 3 · [Import](#import) 1
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Export ### List exportable data {#get-organizations-organization-export}
`GET` `/organizations/{organization}/export` **Scope:** `data.read` · **Changes data:** no The kinds of data you can export and the formats available, each marked `locked` when your plan does not include it. CSV is always available; JSON and XML need a plan with advanced export. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/export" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "resources": [ { "key": "domains", "label": "Domains", "locked": false }, { "key": "contacts", "label": "Contacts", "locked": false } ], "formats": [ { "key": "csv", "locked": false }, { "key": "json", "locked": false } ] } ```
--- ### Export one kind of data {#get-organizations-organization-export-resource}
`GET` `/organizations/{organization}/export/{resource}` **Scope:** `data.read` · **Changes data:** no Downloads every record of one kind as a file, in `format` `csv` (default), `json` or `xml`; pass `ids[]` to export only those records. Refused when your role may not read that data or your plan does not include the resource or format. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | | `resource`
path · required | string | The resource. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/export/1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 — The resource as CSV (the default), JSON or XML, as `format` asks. (`text/csv`, `application/json`, `application/xml`)
--- ### Download a full data export {#get-organizations-organization-export-bundle}
`GET` `/organizations/{organization}/export/bundle` **Scope:** `data.read` · **Changes data:** no A ZIP file with a CSV and a JSON file for every kind of data your role may read, plus a `manifest.json` that lists what is included. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/export/bundle" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 — Every exportable resource, one file each, in a ZIP archive. (`application/zip`)
## Import ### List importable data {#get-organizations-organization-import}
`GET` `/organizations/{organization}/import` **Scope:** `data.read` · **Changes data:** no The kinds of data you can import from CSV, with the column headers each expects, and whether your plan allows importing (`allowed`). Requires permission to manage the organization. | Field | Type | Notes | |---|---|---| | `organization_id`
path · required | integer | Your organization's ID. | **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`scope_missing`](../errors.md#scope_missing), [`wrong_organization`](../errors.md#wrong_organization), [`test_token_required`](../errors.md#test_token_required), [`test_token_sandbox_only`](../errors.md#test_token_sandbox_only), [`permission_denied`](../errors.md#permission_denied), [`not_found`](../errors.md#not_found), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/organizations/42/import" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "resources": [ { "key": "contacts", "label": "Contacts", "headers": [ "first_name", "last_name" ] }, { "key": "ssl-certificates", "label": "SSL certificates", "headers": [ "subject", "sans" ] } ], "allowed": true } ```
--- # Account Who the token belongs to, and revoking it. **Common tasks** - [Who this token belongs to](#get-auth-me) **Only in the app:** Creating, renewing and revoking tokens, and everything about your account. **How it works in the app:** [help article](https://infranest.io/docs/api-tokens). Any token can call these; no scope is needed.
[Auth](#auth) 2
Every endpoint below takes a token in the `Authorization: Bearer` header and answers JSON. Errors use the shared format in [Errors](../errors.md). Base address: `https://api.infranest.io/api`. ## Auth ### Revoke the current token {#post-auth-logout}
`POST` `/auth/logout` **Scope:** any token · **Changes data:** yes — only where API writes are switched on for the organization Deletes the token that authenticated this request, so it stops working immediately. Other tokens on your account are not affected. **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`write_disabled`](../errors.md#write_disabled), [`organization_suspended`](../errors.md#organization_suspended), [`idempotency_in_progress`](../errors.md#idempotency_in_progress), [`validation_failed`](../errors.md#validation_failed), [`idempotency_key_reused`](../errors.md#idempotency_key_reused), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl -X POST "https://api.infranest.io/api/auth/logout" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "message": "Logged out successfully." } ```
--- ### Get the signed-in user {#get-auth-me}
`GET` `/auth/me` **Scope:** any token · **Changes data:** no Your user profile, the organizations you belong to with your role in each, and the state of your account security (two-factor, passkeys, password). A token bound to one organization lists only that organization. **Errors:** [`unauthenticated`](../errors.md#unauthenticated), [`rate_limited`](../errors.md#rate_limited)
**Request** ```sh curl "https://api.infranest.io/api/auth/me" \ -H "Authorization: Bearer $INFRANEST_TOKEN" \ -H "Accept: application/json" ``` **Response** · 200 ```json { "user": { "id": 1, "name": "Alex Rivera", "email": "demo@example.com", "email_verified_at": "2026-01-15T12:00:00.000000Z", "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "is_super_admin": false, "blocked_at": null, "block_reason": null, "locked_until": null, "notification_preferences": { "quiet_hours": { "from": "22:00", "to": "07:00", "tz": "Europe/Amsterdam" } }, "two_factor_confirmed_at": null, "first_name": null, "last_name": null, "language": "en", "avatar_url": "http://localhost/storage/avatars/docs-demo-alex.svg", "dashboard_layout": null, "platform_role": "user", "email_format": "html", "onboarding": null, "password_changed_at": "2026-01-15T12:00:00.000000Z", "pending_email": null, "pending_email_sent_at": null, "terms_accepted_at": null, "terms_version": null, "marketing_consent_at": null, "timezone": null, "time_format": "auto", "force_password_reset": false, "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null, "anonymized_at": null, "analytics_opt_out": false, "open_on_dashboard_id": null, "default_organization_id": null, "analytics_id": "api.infranest.io-4200017", "organizations": [ { "id": 1, "name": "Northwind Cloud", "slug": "northwind-demo", "country": null, "avatar_url": null, "base_currency": "EUR", "timezone": null, "price_display": "net", "vat_rate": "21.00", "require_reauth_destructive": false, "require_reauth_power": false, "require_2fa": false, "password_min_length": null, "password_require_symbols": null, "password_max_age_days": null, "dynamic_ip_default_strict": false, "dynamic_ip_default_fail_closed": false, "recap_cadence": "monthly", "sso_required": false, "sso_required_since": null, "is_demo": true, "is_sandbox": false, "users_count": 4, "org_role": { "id": 1, "organization_id": null, "name": "Owner", "slug": "owner", "is_system": true, "permissions": { "dns.read": true, "dns.write": true, "cloud.read": true, "cloud.write": true, "billing.read": true, "domains.read": true, "roles.manage": true, "billing.write": true, "domains.write": true, "domains.delete": true, "members.manage": true, "monitoring.read": true, "monitoring.write": true }, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z", "deleted_at": null, "purge_after": null, "purge_reminder_sent_at": null, "deleted_by_id": null, "deleted_by_label": null, "deletion_reason": null }, "sso_admitted": true, "effective_timezone": "UTC", "managed_by": [ "Sam Okoye" ], "analytics_id": null, "pivot": { "user_id": 1, "organization_id": 1, "org_role_id": 1, "sso_external": false, "created_at": "2026-01-15T12:00:00.000000Z", "updated_at": "2026-01-15T12:00:00.000000Z" } } ] }, "unread_notifications": 3, "is_impersonated": false, "is_standing_in": false, "impersonated_by": null, "session_country_code": null, "session_region_code": null, "two_factor_setup_required": false, "password_reset_required": false, "email_verification_required": false, "email_undeliverable": false, "email_security_phrase": null, "has_passkeys": false, "passkey_count": 0, "has_password": true, "sso_only": null, "can_go_passwordless": false, "recovery_codes_left": null, "gravatar_enabled": false, "gravatar_default": "mp", "avatar_sources": [ "google", "github" ], "oauth_avatars": [], "stripe_enabled": false, "stripe_publishable_key": null } ```