Skip to main content
On this page

TowerOps API Reference

Use the TowerOps API to manage your network monitoring infrastructure programmatically. Create and monitor sites, devices, and alerts.

This reference is generated from the API's OpenAPI 3 description, which you can consume directly at /api/v1/openapi.json to generate a client.

Official clients

Drive TowerOps as code. The official Terraform provider manages sites, devices, checks, agents, schedules, escalation policies, maintenance windows and integrations from the same REST API documented here: registry.terraform.io/providers/towerops-app/towerops.

Authentication

Every endpoint in the reference below authenticates with an API token, the inbound webhooks and the account export use the schemes noted in their own sections. Include your token in the Authorization header: Quo's inbound listener at /api/v1/webhooks/quo/:organization_id verifies the webhook-id, webhook-timestamp and webhook-signature headers with its provisioned per-organization signing key. Configure Quo under Organization Settings → Integrations; never send a TowerOps API token to this listener. Creating or updating Quo through REST or GraphQL also provisions before enabling. Provisioning failure leaves a disabled integration; retrieve and update it to retry. API-key replacement first removes the old workspace's subscription; cleanup failure retains its credentials and disables capture.

Slack posts form-encoded interactions and /towerops status to /api/v1/webhooks/slack. It authenticates with X-Slack-Signature and X-Slack-Request-Timestamp over the exact raw body, not a TowerOps API token. Timestamps must be within five minutes. Accepted requests are queued immediately; results are sent privately after current membership and action permissions are checked. Install the Slack app from Organization Settings → Integrations.

The masked configuration export API is in Beta. With backups:read, fetch GET /api/v1/devices/:device_id/config_backups/:version_id for masked text, or GET /api/v1/devices/:device_id/config_backups/export?page=1 for a ZIP of up to 100 retained versions. Read the ZIP's X-Page and X-Total-Pages headers and fetch every page to mirror the history. Entries use version IDs in their filenames. These files are not restorable: secret values are placeholders, even if raw-download parameters are supplied. Every successful fetch records the API token or partner app and version IDs in the audit trail. Downloads fail closed if that audit cannot be saved. Disabled rollout, missing versions and pruned text return 404. Access is enabled by the global config_backup_api switch or by an organization pilot while its internal switch is enabled; a superuser alone cannot bypass it.

curl https://staging.towerops.net/api/v1/sites \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Each token is scoped to a single organization and carries an explicit list of scopes. A request is allowed only when the token holds the scope the endpoint requires; otherwise it is rejected with 403 and an error naming the missing scope. Tokens created before scopes existed keep full access until replaced.

Create a token in Account Settings → API Tokens

Scope Grants
sites:read View sites
sites:write Create, update and delete sites
devices:read View devices, metrics and interfaces
devices:write Create, update and delete devices
checks:read View service checks and their results
checks:write Create, update and delete service checks
alerts:read View alerts
alerts:write Acknowledge, resolve, snooze and escalate alerts
agents:read View monitoring agents
agents:write Create and delete monitoring agents
organization:read View organization details
organization:write Update organization settings
members:read View members and pending invitations
members:write Invite members, change roles and remove members
integrations:read View integrations
integrations:write Create, update, test and delete integrations
coverages:read View RF coverage predictions
coverages:write Create, recompute and delete coverage predictions
config:read View schedules, escalation policies and maintenance windows
config:write Manage schedules, escalation policies and maintenance windows
activity:read View the organization activity feed
discovery:read View discovered device candidates
discovery:write Add or ignore discovered device candidates
webhooks:manage Manage outbound webhook endpoints and deliveries
subscribers:read View customers, subscriber traces, and subscriber impact for alerts and open outages
rf:read View RF link health, wireless clients and RF planning resources
rf:write Create and update RF planning towers, radios and links
backups:read View configuration backups and their status
backups:write Request configuration backups
reports:read View reports and download their CSV
reports:write Run reports now
status_pages:read View the status page, its components and incidents
tokens:manage List, create and revoke the organization's API tokens
admin:manage superuser only Superuser administration (MIB files, GeoIP data)

Partner integrations can also obtain scoped access with OAuth 2.0 client credentials. An org admin installs the app with chosen scopes, then the partner exchanges its credentials for a short-lived bearer token:

curl -X POST https://staging.towerops.net/api/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=tpa_... \
  -d client_secret=tps_...

Errors

The API uses conventional HTTP response codes to indicate success or failure:

Code Description
200 Success
201 Created successfully
202 Accepted - queued for background processing
400 Bad request - missing or invalid parameters
401 Unauthorized - invalid or missing API token
402 Payment required - plan limit reached (`plan_limit`)
403 Forbidden - you don't have access to this resource, or the token lacks the required scope
404 Not found - resource doesn't exist
409 Conflict - the resource is in a state that forbids the operation
422 Unprocessable entity - validation errors
429 Too many requests - rate limited
500 Internal error

Every JSON success uses a data envelope, with meta added for pagination or counts. Every JSON error carries a stable code and a human-readable message; validation errors add per-field details.

Device and alert lists always include meta with page, per_page, total_count, total_pages, next_cursor and window. Follow the opaque cursor for the next page; it is null on the last page. Check lists expose only service checks, even with an explicit check_type filter; discovery-managed SNMP checks are never returned.

meta.window reports the time window the rows and total_count were read from: updated_since filters the device list by updated_at, and triggered_since/triggered_until filter the alert list by triggered_at. A lower bound sent without an upper bound closes the window at the response's as_of, rows and count alike, so meta.window.until equals as_of. A write committed after as_of can carry an earlier updated_at or triggered_at, so resume a few minutes before as_of and the overlap is idempotent. A bound that does not parse, or a triggered_since later than triggered_until, is rejected with bad_request. With no bounds the device list's window is mode all and the alert list's is mode recent, the default seven-day-or-unresolved window, so an explicit range is the only way to read resolved alerts older than a week.

Check execution intervals (interval_seconds) and soft-state retry intervals (retry_interval_seconds) must be at least 30 seconds. Per-attempt timeout_ms must be between 1 and 30000 milliseconds. Creating or updating a check outside these bounds returns 422 unprocessable_entity with field-level validation errors.

Alert responses include platform_degraded, a boolean recording whether TowerOps was degraded when the alert was created. It remains true after platform recovery; it does not describe the platform's current health. GraphQL exposes the same historical flag as platformDegraded.

This holds for every response from the REST API documented on this page, including the ones no endpoint produced: an unknown path answers not_found, a rate-limited request answers rate_limited with a Retry-After header, and an unexpected failure answers internal_error. The mobile API under /api/v1/mobile keeps its own older contract, documented in its own section below.

Rate limits are 1000 requests per minute per IP and per organization for /api/v1 and /api/graphql, 10 per minute per IP for the credential endpoints, log-in, registration and the OAuth token exchange, and 300 per minute per IP for the inbound webhooks. A request over budget answers 429 with Retry-After carrying the whole seconds to wait.

Stable error codes: authentication_requiredrate_limitedinvalid_tokenforbiddeninsufficient_scopenot_foundbad_requestvalidation_errorplan_limitconflictupstream_errortimeoutinternal_error

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": {
      "fields": {
        "name": ["can't be blank"]
      }
    }
  }
}

Sites

Physical locations that devices are deployed at.

List sites

GET /api/v1/sites

Lists sites for the authenticated organization, one page at a time. With no pagination parameters the first page of 100 is returned, the list is never unbounded, and `meta` is always present. Responses also carry `X-Page`, `X-Per-Page`, `X-Total-Count` and `X-Total-Pages` headers.

Requires scope sites:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`. Carries both the page and the page size, so it takes precedence over `page` and `per_page`. A cursor that does not decode is rejected with `bad_request`.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.

Responses

200 application/json
Sites
400 application/json
The `cursor` parameter did not decode
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/sites \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=50"

Response 200

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "location": "New York, NY",
      "name": "Main Office",
      "snmp_community_set": true,
      "snmp_port": 161,
      "snmp_version": "2c"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Create a site

POST /api/v1/sites

Creates a site in the authenticated organization.

Requires scope sites:write

Request body

Site attributes

site object required
name string required
Site name.
address string
Street address.
agent_token_id uuid
Agent to poll devices at this site.
description string
Free-form description.
display_order integer
Sort position in the site list.
latitude number
Latitude in decimal degrees.
location string
Human-readable location.
longitude number
Longitude in decimal degrees.
parent_site_id uuid
Parent site.
snmp_community string
Default SNMP community for devices at this site.
snmp_port integer
Default SNMP port.
snmp_transport string
Default SNMP transport.
snmp_version string
Default SNMP version (`1`, `2c` or `3`).

Responses

201 application/json
The created site
400 application/json
The `site` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/sites \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "site": {
    "address": "string",
    "agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "description": "string",
    "display_order": 0,
    "latitude": 0.0,
    "location": "string",
    "longitude": 0.0,
    "name": "string",
    "parent_site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_community": "string",
    "snmp_port": 0,
    "snmp_transport": "udp",
    "snmp_version": "string"
  }
}'

Response 201

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "location": "New York, NY",
    "name": "Main Office",
    "snmp_community_set": true,
    "snmp_port": 161,
    "snmp_version": "2c"
  }
}

Retrieve a site

GET /api/v1/sites/{id}

Gets one site by ID.

Requires scope sites:read

Parameters

id string in path required
Site ID.

Responses

200 application/json
The site
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No site with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/sites/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "location": "New York, NY",
    "name": "Main Office",
    "snmp_community_set": true,
    "snmp_port": 161,
    "snmp_version": "2c"
  }
}

Update a site

PATCH PUT /api/v1/sites/{id}

Updates the given attributes of a site. Omitted attributes are left alone.

Requires scope sites:write

Parameters

id string in path required
Site ID.

Request body

Site attributes

site object required
name string required
Site name.
address string
Street address.
agent_token_id uuid
Agent to poll devices at this site.
description string
Free-form description.
display_order integer
Sort position in the site list.
latitude number
Latitude in decimal degrees.
location string
Human-readable location.
longitude number
Longitude in decimal degrees.
parent_site_id uuid
Parent site.
snmp_community string
Default SNMP community for devices at this site.
snmp_port integer
Default SNMP port.
snmp_transport string
Default SNMP transport.
snmp_version string
Default SNMP version (`1`, `2c` or `3`).

Responses

200 application/json
The updated site
400 application/json
The `site` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No site with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/sites/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "site": {
    "address": "string",
    "agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "description": "string",
    "display_order": 0,
    "latitude": 0.0,
    "location": "string",
    "longitude": 0.0,
    "name": "string",
    "parent_site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_community": "string",
    "snmp_port": 0,
    "snmp_transport": "udp",
    "snmp_version": "string"
  }
}'

Response 200

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "location": "New York, NY",
    "name": "Main Office",
    "snmp_community_set": true,
    "snmp_port": 161,
    "snmp_version": "2c"
  }
}

Delete a site

DELETE /api/v1/sites/{id}

Deletes a site, every site under it, the devices at all of them, and the alerts raised against them.

Requires scope sites:write

Parameters

id string in path required
Site ID.

Responses

200 application/json
The site was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No site with that ID in this organization
422 application/json
The site could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/sites/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Devices

Monitored devices and their SNMP configuration.

List devices

GET /api/v1/devices

Lists devices for the authenticated organization, one page at a time. With no pagination parameters the first page of 100 is returned, the list is never unbounded, and `meta` is always present. `coverage_below` narrows the result to active, monitored devices below that seven-day percentage. The device list is returned under `data` and the next-page cursor under `meta.next_cursor`. A `page` past the last one returns an empty `data`, so paging until the list is empty terminates. `updated_since` (ISO 8601 with an offset) limits the list to devices whose `updated_at` is at or after that instant, and the window closes at the request's own `as_of`, rows and `meta.total_count` alike, so `meta.window.until` equals `as_of`. A write committed after `as_of` can carry an earlier `updated_at`, so resume a few minutes before `as_of`; the overlap is idempotent. `meta.window` echoes the exact bounds the query applied. An unparseable `updated_since` is rejected with `bad_request`.

Requires scope devices:read

Parameters

coverage_below integer in query
Only devices with seven-day monitoring coverage below this percentage.
cursor string in query
Opaque cursor from `meta.next_cursor`. Carries both the page and the page size, so it takes precedence over `page` and `per_page`. A cursor that does not decode is rejected with `bad_request`.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.
site_id string in query
Only return devices at this site.
updated_since date-time in query
Only devices whose `updated_at` is at or after this ISO 8601 instant are listed. The window closes at the request's `as_of`, echoed in `meta.window`; a write committed after `as_of` can carry an earlier `updated_at`, so resume a few minutes before it.

Responses

200 application/json
Devices
400 application/json
The `cursor` parameter did not decode
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/devices \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "coverage_below=90" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=50" \
  --data-urlencode "site_id=VALUE" \
  --data-urlencode "updated_since=2026-10-19T10:00:00Z"

Response 200

{
  "data": [
    {
      "device_role": "router",
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "inserted_at": "2026-01-15T19:44:25Z",
      "ip_address": "10.0.0.1",
      "monitoring_enabled": true,
      "name": "Core Router",
      "site_id": "550e8400-e29b-41d4-a716-446655440000",
      "snmp_enabled": true,
      "snmp_version": "2c"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1,
    "window": {
      "as_of": "2026-10-20T10:00:00Z",
      "field": "updated_at",
      "mode": "range",
      "since": "2026-10-19T10:00:00Z",
      "until": "2026-10-20T10:00:00Z"
    }
  }
}

Create a device

POST /api/v1/devices

Creates a device in the authenticated organization. `organization_id` is always taken from the token, so a value sent in the body is ignored. `site_id` is optional for an unrestricted token and, when given, must name a site in the same organization. A site-restricted token must provide a site in its own restriction; an omitted, `null`, or out-of-scope site is rejected as `not_found`. `check_interval_seconds` defaults to 300 and must be between 300 and 3600; outside that range the request is rejected with `unprocessable_entity`. Creating a device with `snmp_enabled` true also enqueues an SNMP discovery job in the background; the response does not wait for it.

Requires scope devices:write

Request body

Device attributes

device object required
ip_address string required
Management IP address.
check_interval_seconds integer
Probe interval in seconds.
description string
Free-form description.
device_role string
Role the device plays in the network.
eol_on string
Vendor end-of-life date (ISO 8601).
monitoring_enabled boolean
Enable ICMP monitoring.
name string
Device name.
purchased_on string
Purchase date (ISO 8601).
site_id uuid
Site to place the device in.
snmp_community string
SNMPv1/v2c community. Write-only.
snmp_enabled boolean
Enable SNMP polling.
snmp_port integer
SNMP port.
snmp_version string
SNMP version.
snmpv3_auth_password string
SNMPv3 auth password. Write-only.
snmpv3_auth_protocol string
SNMPv3 authentication protocol.
snmpv3_priv_password string
SNMPv3 privacy password. Write-only.
snmpv3_priv_protocol string
SNMPv3 privacy protocol.
snmpv3_security_level string
SNMPv3 security level.
snmpv3_username string
SNMPv3 username.
warranty_expires_on string
Warranty expiry date (ISO 8601).

Responses

201 application/json
The created device. The body carries the collection field set, so `description` and `check_interval_seconds` are omitted here even when they were sent.
400 application/json
The `device` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
402 application/json
The plan's device limit is reached (`plan_limit`; `error.details` carries `current`, `limit` and `upgrade_url`)
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The referenced `site_id` is not in this organization or outside the token's site restriction
409 application/json
A device with this IP address already exists at this site
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/devices \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "device": {
    "check_interval_seconds": 0,
    "description": "string",
    "device_role": "router",
    "eol_on": "string",
    "ip_address": "string",
    "monitoring_enabled": true,
    "name": "string",
    "purchased_on": "string",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_community": "string",
    "snmp_enabled": true,
    "snmp_port": 0,
    "snmp_version": "1",
    "snmpv3_auth_password": "string",
    "snmpv3_auth_protocol": "string",
    "snmpv3_priv_password": "string",
    "snmpv3_priv_protocol": "string",
    "snmpv3_security_level": "string",
    "snmpv3_username": "string",
    "warranty_expires_on": "string"
  }
}'

Response 201

{
  "data": {
    "device_role": "router",
    "id": "770e8400-e29b-41d4-a716-446655440002",
    "inserted_at": "2026-01-15T19:44:25Z",
    "ip_address": "10.0.0.1",
    "monitoring_enabled": true,
    "name": "Core Router",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_enabled": true,
    "snmp_version": "2c"
  }
}

Apply one operation to many devices

PATCH /api/v1/devices/bulk

Applies one fleet write to `device_ids` in a single request (issue 1906): exactly one of `site_id`, `lifecycle` (`paused`/`retired`) or `agent_token_id`. One call applies the same write every UI bulk action performs, including the same webhook events, so a nightly inventory sync costs one request per pass against the organization budget. The response reports the outcome per id under `results` (in request order) plus `updated_ids`/`skipped_ids`: an id outside the caller's site restriction, not in the organization, or a no-op for the operation reports `skipped`, and skipped ids never roll back the updated ones. A `site_id` destination outside the token's site restriction or the organization answers `not_found`; a `site_id` move that would collide an IP is rejected before any write. `Idempotency-Key` is honoured like every other write.

Requires scope devices:write

Request body

Bulk operation

device_ids array of uuid required
Device ids to write (at most 5000 per request).
agent_token_id uuid
Agent token to assign to all devices.
lifecycle string
Lifecycle state to set on all devices.
site_id uuid
Destination site for all devices.

Responses

200 application/json
Per-id outcome
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The destination site does not exist here or is outside the token's site restriction
409 application/json
A destination site's address is already in use
422 application/json
Missing/ambiguous operation, bad lifecycle, non-UUID or over-cap device_ids
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/devices/bulk \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
  "device_ids": [
    "550e8400-e29b-41d4-a716-446655440000"
  ],
  "lifecycle": "paused",
  "site_id": "550e8400-e29b-41d4-a716-446655440000"
}'

Response 200

{
  "data": {
    "results": [
      {
        "device_id": "550e8400-e29b-41d4-a716-446655440000",
        "result": "string"
      }
    ],
    "skipped": 0,
    "skipped_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ],
    "updated": 0,
    "updated_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ]
  }
}

Retrieve a device

GET /api/v1/devices/{id}

Gets one device by ID. The detail shape adds `description`, `check_interval_seconds`, and `monitoring_history`: 24-hour and seven-day coverage, each window's longest recovered gap, and the five latest poll failures and recovered gaps. SNMP secrets are never returned: the body reports only `snmpv3_auth_password_set` and `snmpv3_priv_password_set`.

Requires scope devices:read

Parameters

id string in path required
Device ID.

Responses

200 application/json
The device
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/devices/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "check_interval_seconds": 0,
    "description": "string",
    "device_role": "router",
    "id": "770e8400-e29b-41d4-a716-446655440002",
    "inserted_at": "2026-01-15T19:44:25Z",
    "ip_address": "10.0.0.1",
    "monitoring_enabled": true,
    "monitoring_history": {
      "last_24_hours": {
        "coverage_percent": 0.0,
        "expected": 0,
        "from": "2026-01-15T19:44:25Z",
        "has_data": true,
        "longest_gap": {
          "duration_seconds": null,
          "ended_at": null,
          "id": null,
          "started_at": null
        },
        "observed": 0,
        "to": "2026-01-15T19:44:25Z"
      },
      "last_7_days": {
        "coverage_percent": 0.0,
        "expected": 0,
        "from": "2026-01-15T19:44:25Z",
        "has_data": true,
        "longest_gap": {
          "duration_seconds": null,
          "ended_at": null,
          "id": null,
          "started_at": null
        },
        "observed": 0,
        "to": "2026-01-15T19:44:25Z"
      },
      "recent_poll_failures": [
        {
          "id": null,
          "message": null,
          "started_at": null
        }
      ],
      "recent_poll_gaps": [
        {
          "duration_seconds": null,
          "ended_at": null,
          "id": null,
          "started_at": null
        }
      ]
    },
    "name": "Core Router",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_enabled": true,
    "snmp_version": "2c"
  }
}

Update a device

PATCH PUT /api/v1/devices/{id}

Updates the given attributes of a device. Omitted attributes are left alone. `site_id`, when given, must name a site in the same organization and be inside the caller's site restriction. A site-restricted caller cannot clear a device's site with `null`; out-of-scope or siteless changes are rejected with `not_found`. `check_interval_seconds` must be between 300 and 3600; outside that range the request is rejected with `unprocessable_entity`.

Requires scope devices:write

Parameters

id string in path required
Device ID.

Request body

Device attributes

device object required
ip_address string required
Management IP address.
check_interval_seconds integer
Probe interval in seconds.
description string
Free-form description.
device_role string
Role the device plays in the network.
eol_on string
Vendor end-of-life date (ISO 8601).
monitoring_enabled boolean
Enable ICMP monitoring.
name string
Device name.
purchased_on string
Purchase date (ISO 8601).
site_id uuid
Site to place the device in.
snmp_community string
SNMPv1/v2c community. Write-only.
snmp_enabled boolean
Enable SNMP polling.
snmp_port integer
SNMP port.
snmp_version string
SNMP version.
snmpv3_auth_password string
SNMPv3 auth password. Write-only.
snmpv3_auth_protocol string
SNMPv3 authentication protocol.
snmpv3_priv_password string
SNMPv3 privacy password. Write-only.
snmpv3_priv_protocol string
SNMPv3 privacy protocol.
snmpv3_security_level string
SNMPv3 security level.
snmpv3_username string
SNMPv3 username.
warranty_expires_on string
Warranty expiry date (ISO 8601).

Responses

200 application/json
The updated device
400 application/json
The `device` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID in this organization, or the referenced `site_id` is not in this organization or outside the token's site restriction
409 application/json
A device with this IP address already exists at this site
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/devices/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "device": {
    "check_interval_seconds": 0,
    "description": "string",
    "device_role": "router",
    "eol_on": "string",
    "ip_address": "string",
    "monitoring_enabled": true,
    "name": "string",
    "purchased_on": "string",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_community": "string",
    "snmp_enabled": true,
    "snmp_port": 0,
    "snmp_version": "1",
    "snmpv3_auth_password": "string",
    "snmpv3_auth_protocol": "string",
    "snmpv3_priv_password": "string",
    "snmpv3_priv_protocol": "string",
    "snmpv3_security_level": "string",
    "snmpv3_username": "string",
    "warranty_expires_on": "string"
  }
}'

Response 200

{
  "data": {
    "check_interval_seconds": 0,
    "description": "string",
    "device_role": "router",
    "id": "770e8400-e29b-41d4-a716-446655440002",
    "inserted_at": "2026-01-15T19:44:25Z",
    "ip_address": "10.0.0.1",
    "monitoring_enabled": true,
    "monitoring_history": {
      "last_24_hours": {
        "coverage_percent": 0.0,
        "expected": 0,
        "from": "2026-01-15T19:44:25Z",
        "has_data": true,
        "longest_gap": {
          "duration_seconds": null,
          "ended_at": null,
          "id": null,
          "started_at": null
        },
        "observed": 0,
        "to": "2026-01-15T19:44:25Z"
      },
      "last_7_days": {
        "coverage_percent": 0.0,
        "expected": 0,
        "from": "2026-01-15T19:44:25Z",
        "has_data": true,
        "longest_gap": {
          "duration_seconds": null,
          "ended_at": null,
          "id": null,
          "started_at": null
        },
        "observed": 0,
        "to": "2026-01-15T19:44:25Z"
      },
      "recent_poll_failures": [
        {
          "id": null,
          "message": null,
          "started_at": null
        }
      ],
      "recent_poll_gaps": [
        {
          "duration_seconds": null,
          "ended_at": null,
          "id": null,
          "started_at": null
        }
      ]
    },
    "name": "Core Router",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_enabled": true,
    "snmp_version": "2c"
  }
}

Delete a device

DELETE /api/v1/devices/{id}

Deletes a device along with the checks and results that hang off it.

Requires scope devices:write

Parameters

id string in path required
Device ID.

Responses

200 application/json
The device was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID in this organization
422 application/json
The device could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/devices/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Metrics

Check results, time series and interfaces for a device.

List a device's checks with their latest result

GET /api/v1/devices/{device_id}/checks

Lists every check configured on the device, each with the status, value and timestamp of its most recent result. Unlike `GET /api/v1/checks` this includes the internally managed SNMP check types. `enabled` and `alerting` are different columns: a disarmed check still runs and still reports a status here, it just never pages. `latest_status`, `latest_value` and `latest_checked_at` are null while a check has not produced a result yet.

Requires scope checks:read

Parameters

device_id string in path required
Device ID.

Responses

200 application/json
Checks on the device
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/devices/$DEVICE_ID/checks \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "alerting": true,
      "check_type": "string",
      "enabled": true,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "interval_seconds": 0,
      "latest_checked_at": "2026-01-15T19:44:25Z",
      "latest_status": "string",
      "latest_value": 0.0,
      "name": "string"
    }
  ]
}

List a device's SNMP interfaces

GET /api/v1/devices/{device_id}/interfaces

Lists the interfaces SNMP discovery has recorded for the device. `data` is an empty list when the device has no SNMP record yet, because SNMP is disabled on it, or because discovery has not run. A device that belongs to another organization answers `404`, the same as one that does not exist at all, telling a caller that a device id it cannot read nevertheless exists would be an enumeration oracle.

Requires scope devices:read

Parameters

device_id string in path required
Device ID.

Responses

200 application/json
Interfaces on the device
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/devices/$DEVICE_ID/interfaces \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "if_admin_status": "string",
      "if_alias": "string",
      "if_descr": "string",
      "if_index": 0,
      "if_name": "string",
      "if_oper_status": "string",
      "if_speed": 0,
      "monitored": true
    }
  ]
}

List a device's metric series

GET /api/v1/devices/{device_id}/metrics

Returns one time series per matching check on the device, each point carrying the result's `timestamp`, `value` and `status`. The window ends now and reaches back `hours` hours; it defaults to the last 24 hours and is clamped to 1-720 hours (30 days) regardless of the requested value. A `hours` value that is not a number falls back to the default rather than failing. The series always spans the whole requested window: when the window holds more raw results than the response should carry, at most 2,000 points per check and 20,000 across the device, the series is downsampled (newest point plus evenly spread older points) rather than cut short. A device that belongs to another organization answers `404`, the same as one that does not exist at all, telling a caller that a device id it cannot read nevertheless exists would be an enumeration oracle.

Requires scope devices:read

Parameters

device_id string in path required
Device ID.
check_type string in query
Only return series for checks of this type. Omit for every check on the device.
hours integer in query
How many hours back to read. Defaults to 24, clamped to 1-720.
sensor_type string in query
Legacy alias for `check_type`, kept for older clients. It takes precedence when both are sent.

Responses

200 application/json
One series per matching check
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/devices/$DEVICE_ID/metrics \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "check_type=snmp_sensor" \
  --data-urlencode "hours=24" \
  --data-urlencode "sensor_type=VALUE"

Response 200

{
  "data": [
    {
      "check_id": "550e8400-e29b-41d4-a716-446655440000",
      "check_name": "string",
      "check_type": "string",
      "data": [
        {
          "status": "string",
          "timestamp": "2026-01-15T19:44:25Z",
          "value": 0.0
        }
      ]
    }
  ]
}

Checks

Monitoring checks attached to devices.

List checks

GET /api/v1/checks

Lists service checks for the authenticated organization. Only the service check types (`http`, `tcp`, `dns`, `ping`) are exposed over the API; SNMP checks are managed internally and never appear in this list, the type filter is applied in SQL, so the page window and `meta.total_count` cover the service checks only, even with an explicit `check_type` filter. Pagination works like the other collection endpoints: `page`/`per_page` (per page defaults to 100 and is capped at 100), or an opaque `cursor` from `meta.next_cursor`, which takes precedence. `meta` is always present. A cursor that does not decode is rejected with `bad_request`.

Requires scope checks:read

Parameters

check_type string in query
Only return checks of this type. One of `http`, `tcp`, `dns`, `ping`. Other non-empty strings return an empty page; non-string values are rejected with `bad_request`.
cursor string in query
Opaque cursor from `meta.next_cursor`. Carries both the page and the page size, so it takes precedence over `page` and `per_page`. A cursor that does not decode is rejected with `bad_request`.
device_id string in query
Only return checks that run against this device.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.

Responses

200 application/json
Checks
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/checks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "check_type=http" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "device_id=VALUE" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=50"

Response 200

{
  "data": [
    {
      "alerting": true,
      "check_type": "http",
      "config": {
        "expected_status": 200,
        "url": "https://example.com/health"
      },
      "current_state": "ok",
      "current_state_type": "hard",
      "enabled": true,
      "id": "880e8400-e29b-41d4-a716-446655440003",
      "interval_seconds": 60,
      "name": "HTTPS health"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Create a check

POST /api/v1/checks

Creates a service check in the authenticated organization. `check_type` must be one of `http`, `tcp`, `dns` or `ping`; any other type, including the internally managed SNMP types, is rejected with `bad_request`. A `device_id` that belongs to another organization is rejected with `403`. Probe details go in `config` and are type-specific, e.g. `url` and `expected_status` for an `http` check, or `host` and `port` for `tcp`. `interval_seconds` and `retry_interval_seconds` must be at least 30. `timeout_ms` must be between 1 and 30000. Unsafe execution bounds return `unprocessable_entity` with field-level validation errors. `enabled` and `alerting` are different columns: `enabled` controls whether the check runs at all, `alerting` whether a failure raises an alert. A check created with `enabled` true is scheduled immediately.

Requires scope checks:write

Request body

Check attributes

check object required
check_type string required
What the check probes. The REST API accepts only these service-check types.
config object required
Type-specific configuration.
name string required
Check name.
agent_token_id uuid
Pin execution to a specific agent.
alerting boolean
Whether a failing check raises an alert.
description string
Free-form description.
device_id uuid
Device the check runs against. Omit for a check that is not attached to a device.
enabled boolean
Whether the check runs.
interval_seconds integer
Execution interval in seconds (minimum 30).
max_check_attempts integer
Attempts before a soft state is promoted to hard.
retry_interval_seconds integer
Interval between retries in a soft state, in seconds (minimum 30).
timeout_ms integer
Per-attempt timeout in milliseconds (1-30000).

Responses

201 application/json
The created check
400 application/json
The `check` parameter is missing, or `check_type` is not a service check type
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/checks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "check": {
    "agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "alerting": true,
    "check_type": "http",
    "config": null,
    "description": "string",
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "enabled": true,
    "interval_seconds": 0,
    "max_check_attempts": 0,
    "name": "string",
    "retry_interval_seconds": 0,
    "timeout_ms": 0
  }
}'

Response 201

{
  "data": {
    "alerting": true,
    "check_type": "http",
    "config": {
      "expected_status": 200,
      "url": "https://example.com/health"
    },
    "current_state": "ok",
    "current_state_type": "hard",
    "enabled": true,
    "id": "880e8400-e29b-41d4-a716-446655440003",
    "interval_seconds": 60,
    "name": "HTTPS health"
  }
}

Retrieve a check

GET /api/v1/checks/{id}

Gets one check by ID. `config` carries the type-specific probe settings and `alerting` reports whether the check is armed, independently of `enabled`. A check that has been deleted answers `not_found`, and so does an internally managed SNMP check: this endpoint exposes service checks only.

Requires scope checks:read

Parameters

id string in path required
Check ID.

Responses

200 application/json
The check
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No check with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/checks/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "alerting": true,
    "check_type": "http",
    "config": {
      "expected_status": 200,
      "url": "https://example.com/health"
    },
    "current_state": "ok",
    "current_state_type": "hard",
    "enabled": true,
    "id": "880e8400-e29b-41d4-a716-446655440003",
    "interval_seconds": 60,
    "name": "HTTPS health"
  }
}

Update a check

PATCH PUT /api/v1/checks/{id}

Updates the given attributes of a check. Omitted attributes are left alone. `check_type` is identity, not a setting, and is ignored here: a payload that carries it changes nothing, so a check can never be re-typed over the API (including onto an internally managed SNMP type). `alerting` is handled apart from the rest of the payload: it must be a boolean, or the strings `"true"`/`"false"` for form-encoded requests. Anything else is refused with `bad_request` rather than defaulted. Disarming a check also resolves the alerts it has open, so it stops escalating; it does not stop the check running, that is `enabled`. The rest of the update is applied first, so a payload rejected with `unprocessable_entity` leaves the arming state untouched. Execution and retry intervals must be at least 30 seconds; `timeout_ms` must be between 1 and 30000, just as on creation.

Requires scope checks:write

Parameters

id string in path required
Check ID.

Request body

Check attributes

check object required
check_type string required
What the check probes. The REST API accepts only these service-check types.
config object required
Type-specific configuration.
name string required
Check name.
agent_token_id uuid
Pin execution to a specific agent.
alerting boolean
Whether a failing check raises an alert.
description string
Free-form description.
device_id uuid
Device the check runs against. Omit for a check that is not attached to a device.
enabled boolean
Whether the check runs.
interval_seconds integer
Execution interval in seconds (minimum 30).
max_check_attempts integer
Attempts before a soft state is promoted to hard.
retry_interval_seconds integer
Interval between retries in a soft state, in seconds (minimum 30).
timeout_ms integer
Per-attempt timeout in milliseconds (1-30000).

Responses

200 application/json
The updated check
400 application/json
The `check` parameter is missing, or `alerting` was neither true nor false
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No check with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/checks/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "check": {
    "agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "alerting": true,
    "check_type": "http",
    "config": null,
    "description": "string",
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "enabled": true,
    "interval_seconds": 0,
    "max_check_attempts": 0,
    "name": "string",
    "retry_interval_seconds": 0,
    "timeout_ms": 0
  }
}'

Response 200

{
  "data": {
    "alerting": true,
    "check_type": "http",
    "config": {
      "expected_status": 200,
      "url": "https://example.com/health"
    },
    "current_state": "ok",
    "current_state_type": "hard",
    "enabled": true,
    "id": "880e8400-e29b-41d4-a716-446655440003",
    "interval_seconds": 60,
    "name": "HTTPS health"
  }
}

Delete a check

DELETE /api/v1/checks/{id}

Deletes a check and cancels any jobs already scheduled for it.

Requires scope checks:write

Parameters

id string in path required
Check ID.

Responses

200 application/json
The check was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No check with that ID in this organization
422 application/json
The check could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/checks/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Alerts

Alerts, and acknowledging or resolving them.

List alerts

GET /api/v1/alerts

Lists alerts for the authenticated organization, most recently triggered first, one page at a time. Pagination works like the other collection endpoints: `page`/`per_page` (per page defaults to 100 and is capped at 100), or an opaque `cursor` from `meta.next_cursor`, which takes precedence. `meta` carries the page, `total_count` and the next cursor. A cursor that does not decode is rejected with `bad_request`. A `page` past the last one returns an empty `data`, so paging until the list is empty terminates. By default history is capped at the last seven days, except that an alert which is still unresolved is always listed no matter how old it is. `triggered_since`/`triggered_until` (ISO 8601 with an offset) replace that default with an explicit inclusive `triggered_at` range, resolved alerts older than a week inside the range are listed too. An omitted `triggered_until` closes at the request's `as_of`, rows and `meta.total_count` alike, so `meta.window.until` equals `as_of`. A write committed after `as_of` can carry an earlier `triggered_at`, so resume a few minutes before `as_of`; the overlap is idempotent. `meta.window` echoes the exact bounds the query applied. An unparseable or inverted bound is rejected with `bad_request`. `subscriber_impact` is a nullable object, not a subscriber count. Totals, level amounts and individual subscriber revenue are included only for a user allowed to view financials; partner apps never receive revenue. `platform_degraded` records whether TowerOps was degraded when the alert was created. It remains true after recovery, rather than tracking current health.

Requires scope alerts:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`. Carries both the page and the page size, so it takes precedence over `page` and `per_page`. A cursor that does not decode is rejected with `bad_request`.
device_id string in query
Only return alerts raised against this device ID.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.
status string in query
Filter by alert state: `active` (unresolved, unacknowledged, not snoozed), `acknowledged`, `snoozed`, `resolved`, `unresolved`, or `critical`. Any other value is ignored and no status filter is applied.
triggered_since date-time in query
Only alerts whose `triggered_at` is at or after this ISO 8601 instant are listed; the window replaces the seven-day-or-unresolved default and closes at `as_of` unless `triggered_until` is sent. A write committed after `as_of` can carry an earlier `triggered_at`, so resume a few minutes before it.
triggered_until date-time in query
Only alerts whose `triggered_at` is at or before this ISO 8601 instant are listed.

Responses

200 application/json
Alerts
400 application/json
The `cursor` parameter did not decode, or a `triggered_since`/`triggered_until` bound was unparseable, a non-string value, or inverted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/alerts \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "device_id=VALUE" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=50" \
  --data-urlencode "status=active" \
  --data-urlencode "triggered_since=2026-10-13T00:00:00Z" \
  --data-urlencode "triggered_until=2026-10-20T00:00:00Z"

Response 200

{
  "data": [
    {
      "acknowledged_at": null,
      "alert_type": "device_down",
      "device_name": "Core Router",
      "id": "bb0e8400-e29b-41d4-a716-446655440005",
      "message": "Core Router is not responding to ICMP",
      "platform_degraded": false,
      "resolved_at": null,
      "triggered_at": "2026-01-15T19:44:25Z"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1,
    "window": {
      "as_of": "2026-10-20T10:00:00Z",
      "field": "updated_at",
      "mode": "range",
      "since": "2026-10-19T10:00:00Z",
      "until": "2026-10-20T10:00:00Z"
    }
  }
}

Retrieve an alert

GET /api/v1/alerts/{id}

Gets one alert by ID. Alerts belonging to another organization are reported as not found.

Requires scope alerts:read

Parameters

id string in path required
Alert ID.

Responses

200 application/json
The alert
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/alerts/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "acknowledged_at": null,
    "alert_type": "device_down",
    "device_name": "Core Router",
    "id": "bb0e8400-e29b-41d4-a716-446655440005",
    "message": "Core Router is not responding to ICMP",
    "platform_degraded": false,
    "resolved_at": null,
    "triggered_at": "2026-01-15T19:44:25Z"
  }
}

Acknowledge an alert

POST /api/v1/alerts/{id}/acknowledge

Marks an alert as acknowledged and returns the updated alert with `200`, in the usual `data` envelope, not `202`, and not an empty body. The acknowledging user is taken from the session behind the token, so `acknowledged_by_email` is only populated when the request carries one; otherwise the alert is acknowledged without an attributed user.

Requires scope alerts:write

Parameters

id string in path required
Alert ID.

Responses

200 application/json
The acknowledged alert
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
422 application/json
The alert could not be acknowledged
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/alerts/$ID/acknowledge \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "acknowledged_at": null,
    "alert_type": "device_down",
    "device_name": "Core Router",
    "id": "bb0e8400-e29b-41d4-a716-446655440005",
    "message": "Core Router is not responding to ICMP",
    "platform_degraded": false,
    "resolved_at": null,
    "triggered_at": "2026-01-15T19:44:25Z"
  }
}

Escalate an alert

POST /api/v1/alerts/{id}/escalate

Pages the next on-call level for the alert's active escalation now and returns the alert with `200` in the usual `data` envelope, the level itself lives on the on-call incident, not on the alert. Refused with `unprocessable_entity` when the alert has no active escalation or the policy is already at its last level, matching the UI's Escalate button.

Requires scope alerts:write

Parameters

id string in path required
Alert ID.

Responses

200 application/json
The escalated alert
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
422 application/json
No escalation to advance
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/alerts/$ID/escalate \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "acknowledged_at": null,
    "alert_type": "device_down",
    "device_name": "Core Router",
    "id": "bb0e8400-e29b-41d4-a716-446655440005",
    "message": "Core Router is not responding to ICMP",
    "platform_degraded": false,
    "resolved_at": null,
    "triggered_at": "2026-01-15T19:44:25Z"
  }
}

Add a note to an alert

POST /api/v1/alerts/{id}/notes

Records an operator note ("what I did") on the alert and returns it with `200` in the usual `data` envelope. Notes are attributed to the user behind the token when there is one, timestamped, listed oldest-first in the single-alert response, and kept on the alert after it resolves.

Requires scope alerts:write

Parameters

id string in path required
Alert ID.

Request body

The note

note object required
body string required
What was done, the note itself.

Responses

200 application/json
The created note
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
422 application/json
The note is empty or too long
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/alerts/$ID/notes \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "note": {
    "body": "string"
  }
}'

Response 200

{
  "data": {
    "alert_id": "bb0e8400-e29b-41d4-a716-446655440005",
    "author_email": "noc@example.com",
    "body": "Dispatched a tech at 14:20; power company notified.",
    "id": "bb0e8400-e29b-41d4-a716-4466554400cc",
    "inserted_at": "2026-01-15T19:44:30Z",
    "user_id": "bb0e8400-e29b-41d4-a716-4466554400bb"
  }
}

Resolve an alert

POST /api/v1/alerts/{id}/resolve

Marks an alert as resolved and returns the updated alert with `200` in the usual `data` envelope, with `resolved_at` set. Resolving does not require the alert to have been acknowledged first.

Requires scope alerts:write

Parameters

id string in path required
Alert ID.

Responses

200 application/json
The resolved alert
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
422 application/json
The alert could not be resolved
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/alerts/$ID/resolve \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "acknowledged_at": null,
    "alert_type": "device_down",
    "device_name": "Core Router",
    "id": "bb0e8400-e29b-41d4-a716-446655440005",
    "message": "Core Router is not responding to ICMP",
    "platform_degraded": false,
    "resolved_at": null,
    "triggered_at": "2026-01-15T19:44:25Z"
  }
}

Snooze an alert

POST /api/v1/alerts/{id}/snooze

Silences an alert for `minutes` from now and returns the updated alert with `200` in the usual `data` envelope, with `snoozed_until` set. The alert stays open and visible under the `snoozed` status filter but stops paging. `minutes` is clamped to one week, matching the UI's own cap; snoozing a resolved alert is refused with `unprocessable_entity`.

Requires scope alerts:write

Parameters

id string in path required
Alert ID.

Request body

Snooze duration

snooze object required
minutes integer required
How many minutes from now the alert stays quiet.

Responses

200 application/json
The snoozed alert
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
422 application/json
The alert could not be snoozed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/alerts/$ID/snooze \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "snooze": {
    "minutes": 0
  }
}'

Response 200

{
  "data": {
    "acknowledged_at": null,
    "alert_type": "device_down",
    "device_name": "Core Router",
    "id": "bb0e8400-e29b-41d4-a716-446655440005",
    "message": "Core Router is not responding to ICMP",
    "platform_degraded": false,
    "resolved_at": null,
    "triggered_at": "2026-01-15T19:44:25Z"
  }
}

Agents

Remote pollers and their tokens.

List agents

GET /api/v1/agents

Lists every agent token in the authenticated organization. The list is not paginated and never includes the raw token.

Requires scope agents:read

Responses

200 application/json
Agents
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/agents \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "enabled": true,
      "expires_at": "2027-01-15T19:44:25Z",
      "id": "aa0e8400-e29b-41d4-a716-446655440004",
      "last_ip": "203.0.113.7",
      "last_seen_at": "2026-01-15T19:44:25Z",
      "name": "noc-poller-1"
    }
  ]
}

Create an agent

POST /api/v1/agents

Creates an agent token in the authenticated organization. Unlike the resourceful endpoints the body is sent unwrapped: `name` is read from the top level of the request, not from under an `agent` key. Only `name` is read; any other attribute in the body is ignored. The response carries the raw bearer token under `data.token`. It is returned exactly once, at creation, and cannot be retrieved afterwards, store it now.

Requires scope agents:write

Request body

Agent attributes, sent unwrapped

name string required
Agent name.

Responses

201 application/json
The created agent, including its raw token
400 application/json
The `name` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/agents \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "noc-poller-1"
}'

Response 201

{
  "data": {
    "enabled": true,
    "expires_at": "2027-01-15T19:44:25Z",
    "id": "aa0e8400-e29b-41d4-a716-446655440004",
    "last_ip": "203.0.113.7",
    "last_seen_at": "2026-01-15T19:44:25Z",
    "name": "noc-poller-1",
    "token": "string"
  }
}

Retrieve an agent

GET /api/v1/agents/{id}

Gets one agent by ID, with `device_count`: the number of devices currently assigned to it. The raw token is never returned here.

Requires scope agents:read

Parameters

id string in path required
Agent token ID.

Responses

200 application/json
The agent
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No agent with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/agents/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "device_count": 0,
    "enabled": true,
    "expires_at": "2027-01-15T19:44:25Z",
    "id": "aa0e8400-e29b-41d4-a716-446655440004",
    "last_ip": "203.0.113.7",
    "last_seen_at": "2026-01-15T19:44:25Z",
    "name": "noc-poller-1"
  }
}

Delete an agent

DELETE /api/v1/agents/{id}

Deletes an agent token. The agent can no longer connect; devices assigned to it are not deleted. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope agents:write

Parameters

id string in path required
Agent token ID.

Responses

200 application/json
The agent was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No agent with that ID in this organization
422 application/json
The agent could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/agents/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Organization

The organization the token belongs to.

Retrieve the organization

GET /api/v1/organization

Returns the organization the API token belongs to, including its default SNMP and RouterOS settings. Secrets are never returned: the stored SNMP community is reported only as the boolean `snmp_community_set`.

Requires scope organization:read

Responses

200 application/json
The organization
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/organization \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "default_agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "device_count": 0,
    "device_limit": 0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "mikrotik_enabled": true,
    "mikrotik_port": 0,
    "mikrotik_ssh_port": 0,
    "mikrotik_use_ssl": true,
    "mikrotik_username": "string",
    "name": "string",
    "slug": "string",
    "snmp_community_set": true,
    "snmp_port": 0,
    "snmp_transport": "udp",
    "snmp_version": "string",
    "snmpv3_auth_protocol": "string",
    "snmpv3_priv_protocol": "string",
    "snmpv3_security_level": "string",
    "snmpv3_username": "string",
    "subscription_plan": "string",
    "temperature_units": "celsius",
    "updated_at": "2026-01-15T19:44:25Z",
    "use_sites": true,
    "weather_units": "imperial"
  }
}

Update the organization

PATCH /api/v1/organization

Updates organization settings. Omitted attributes are left alone. Beyond the `organization:write` scope this endpoint also checks the user behind the token: only an organization owner (or a superuser) may change settings, so an otherwise valid token belonging to a non-owner is rejected with `403`.

Requires scope organization:write

Request body

Organization attributes, nested under `organization`

organization object required
alert_routing string
How alerts are routed, e.g. `builtin`.
default_agent_token_id uuid
Fallback agent for the whole organization.
default_escalation_policy_id uuid
Escalation policy applied when no schedule matches.
mikrotik_enabled boolean
Whether MikroTik integration credentials are used.
mikrotik_password string
MikroTik password. Write-only.
mikrotik_port integer
MikroTik REST API port.
mikrotik_ssh_port integer
MikroTik SSH port.
mikrotik_use_ssl boolean
Whether to reach the MikroTik API over TLS.
mikrotik_username string
MikroTik username.
name string
Organization name.
snmp_community string
Organization-default SNMP community. Write-only.
snmp_port integer
Organization-default SNMP port.
snmp_transport string
Organization-default SNMP transport.
snmp_version string
Organization-default SNMP version.
snmpv3_auth_password string
Organization-default SNMPv3 authentication password. Write-only.
snmpv3_auth_protocol string
Organization-default SNMPv3 authentication protocol.
snmpv3_priv_password string
Organization-default SNMPv3 privacy password. Write-only.
snmpv3_priv_protocol string
Organization-default SNMPv3 privacy protocol.
snmpv3_security_level string
Organization-default SNMPv3 security level.
snmpv3_username string
Organization-default SNMPv3 username.
temperature_units string
Device temperature display units. Display only, stored values stay Celsius.
use_sites boolean
Whether to group devices by site.
weather_units string
Weather display units. Display only, stored values stay SI.

Responses

200 application/json
The updated organization
400 application/json
The `organization` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/organization \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "organization": {
    "alert_routing": "string",
    "default_agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "default_escalation_policy_id": "550e8400-e29b-41d4-a716-446655440000",
    "mikrotik_enabled": true,
    "mikrotik_password": "string",
    "mikrotik_port": 0,
    "mikrotik_ssh_port": 0,
    "mikrotik_use_ssl": true,
    "mikrotik_username": "string",
    "name": "string",
    "snmp_community": "string",
    "snmp_port": 0,
    "snmp_transport": "udp",
    "snmp_version": "string",
    "snmpv3_auth_password": "string",
    "snmpv3_auth_protocol": "string",
    "snmpv3_priv_password": "string",
    "snmpv3_priv_protocol": "string",
    "snmpv3_security_level": "string",
    "snmpv3_username": "string",
    "temperature_units": "celsius",
    "use_sites": true,
    "weather_units": "imperial"
  }
}'

Response 200

{
  "data": {
    "default_agent_token_id": "550e8400-e29b-41d4-a716-446655440000",
    "device_count": 0,
    "device_limit": 0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "mikrotik_enabled": true,
    "mikrotik_port": 0,
    "mikrotik_ssh_port": 0,
    "mikrotik_use_ssl": true,
    "mikrotik_username": "string",
    "name": "string",
    "slug": "string",
    "snmp_community_set": true,
    "snmp_port": 0,
    "snmp_transport": "udp",
    "snmp_version": "string",
    "snmpv3_auth_protocol": "string",
    "snmpv3_priv_protocol": "string",
    "snmpv3_security_level": "string",
    "snmpv3_username": "string",
    "subscription_plan": "string",
    "temperature_units": "celsius",
    "updated_at": "2026-01-15T19:44:25Z",
    "use_sites": true,
    "weather_units": "imperial"
  }
}

Members

Organization membership and roles.

List members

GET /api/v1/members

Lists the users who belong to the authenticated organization, with their roles. The list is not paginated. Each member's `id` is that user's ID, which is what the member endpoints take in the path.

Requires scope members:read

Responses

200 application/json
Members
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/members \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "email": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "role": "string",
      "site_ids": [
        "string"
      ]
    }
  ]
}

Change a member's role or site restriction

PATCH PUT /api/v1/members/{id}

Changes the role of one member of the authenticated organization, their site restriction, or both, and returns the updated membership. The body is sent unwrapped: `role` and `site_ids` are read from the top level, not from under a `member` key. `site_ids` is the complete list of site ids the member may see, `[]` lifts the restriction back to the whole organization (issue 1775). The owner cannot be site-restricted, and a caller whose own membership is fenced can only grant sites inside their own list. Authorization is checked against the user behind the token as well as the scope, and every one of these failures is a `403`: the caller is not an owner or admin of the organization; the target member is the owner, whose role cannot be changed; the caller is not an owner and tried to grant `owner`; or the caller tried to assign a role more privileged than their own. Changing your own role is not itself blocked, the same rules apply as for anyone else.

Requires scope members:write

Parameters

id string in path required
The member's **user** ID, as returned in `data[].id` by the list endpoint, not a membership ID.

Request body

The new role and/or site list, sent unwrapped

role string
Role to assign.
site_ids array of string
The complete list of sites the member may see; [] lifts the restriction.

Responses

200 application/json
The updated membership
400 application/json
Neither `role` nor `site_ids` was sent
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No member with that user ID in this organization
422 application/json
The requested role is not a valid role
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/members/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "role": "admin",
  "site_ids": [
    "<site-uuid>"
  ]
}'

Response 200

{
  "data": {
    "email": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "role": "string",
    "site_ids": [
      "string"
    ]
  }
}

Remove a member

DELETE /api/v1/members/{id}

Removes a user from the authenticated organization. Success is `200` with `{"data": {"deleted": true}}`. Only owners and admins may remove members, and the organization's owner can never be removed; both refusals are a `403`.

Requires scope members:write

Parameters

id string in path required
The member's **user** ID, as returned in `data[].id` by the list endpoint, not a membership ID.

Responses

200 application/json
The member was removed
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No member with that user ID in this organization
422 application/json
The member could not be removed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/members/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Invitations

Pending invitations to the organization.

List pending invitations

GET /api/v1/invitations

Lists invitations for the authenticated organization that have not yet been accepted. The list is not paginated and never includes the invitation token.

Requires scope members:read

Responses

200 application/json
Pending invitations
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/invitations \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "email": "string",
      "expires_at": "2026-01-15T19:44:25Z",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "invited_by_email": "string",
      "role": "string"
    }
  ]
}

Invite a user

POST /api/v1/invitations

Invites an email address to join the authenticated organization. The invitation expires one week after it is created. The body is sent unwrapped and both fields are required: `email` and `role` are read from the top level, not from under an `invitation` key, and a request missing either one is rejected with `400`. Authorization is checked against the user behind the token as well as the scope. All of these are a `403`: the token has no user, the user is not a member of the organization, the user is not an owner or admin, the requested role is not a known role name, or the requested role is more privileged than the caller's own. `owner` is a known role but not an assignable invitation one, so it is rejected as a validation failure with `422`. An address that is already a member is accepted: membership is not checked. An address with a live pending invitation is not: the partial unique index on pending invitations makes that a `422` with `already has a pending invitation`. Only an expired pending invitation is replaced by a new one.

Requires scope members:write

Request body

Who to invite and as what, sent unwrapped

email string required
Email address to invite.
role string required
Role to grant on acceptance.

Responses

201 application/json
The created invitation
400 application/json
The `email` or `role` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed, e.g. a malformed email address or a role that is not assignable to an invitee
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/invitations \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "new.engineer@example.com",
  "role": "member"
}'

Response 201

{
  "data": {
    "email": "string",
    "expires_at": "2026-01-15T19:44:25Z",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "invited_by_email": "string",
    "role": "string"
  }
}

Revoke an invitation

DELETE /api/v1/invitations/{id}

Deletes a pending invitation so its link can no longer be redeemed. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope members:write

Parameters

id string in path required
Invitation ID.

Responses

200 application/json
The invitation was revoked
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No invitation with that ID in this organization
422 application/json
The invitation could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/invitations/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Integrations

Vendor integrations and their credentials.

List integrations

GET /api/v1/integrations

Lists every third-party integration configured for the authenticated organization. Not paginated: the full list is returned under `data` and no `meta` is present. Vendor credentials are write-only and are never read back out, so entries carry only the provider key, whether the integration is enabled, its sync interval and the outcome of the last sync.

Requires scope integrations:read

Responses

200 application/json
Integrations
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/integrations \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "enabled": true,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "last_sync_message": "string",
      "last_sync_status": "string",
      "last_synced_at": "2026-01-15T19:44:25Z",
      "provider": "string",
      "sync_interval_minutes": 0,
      "updated_at": "2026-01-15T19:44:25Z"
    }
  ]
}

Create an integration

POST /api/v1/integrations

Configures a vendor integration for the authenticated organization. Omitting `sync_interval_minutes` uses the provider default: 30 minutes for NetBox, 15 for Gaiia and 10 for other providers. Updates preserve an existing interval unless the caller supplies a replacement. Quo with `enabled: true` provisions its listener before reporting success. Provisioning failure returns 422 but retains a disabled integration; retrieve it and update it to retry. Credentials supplied in the body are write-only: they are protected by database-level access control (not application-level encryption) and are never echoed back by this or any other endpoint. Attribute keys outside the supported allowlist are dropped silently rather than rejected.

Requires scope integrations:write

Request body

Integration attributes

integration object required
provider string required
Vendor key. `gaiia` and `splynx` are full billing adapters (outage impact, plan speeds and the subscriber trace); `sonar` and `visp` are totals only: subscriber count, MRR and the customer list.
api_key string
Vendor API key. Never read back out.
api_secret string
Vendor API secret, for key+secret providers like Splynx.
api_token string
Vendor API token, for token providers like NetBox and Sonar.
api_url string
Vendor API base URL, for self-hosted or regional instances. Stored on whichever URL field the provider reads.
client_id string
OAuth client ID, for client-credentials providers like cnMaestro.
client_secret string
OAuth client secret, for client-credentials providers like cnMaestro.
config object
Provider-specific settings. Write-only.
enabled boolean
Whether the integration syncs.
instance_url string
Vendor instance base URL. Equivalent to `api_url` for providers that store it as `instance_url`.
sync_interval_minutes integer
Minutes between syncs.
webhook_token string
Shared secret a MikroTik router presents in `X-TowerOps-Token`.

Responses

201 application/json
The created integration
400 application/json
The `integration` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/integrations \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "integration": {
    "api_key": "string",
    "api_secret": "string",
    "api_token": "string",
    "api_url": "string",
    "client_id": "string",
    "client_secret": "string",
    "config": null,
    "enabled": true,
    "instance_url": "string",
    "provider": "string",
    "sync_interval_minutes": 0,
    "webhook_token": "string"
  }
}'

Response 201

{
  "data": {
    "enabled": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "last_sync_message": "string",
    "last_sync_status": "string",
    "last_synced_at": "2026-01-15T19:44:25Z",
    "provider": "string",
    "sync_interval_minutes": 0,
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Retrieve an integration

GET /api/v1/integrations/{id}

Gets one integration by ID. Credentials are write-only and are not included.

Requires scope integrations:read

Parameters

id string in path required
Integration ID.

Responses

200 application/json
The integration
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No integration with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/integrations/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "enabled": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "last_sync_message": "string",
    "last_sync_status": "string",
    "last_synced_at": "2026-01-15T19:44:25Z",
    "provider": "string",
    "sync_interval_minutes": 0,
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Update an integration

PATCH PUT /api/v1/integrations/{id}

Updates the given attributes of an integration. Omitted attributes are left alone, and keys outside the supported allowlist are dropped silently. Credentials can be rotated by sending them again; they remain write-only and are not returned in the response. Enabling Quo provisions its listener first. Replacing its API key removes the previous workspace's subscription with the old key before saving the replacement. Failure returns 422 and leaves capture disabled; failed previous-workspace cleanup retains the old credentials for retry. Disabling rejects deliveries locally even if remote deletion fails.

Requires scope integrations:write

Parameters

id string in path required
Integration ID.

Request body

Integration attributes

integration object required
provider string required
Vendor key. `gaiia` and `splynx` are full billing adapters (outage impact, plan speeds and the subscriber trace); `sonar` and `visp` are totals only: subscriber count, MRR and the customer list.
api_key string
Vendor API key. Never read back out.
api_secret string
Vendor API secret, for key+secret providers like Splynx.
api_token string
Vendor API token, for token providers like NetBox and Sonar.
api_url string
Vendor API base URL, for self-hosted or regional instances. Stored on whichever URL field the provider reads.
client_id string
OAuth client ID, for client-credentials providers like cnMaestro.
client_secret string
OAuth client secret, for client-credentials providers like cnMaestro.
config object
Provider-specific settings. Write-only.
enabled boolean
Whether the integration syncs.
instance_url string
Vendor instance base URL. Equivalent to `api_url` for providers that store it as `instance_url`.
sync_interval_minutes integer
Minutes between syncs.
webhook_token string
Shared secret a MikroTik router presents in `X-TowerOps-Token`.

Responses

200 application/json
The updated integration
400 application/json
The `integration` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No integration with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/integrations/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "integration": {
    "api_key": "string",
    "api_secret": "string",
    "api_token": "string",
    "api_url": "string",
    "client_id": "string",
    "client_secret": "string",
    "config": null,
    "enabled": true,
    "instance_url": "string",
    "provider": "string",
    "sync_interval_minutes": 0,
    "webhook_token": "string"
  }
}'

Response 200

{
  "data": {
    "enabled": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "last_sync_message": "string",
    "last_sync_status": "string",
    "last_synced_at": "2026-01-15T19:44:25Z",
    "provider": "string",
    "sync_interval_minutes": 0,
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Delete an integration

DELETE /api/v1/integrations/{id}

Removes an integration and its stored credentials. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope integrations:write

Parameters

id string in path required
Integration ID.

Responses

200 application/json
The integration was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No integration with that ID in this organization
422 application/json
The integration could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/integrations/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Test an integration's connection

POST /api/v1/integrations/{id}/test

Runs a live credential check against the vendor with the integration's stored credentials and reports whether the vendor accepted them. On success the payload carries `status` (`"ok"`) and `provider`. Any failure rejected credentials, a provider that does not support connection testing, or an unexpected upstream status, comes back as `502 Bad Gateway` with a deliberately coarse message: upstream response bodies and credentials are kept server-side rather than relayed to the caller.

Requires scope integrations:write

Parameters

id string in path required
Integration ID.

Responses

200 application/json
The vendor accepted the credentials
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No integration with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
502 application/json
The vendor rejected the credentials or could not be reached

Request

curl -X POST https://staging.towerops.net/api/v1/integrations/$ID/test \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "provider": "preseem",
    "status": "ok"
  }
}

Coverages

RF coverage predictions.

List coverages

GET /api/v1/coverages

Lists every RF coverage prediction in the authenticated organization. Not paginated: the whole list comes back under `data` and no `meta` is present. Every entry carries `status` and `progress_pct`, so Terraform and other IaC consumers can poll until a coverage reaches `ready` before treating it as healthy.

Requires scope coverages:read

Responses

200 application/json
Coverages
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/coverages \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "antenna_slug": "rf-elements-tp-sh-30",
      "frequency_mhz": 5.8e3,
      "id": "cc0e8400-e29b-41d4-a716-446655440006",
      "name": "North sector 5 GHz",
      "progress_pct": 100,
      "status": "ready",
      "tx_power_dbm": 22.0
    }
  ]
}

Create a coverage

POST /api/v1/coverages

Creates a coverage and immediately enqueues the compute pipeline, so the response is returned long before the prediction finishes. Poll `GET /api/v1/coverages/:id` on `status` until it becomes `ready`, the resource is designed for Terraform/IaC read-after-write reconciliation. The response is `201` even when compute could not be queued: the row is exposed anyway so a provider's read-after-write still reconciles, with `status` reflecting that nothing is running. Note the unit asymmetry on this frozen wire format: the request takes `frequency_ghz`, `radius_mi` and `height_agl_ft`, while the resource is returned as `frequency_mhz`, `radius_m` and `height_agl_m`.

Requires scope coverages:write

Request body

Coverage attributes

coverage object required
antenna_slug string required
Antenna pattern key.
azimuth_deg number required
Boresight azimuth in degrees true.
name string required
Coverage name.
site_id uuid required
Site the transmitter sits at.
tx_power_dbm number required
Transmit power in dBm.
cable_loss_db number
Feeder loss in dB.
device_id uuid
Device the transmitter is.
downtilt_deg number
Mechanical downtilt in degrees.
foliage_tuning number
Tuning factor for tree-canopy loss.
frequency_ghz number
Centre frequency in GHz.
height_agl_ft number
Antenna height above ground level, in feet.
latitude_override number
Transmitter latitude override.
longitude_override number
Transmitter longitude override.
radius_mi number
Prediction radius in miles.
rx_threshold_dbm number
Edge-of-coverage signal level.
sm_gain_dbi number
Subscriber-module antenna gain in dBi.

Responses

201 application/json
The created coverage, with compute queued
400 application/json
The `coverage` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/coverages \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "coverage": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "downtilt_deg": 0.0,
    "foliage_tuning": 0.0,
    "frequency_ghz": 0.0,
    "height_agl_ft": 0.0,
    "latitude_override": 0.0,
    "longitude_override": 0.0,
    "name": "string",
    "radius_mi": 0.0,
    "rx_threshold_dbm": 0.0,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "sm_gain_dbi": 0.0,
    "tx_power_dbm": 0.0
  }
}'

Response 201

{
  "data": {
    "antenna_slug": "rf-elements-tp-sh-30",
    "frequency_mhz": 5.8e3,
    "id": "cc0e8400-e29b-41d4-a716-446655440006",
    "name": "North sector 5 GHz",
    "progress_pct": 100,
    "status": "ready",
    "tx_power_dbm": 22.0
  }
}

Retrieve a coverage

GET /api/v1/coverages/{id}

Gets one coverage by ID, including `status`, `progress_pct`, `error_message`, the computed bounding box and the generated raster paths. This is the endpoint Terraform/IaC consumers poll on `status` until it reads `ready`.

Requires scope coverages:read

Parameters

id string in path required
Coverage ID.

Responses

200 application/json
The coverage
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No coverage with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/coverages/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "antenna_slug": "rf-elements-tp-sh-30",
    "frequency_mhz": 5.8e3,
    "id": "cc0e8400-e29b-41d4-a716-446655440006",
    "name": "North sector 5 GHz",
    "progress_pct": 100,
    "status": "ready",
    "tx_power_dbm": 22.0
  }
}

Update a coverage

PATCH PUT /api/v1/coverages/{id}

Updates editable fields on a coverage. Omitted attributes are left alone. This does **not** recompute: the previously computed outputs stay as they are until you `POST /api/v1/coverages/:id/recompute`. As on create, input is given as `frequency_ghz`, `radius_mi` and `height_agl_ft` while the resource is returned in MHz and metres.

Requires scope coverages:write

Parameters

id string in path required
Coverage ID.

Request body

Coverage attributes

coverage object required
antenna_slug string required
Antenna pattern key.
azimuth_deg number required
Boresight azimuth in degrees true.
name string required
Coverage name.
site_id uuid required
Site the transmitter sits at.
tx_power_dbm number required
Transmit power in dBm.
cable_loss_db number
Feeder loss in dB.
device_id uuid
Device the transmitter is.
downtilt_deg number
Mechanical downtilt in degrees.
foliage_tuning number
Tuning factor for tree-canopy loss.
frequency_ghz number
Centre frequency in GHz.
height_agl_ft number
Antenna height above ground level, in feet.
latitude_override number
Transmitter latitude override.
longitude_override number
Transmitter longitude override.
radius_mi number
Prediction radius in miles.
rx_threshold_dbm number
Edge-of-coverage signal level.
sm_gain_dbi number
Subscriber-module antenna gain in dBi.

Responses

200 application/json
The updated coverage
400 application/json
The `coverage` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No coverage with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/coverages/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "coverage": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "downtilt_deg": 0.0,
    "foliage_tuning": 0.0,
    "frequency_ghz": 0.0,
    "height_agl_ft": 0.0,
    "latitude_override": 0.0,
    "longitude_override": 0.0,
    "name": "string",
    "radius_mi": 0.0,
    "rx_threshold_dbm": 0.0,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "sm_gain_dbi": 0.0,
    "tx_power_dbm": 0.0
  }
}'

Response 200

{
  "data": {
    "antenna_slug": "rf-elements-tp-sh-30",
    "frequency_mhz": 5.8e3,
    "id": "cc0e8400-e29b-41d4-a716-446655440006",
    "name": "North sector 5 GHz",
    "progress_pct": 100,
    "status": "ready",
    "tx_power_dbm": 22.0
  }
}

Delete a coverage

DELETE /api/v1/coverages/{id}

Deletes a coverage and its computed artifacts. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope coverages:write

Parameters

id string in path required
Coverage ID.

Responses

200 application/json
The coverage was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No coverage with that ID in this organization
422 application/json
The coverage could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/coverages/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Download a coverage as KMZ

GET /api/v1/coverages/{id}/kmz

Returns the computed heatmap as a KMZ bundle (a ZIP holding `doc.kml` plus the `overlay.png` ground overlay) ready to open in Google Earth or any compatible viewer. The body is binary, not JSON, and is served as an attachment via `Content-Disposition: attachment; filename="coverage-<id>.kmz"`. Only a coverage whose `status` is `ready` and which has a rendered raster can be exported; anything else is rejected with `409 Conflict`, so poll `GET /api/v1/coverages/:id` until `ready` before downloading. Failures while assembling the bundle come back as `500` with a generic message, since the underlying filesystem and archive details stay server-side.

Requires scope coverages:read

Parameters

id string in path required
Coverage ID.

Responses

200 application/vnd.google-earth.kmz
KMZ bundle
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No coverage with that ID in this organization
409 application/json
The coverage has not finished computing
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The KMZ bundle could not be built

Request

curl https://staging.towerops.net/api/v1/coverages/$ID/kmz \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

"string"

Recompute a coverage

POST /api/v1/coverages/{id}/recompute

Re-runs the compute pipeline for an existing coverage without changing its parameters, useful from Terraform after an apply when you want a fresh heatmap against current terrain and clutter data. Accepted asynchronously: the returned coverage has `status` set to the queued state, and consumers poll `GET /api/v1/coverages/:id` until it reaches `ready`. A coverage that is already computing is rejected with `409 Conflict` rather than queued twice.

Requires scope coverages:write

Parameters

id string in path required
Coverage ID.

Responses

202 application/json
Compute was queued
400 application/json
Compute could not be queued
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No coverage with that ID in this organization
409 application/json
A compute is already running for this coverage
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/coverages/$ID/recompute \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 202

{
  "data": {
    "antenna_slug": "rf-elements-tp-sh-30",
    "frequency_mhz": 5.8e3,
    "id": "cc0e8400-e29b-41d4-a716-446655440006",
    "name": "North sector 5 GHz",
    "progress_pct": 100,
    "status": "ready",
    "tx_power_dbm": 22.0
  }
}

Activity

The unified organization activity feed.

List organization activity

GET /api/v1/activity

Returns the unified activity feed for the authenticated organization, newest first, merging config changes, alert transitions, device events, integration syncs and device creation into one stream. Not paginated: the feed is capped by `limit` and no `meta` is present.

Requires scope activity:read

Parameters

limit integer in query
Maximum number of entries to return. Defaults to 50, and is capped at 500 regardless of the requested value. A value that is not an integer falls back to the default.
types string in query
Comma-separated list of entry types to include. Allowed values are `config_change`, `alert_fired`, `alert_resolved`, `device_event`, `sync`, `device_added` and `discovery_candidate`; surrounding whitespace is trimmed and unrecognised values are dropped silently rather than rejected. Omit the parameter to include every type.

Responses

200 application/json
Activity entries
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/activity \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "limit=50" \
  --data-urlencode "types=alert_fired,alert_resolved"

Response 200

{
  "data": [
    {
      "detail": "string",
      "device_name": "string",
      "icon": "string",
      "id": "string",
      "link": "string",
      "severity": "info",
      "site_name": "string",
      "summary": "string",
      "timestamp": "2026-01-15T19:44:25Z",
      "type": "config_change"
    }
  ]
}

Customers

The billing-mirrored customer registry and the subscriber trace.

List customers

GET /api/v1/customers

One page of the organization's billing-mirrored customers (Gaiia, Splynx, Sonar, VISP), ordered by name. `q` matches name or account reference case-insensitively, and a phone number in any format matches the customer's normalized phone exactly. Walk the collection with `page` and `per_page`, or by sending back `meta.next_cursor`.

Requires scope subscribers:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`; carries page and page size, taking precedence over `page`/`per_page`.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.
provider string in query
Billing provider: gaiia, splynx, sonar or visp.
q string in query
Name, account reference or phone number.
status string in query
The billing system's own status (for example `active`), matched ignoring case and surrounding whitespace.

Responses

200 application/json
Customers
400 application/json
The `cursor` parameter did not decode
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/customers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=VALUE" \
  --data-urlencode "per_page=VALUE" \
  --data-urlencode "provider=VALUE" \
  --data-urlencode "q=VALUE" \
  --data-urlencode "status=VALUE"

Response 200

{
  "data": [
    {
      "email": "string",
      "external_id": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "mrr": "string",
      "name": "string",
      "phone": "string",
      "provider": "gaiia",
      "reference": "string",
      "status": "string",
      "synced_at": "2026-01-15T19:44:25Z"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Retrieve a customer

GET /api/v1/customers/{id}

One customer by its stable TowerOps id. A customer the billing system withdrew is not found.

Requires scope subscribers:read

Parameters

id string in path required
Customer ID.

Responses

200 application/json
The customer
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No customer with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/customers/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "email": "string",
    "external_id": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "mrr": "string",
    "name": "string",
    "phone": "string",
    "provider": "gaiia",
    "reference": "string",
    "status": "string",
    "synced_at": "2026-01-15T19:44:25Z"
  }
}

Trace a customer

GET /api/v1/customers/{id}/trace

The subscriber trace the `/trace` page renders for this customer: the serving device and access point, the subscriber's wireless stations, the router connection (PPPoE session or DHCP assignment), QoE, open alerts and maintenance windows. By default this is the stored snapshot and the request touches no equipment; `connectivity` is `null`. With `probe=true` the request also runs the live connectivity checks through the organization's agents (router session, radio registration, Preseem, ping), which can take several seconds, and returns the evaluated checklist in `connectivity`. A customer whose billing provider has no subscriber trace (Sonar, VISP, or a provider record not yet mirrored) answers 404.

Requires scope subscribers:read

Parameters

id string in path required
Customer ID.
probe boolean in query
Run the live router connectivity checks. Defaults to false.

Responses

200 application/json
The trace
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No customer with that ID in this organization, or no trace for it
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/customers/$ID/trace \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "probe=VALUE"

Response 200

{
  "data": {
    "access_point": null,
    "active_alert_count": 0,
    "active_alerts": [],
    "co_tenants": [],
    "connectivity": null,
    "customer_id": "550e8400-e29b-41d4-a716-446655440000",
    "device": null,
    "maintenance_windows": [],
    "qoe": null,
    "router_connection": null,
    "sector_verdict": "overcrowding",
    "stations": [],
    "subscriber": null
  }
}

Maintenance Windows

Planned windows that suppress alerting.

List maintenance windows

GET /api/v1/maintenance_windows

Lists maintenance windows for the authenticated organization, newest first, one page at a time. Pagination works like the other collection endpoints: `page`/`per_page` (per page defaults to 100 and is capped at 100), or an opaque `cursor` from `meta.next_cursor`, which takes precedence. `meta` carries the page, `total_count` and the next cursor. A cursor that does not decode is rejected with `bad_request`. A `page` past the last one returns an empty `data`, so paging until the list is empty terminates.

Requires scope config:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`. Carries both the page and the page size, so it takes precedence over `page` and `per_page`. A cursor that does not decode is rejected with `bad_request`.
filter string in query
Restrict the list by window timing: `active`, `upcoming` or `past`. Any other value (or an omitted parameter) returns every window.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.

Responses

200 application/json
Maintenance windows
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/maintenance_windows \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "filter=active" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=50"

Response 200

{
  "data": [
    {
      "announce_on_status_page": true,
      "device_id": "550e8400-e29b-41d4-a716-446655440000",
      "ends_at": "2026-01-15T19:44:25Z",
      "exclude_from_uptime": true,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "name": "string",
      "public_title": "string",
      "reason": "string",
      "recurrence_rule": "string",
      "recurring": true,
      "site_id": "550e8400-e29b-41d4-a716-446655440000",
      "starts_at": "2026-01-15T19:44:25Z",
      "suppress_alerts": true
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Create a maintenance window

POST /api/v1/maintenance_windows

Creates a maintenance window in the authenticated organization. The window is attributed to the user the API token belongs to, and `organization_id` and the creator are set from the token rather than the body. A partner-app token, which has no user behind it, is rejected with `403`. Scope the window to a single site or device with `site_id`/`device_id`; leave both out for an organization-wide window.

Requires scope config:write

Request body

Maintenance window attributes

maintenance_window object required
ends_at date-time required
Window end.
name string required
Window name.
starts_at date-time required
Window start.
announce_on_status_page boolean
Publicly announce the window's public title and end time for finite windows on mapped status components. Defaults to false; set true to opt in. No mapped component means no public change.
device_id uuid
Limit the window to a device.
exclude_from_uptime boolean
Exclude downtime in the window from uptime figures.
public_title string
Customer-facing label used when the window is announced; the internal name is never published.
reason string
Why the work is happening.
recurrence_rule string
Recurrence rule.
recurring boolean
Whether the window repeats.
site_id uuid
Limit the window to a site.
suppress_alerts boolean
Suppress alerts during the window.

Responses

201 application/json
The created maintenance window
400 application/json
The `maintenance_window` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/maintenance_windows \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "maintenance_window": {
    "announce_on_status_page": true,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "ends_at": "2026-01-15T19:44:25Z",
    "exclude_from_uptime": true,
    "name": "string",
    "public_title": "string",
    "reason": "string",
    "recurrence_rule": "string",
    "recurring": true,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "starts_at": "2026-01-15T19:44:25Z",
    "suppress_alerts": true
  }
}'

Response 201

{
  "data": {
    "announce_on_status_page": true,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "ends_at": "2026-01-15T19:44:25Z",
    "exclude_from_uptime": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "public_title": "string",
    "reason": "string",
    "recurrence_rule": "string",
    "recurring": true,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "starts_at": "2026-01-15T19:44:25Z",
    "suppress_alerts": true
  }
}

Retrieve a maintenance window

GET /api/v1/maintenance_windows/{id}

Gets one maintenance window by ID.

Requires scope config:read

Parameters

id string in path required
Maintenance window ID.

Responses

200 application/json
The maintenance window
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No maintenance window with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/maintenance_windows/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "announce_on_status_page": true,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "ends_at": "2026-01-15T19:44:25Z",
    "exclude_from_uptime": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "public_title": "string",
    "reason": "string",
    "recurrence_rule": "string",
    "recurring": true,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "starts_at": "2026-01-15T19:44:25Z",
    "suppress_alerts": true
  }
}

Update a maintenance window

PATCH PUT /api/v1/maintenance_windows/{id}

Updates the given attributes of a maintenance window. Omitted attributes are left alone.

Requires scope config:write

Parameters

id string in path required
Maintenance window ID.

Request body

Maintenance window attributes

maintenance_window object required
ends_at date-time required
Window end.
name string required
Window name.
starts_at date-time required
Window start.
announce_on_status_page boolean
Publicly announce the window's public title and end time for finite windows on mapped status components. Defaults to false; set true to opt in. No mapped component means no public change.
device_id uuid
Limit the window to a device.
exclude_from_uptime boolean
Exclude downtime in the window from uptime figures.
public_title string
Customer-facing label used when the window is announced; the internal name is never published.
reason string
Why the work is happening.
recurrence_rule string
Recurrence rule.
recurring boolean
Whether the window repeats.
site_id uuid
Limit the window to a site.
suppress_alerts boolean
Suppress alerts during the window.

Responses

200 application/json
The updated maintenance window
400 application/json
The `maintenance_window` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No maintenance window with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/maintenance_windows/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "maintenance_window": {
    "announce_on_status_page": true,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "ends_at": "2026-01-15T19:44:25Z",
    "exclude_from_uptime": true,
    "name": "string",
    "public_title": "string",
    "reason": "string",
    "recurrence_rule": "string",
    "recurring": true,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "starts_at": "2026-01-15T19:44:25Z",
    "suppress_alerts": true
  }
}'

Response 200

{
  "data": {
    "announce_on_status_page": true,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "ends_at": "2026-01-15T19:44:25Z",
    "exclude_from_uptime": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "public_title": "string",
    "reason": "string",
    "recurrence_rule": "string",
    "recurring": true,
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "starts_at": "2026-01-15T19:44:25Z",
    "suppress_alerts": true
  }
}

Delete a maintenance window

DELETE /api/v1/maintenance_windows/{id}

Deletes a maintenance window. Alerts are no longer suppressed for its scope once it is gone.

Requires scope config:write

Parameters

id string in path required
Maintenance window ID.

Responses

200 application/json
The maintenance window was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No maintenance window with that ID in this organization
422 application/json
The maintenance window could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/maintenance_windows/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Schedules

On-call schedules, rotation layers and overrides.

List schedules

GET /api/v1/schedules

Lists every on-call schedule in the authenticated organization. This endpoint does not paginate and omits `meta`; each entry carries the schedule summary only, without its layers or overrides.

Requires scope config:read

Responses

200 application/json
Schedules
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/schedules \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "description": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "name": "string",
      "timezone": "string"
    }
  ]
}

Create a schedule

POST /api/v1/schedules

Creates an on-call schedule in the authenticated organization. The schedule is created empty: add rotation layers, layer members and overrides with the nested endpoints below.

Requires scope config:write

Request body

Schedule attributes

schedule object required
name string required
Schedule name.
timezone string required
IANA timezone, e.g. `America/New_York`.
description string
Free-form description.

Responses

201 application/json
The created schedule
400 application/json
The `schedule` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/schedules \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "schedule": {
    "description": "string",
    "name": "string",
    "timezone": "string"
  }
}'

Response 201

{
  "data": {
    "description": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "timezone": "string"
  }
}

Retrieve a schedule

GET /api/v1/schedules/{id}

Gets one schedule by ID. Unlike the list endpoint, this returns the full detail shape: every rotation layer with its ordered members (each member carrying the user it points at), plus recent and upcoming overrides with their users. Overrides that ended more than about two weeks ago are omitted, they can no longer affect the rendered schedule.

Requires scope config:read

Parameters

id string in path required
Schedule ID.

Responses

200 application/json
The schedule
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No schedule with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/schedules/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "description": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "layers": [
      {
        "handoff_day": 0,
        "handoff_time": "string",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "members": [],
        "name": "string",
        "position": 0,
        "rotation_interval": 0,
        "rotation_type": "daily",
        "start_date": "2026-01-15"
      }
    ],
    "name": "string",
    "overrides": [
      {
        "accepted_at": "2026-01-15T19:44:25Z",
        "end_time": "2026-01-15T19:44:25Z",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "pending": true,
        "reason": "string",
        "requested_by": {},
        "requested_by_id": "550e8400-e29b-41d4-a716-446655440000",
        "schedule_id": "550e8400-e29b-41d4-a716-446655440000",
        "start_time": "2026-01-15T19:44:25Z",
        "user": {
          "email": "noc@example.com",
          "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
        },
        "user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "timezone": "string"
  }
}

Update a schedule

PATCH PUT /api/v1/schedules/{id}

Updates the given attributes of a schedule. Omitted attributes are left alone. Layers, members and overrides are not touched here; use the nested endpoints. The response is the summary shape, without layers or overrides.

Requires scope config:write

Parameters

id string in path required
Schedule ID.

Request body

Schedule attributes

schedule object required
name string required
Schedule name.
timezone string required
IANA timezone, e.g. `America/New_York`.
description string
Free-form description.

Responses

200 application/json
The updated schedule
400 application/json
The `schedule` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No schedule with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/schedules/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "schedule": {
    "description": "string",
    "name": "string",
    "timezone": "string"
  }
}'

Response 200

{
  "data": {
    "description": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "timezone": "string"
  }
}

Delete a schedule

DELETE /api/v1/schedules/{id}

Deletes a schedule along with its layers, layer members and overrides.

Requires scope config:write

Parameters

id string in path required
Schedule ID.

Responses

200 application/json
The schedule was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No schedule with that ID in this organization
422 application/json
The schedule could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/schedules/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Add a rotation layer

POST /api/v1/schedules/{id}/layers

Adds a rotation layer to a schedule. Attributes are nested under a `layer` key. `schedule_id` is taken from the path and ignored in the body. Layers are evaluated in `position` order, so a higher-positioned layer takes precedence over the ones below it when both are active.

Requires scope config:write

Parameters

id string in path required
Schedule ID.

Request body

Layer attributes

layer object required
handoff_time string required
Handoff time, `HH:MM:SS`, in the schedule's timezone.
name string required
Layer name.
position integer required
Layer order within the schedule.
rotation_interval integer required
Interval in days for `custom` rotations.
rotation_type string required
Rotation cadence.
start_date date required
First day the layer is in effect.
handoff_day integer
Weekday 0 (Sunday)-6 (Saturday) in the schedule's timezone, required for `weekly` rotations.

Responses

201 application/json
The created layer
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No schedule with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/schedules/$ID/layers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "layer": {
    "handoff_day": 0,
    "handoff_time": "string",
    "name": "string",
    "position": 0,
    "rotation_interval": 0,
    "rotation_type": "daily",
    "start_date": "2026-01-15"
  }
}'

Response 201

{
  "data": {
    "handoff_day": 0,
    "handoff_time": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "members": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "layer_id": "550e8400-e29b-41d4-a716-446655440000",
        "position": 0,
        "user": {
          "email": "noc@example.com",
          "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
        },
        "user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "name": "string",
    "position": 0,
    "rotation_interval": 0,
    "rotation_type": "daily",
    "start_date": "2026-01-15"
  }
}

Update a rotation layer

PATCH /api/v1/schedules/{id}/layers/{layer_id}

Updates the given attributes of a layer. Attributes are nested under a `layer` key; omitted attributes are left alone. The layer must belong to the schedule named in the path, otherwise the request answers 404.

Requires scope config:write

Parameters

id string in path required
Schedule ID.
layer_id string in path required
Rotation layer ID within the schedule.

Request body

Layer attributes

layer object required
handoff_time string required
Handoff time, `HH:MM:SS`, in the schedule's timezone.
name string required
Layer name.
position integer required
Layer order within the schedule.
rotation_interval integer required
Interval in days for `custom` rotations.
rotation_type string required
Rotation cadence.
start_date date required
First day the layer is in effect.
handoff_day integer
Weekday 0 (Sunday)-6 (Saturday) in the schedule's timezone, required for `weekly` rotations.

Responses

200 application/json
The updated layer
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The schedule or the layer does not exist here
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/schedules/$ID/layers/$LAYER_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "layer": {
    "handoff_day": 0,
    "handoff_time": "string",
    "name": "string",
    "position": 0,
    "rotation_interval": 0,
    "rotation_type": "daily",
    "start_date": "2026-01-15"
  }
}'

Response 200

{
  "data": {
    "handoff_day": 0,
    "handoff_time": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "members": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "layer_id": "550e8400-e29b-41d4-a716-446655440000",
        "position": 0,
        "user": {
          "email": "noc@example.com",
          "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
        },
        "user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "name": "string",
    "position": 0,
    "rotation_interval": 0,
    "rotation_type": "daily",
    "start_date": "2026-01-15"
  }
}

Delete a rotation layer

DELETE /api/v1/schedules/{id}/layers/{layer_id}

Removes a layer from a schedule along with its members. The layer must belong to the schedule named in the path, otherwise the request answers 404. Success is 200 with `{"data": {"deleted": true}}`, not 204.

Requires scope config:write

Parameters

id string in path required
Schedule ID.
layer_id string in path required
Rotation layer ID within the schedule.

Responses

200 application/json
The layer was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The schedule or the layer does not exist here
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/schedules/$ID/layers/$LAYER_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Add a member to a rotation layer

POST /api/v1/schedules/{id}/layers/{layer_id}/members

Adds a user to a layer's rotation. Attributes are nested under a `member` key; `layer_id` is taken from the path and ignored in the body. Members take their turn in `position` order.

Requires scope config:write

Parameters

id string in path required
Schedule ID.
layer_id string in path required
Rotation layer ID within the schedule.

Request body

Member attributes

member object required
position integer required
Order within the rotation.
user_id uuid required
User to add to the rotation.

Responses

201 application/json
The created member
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The schedule or the layer does not exist here
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/schedules/$ID/layers/$LAYER_ID/members \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "member": {
    "position": 0,
    "user_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}'

Response 201

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "layer_id": "550e8400-e29b-41d4-a716-446655440000",
    "position": 0,
    "user": {
      "email": "noc@example.com",
      "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
    },
    "user_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Remove a member from a rotation layer

DELETE /api/v1/schedules/{id}/layers/{layer_id}/members/{member_id}

Removes a user from a layer's rotation. The whole chain is checked: the layer must belong to the schedule and the member to the layer, otherwise the request answers 404. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope config:write

Parameters

id string in path required
Schedule ID.
layer_id string in path required
Rotation layer ID within the schedule.
member_id string in path required
Layer member ID within the layer.

Responses

200 application/json
The member was removed
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The schedule, layer or member does not exist here
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/schedules/$ID/layers/$LAYER_ID/members/$MEMBER_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Resolve who is on call

GET /api/v1/schedules/{id}/on_call

Resolves the single user currently on call for the schedule, applying overrides over the rotation layers. When nobody is on call, no layers, no members in the active rotation, or the schedule has not started yet, the request still succeeds with `{"data": null}` rather than a 404. A 404 here means the schedule itself does not exist in this organization.

Requires scope config:read

Parameters

id string in path required
Schedule ID.

Responses

200 application/json
The user on call, or null when nobody is
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No schedule with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/schedules/$ID/on_call \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "email": "noc@example.com",
    "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
  }
}

Create a schedule override

POST /api/v1/schedules/{id}/overrides

Puts one user on call for a fixed window, overriding whatever the rotation layers would otherwise resolve to. Attributes are nested under an `override` key; `schedule_id` is taken from the path and ignored in the body.

Requires scope config:write

Parameters

id string in path required
Schedule ID.

Request body

Override attributes

override object required
end_time date-time required
When the override ends.
start_time date-time required
When the override starts.
user_id uuid required
User who takes the shift.

Responses

201 application/json
The created override
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No schedule with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/schedules/$ID/overrides \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "override": {
    "end_time": "2026-01-15T19:44:25Z",
    "start_time": "2026-01-15T19:44:25Z",
    "user_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}'

Response 201

{
  "data": {
    "accepted_at": "2026-01-15T19:44:25Z",
    "end_time": "2026-01-15T19:44:25Z",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "pending": true,
    "reason": "string",
    "requested_by": {
      "email": "noc@example.com",
      "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
    },
    "requested_by_id": "550e8400-e29b-41d4-a716-446655440000",
    "schedule_id": "550e8400-e29b-41d4-a716-446655440000",
    "start_time": "2026-01-15T19:44:25Z",
    "user": {
      "email": "noc@example.com",
      "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
    },
    "user_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Delete a schedule override

DELETE /api/v1/schedules/{id}/overrides/{override_id}

Cancels an override; the rotation layers resolve the window again. The override must belong to the schedule named in the path, otherwise the request answers 404. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope config:write

Parameters

id string in path required
Schedule ID.
override_id string in path required
Override ID within the schedule.

Responses

200 application/json
The override was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The schedule or the override does not exist here
422 application/json
The override could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/schedules/$ID/overrides/$OVERRIDE_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Escalation Policies

Escalation steps and who they notify.

For separate recipients by alert class, create type-filtered policies with their own rules and targets: for example, use alert_types ["agent_offline"] for infrastructure responders and alert_types ["check_http"] with check_types ["http"] for application responders. Lower routing_order values are tried first, then creation time and ID, and all configured scope dimensions must match. Explicit device assignment takes precedence; otherwise the default policy is the fallback. Unfiltered non-default policies remain assignment-only. On update, omit a field to retain it or send [] to clear a list constraint. Alerts without a device bypass only missing site/role context, not severity or alert/check type filters.

List escalation policies

GET /api/v1/escalation_policies

Lists every escalation policy in the authenticated organization. This endpoint does not paginate and omits `meta`; each entry carries the policy summary only, without its rules or their targets.

Requires scope config:read

Responses

200 application/json
Escalation policies
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/escalation_policies \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "alert_types": [
        "string"
      ],
      "check_types": [
        "string"
      ],
      "description": "string",
      "device_roles": [
        "string"
      ],
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "minimum_severity": 1,
      "name": "string",
      "repeat_count": 0,
      "routing_order": 0,
      "site_ids": [
        "550e8400-e29b-41d4-a716-446655440000"
      ]
    }
  ]
}

Create an escalation policy

POST /api/v1/escalation_policies

Creates an escalation policy in the authenticated organization. The policy is created without rules: add them with the nested rule endpoints, then attach targets to each rule. `repeat_count` is the paging ceiling in hours: the chain loops its rules until `repeat_count` hours after the alert triggered, then stops paging and the alert shows "paging stopped". `minimum_severity`, `site_ids`, `device_roles`, `alert_types` and `check_types` scope which alerts the policy pages for. Severity is a floor (`1` warning, `2` critical; omit or `null` for any severity); sites must belong to this organization. Device roles include `router`, `switch`, `access_point`, `backhaul`, `server` and `other`. Alert types use persisted identifiers such as `agent_offline` or `check_http`; check types use the suffix of a `check_` alert type, such as `http`. All non-empty dimensions must match; values within a list are alternatives. Empty lists are unconstrained. Missing device/site context fails open only for site/role constraints: severity and type filters still apply. Scope identifiers are validated on write: every `alert_types` value must be an emitted alert class or a stored value already on the policy, and every `check_types` value a registered check type, anything else fails with `422` naming the offending values. To scope on a class the product does not emit (for example an SNMP trap rule's alert type), declare it in `custom_alert_types`, which merges into `alert_types` on save. There is no `custom_check_types`: every check alert carries a registry check type, so `check_types` is registry-only. An explicitly assigned device policy takes precedence, even when its scope refuses the alert. Otherwise, non-default policies with alert/check type filters are tried in ascending `routing_order` (nonnegative, default `0`), then creation time and ID; the first full-scope match wins. Existing non-default policies without type filters remain assignment-only. The organization default is the fallback, and its scope is still enforced.

Requires scope config:write

Request body

Policy attributes

escalation_policy object required
name string required
Policy name.
alert_types array of string
Restrict paging to persisted alert types (e.g. `agent_offline`, `check_http`). An empty list clears the constraint; omission on update preserves it.
check_types array of string
Restrict paging to the `check_` alert-type suffix (e.g. `http`). An empty list clears the constraint; omission on update preserves it. All non-empty dimensions must match.
custom_alert_types array of string
Alert class identifiers the product does not emit (e.g. an SNMP trap rule's class). Declared here they are admitted into `alert_types` on save; a non-catalog value in `alert_types` itself is rejected. `check_types` has no equivalent: every check alert carries a registry type, so `check_types` accepts registry entries only.
description string
Free-form description.
device_roles array of string
Restrict paging to these device roles (e.g. `router`, `access_point`). An empty list clears the constraint.
handoff_notifications_mode string
When handoff notifications are sent.
minimum_severity integer
Lowest alert severity this policy pages for (`1` = warning, `2` = critical). Omit on create or send `null` for every severity; omission on update preserves the floor.
repeat_count integer
How many hours the policy keeps paging an unacknowledged alert before the chain stops (paging ceiling; default 24).
routing_order integer
Nonnegative automatic routing priority (default 0 on create). Lower values win, then creation time and ID. Omission on update preserves it.
site_ids array of uuid
Restrict paging to alerts at these sites. Every id must belong to the caller's organization. An empty list clears the constraint.

Responses

201 application/json
The created policy
400 application/json
The `escalation_policy` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/escalation_policies \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "escalation_policy": {
    "alert_types": [
      "string"
    ],
    "check_types": [
      "string"
    ],
    "custom_alert_types": [
      "string"
    ],
    "description": "string",
    "device_roles": [
      "string"
    ],
    "handoff_notifications_mode": "when_in_use_by_service",
    "minimum_severity": 1,
    "name": "string",
    "repeat_count": 0,
    "routing_order": 0,
    "site_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ]
  }
}'

Response 201

{
  "data": {
    "alert_types": [
      "string"
    ],
    "check_types": [
      "string"
    ],
    "description": "string",
    "device_roles": [
      "string"
    ],
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "minimum_severity": 1,
    "name": "string",
    "repeat_count": 0,
    "routing_order": 0,
    "site_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ]
  }
}

Retrieve an escalation policy

GET /api/v1/escalation_policies/{id}

Gets one policy by ID. Unlike the list endpoint, this returns the full detail shape: every rule in `position` order with its notification targets nested underneath. Rules escalate in `position` order: a rule's targets are notified, and if the alert is still unacknowledged after that rule's `timeout_minutes` the next position is notified.

Requires scope config:read

Parameters

id string in path required
Escalation policy ID.

Responses

200 application/json
The policy
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No policy with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/escalation_policies/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "alert_types": [
      "string"
    ],
    "check_types": [
      "string"
    ],
    "description": "string",
    "device_roles": [
      "string"
    ],
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "minimum_severity": 1,
    "name": "string",
    "repeat_count": 0,
    "routing_order": 0,
    "rules": [
      {
        "escalation_policy_id": "550e8400-e29b-41d4-a716-446655440000",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "position": 0,
        "targets": [],
        "timeout_minutes": 0
      }
    ],
    "site_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ]
  }
}

Update an escalation policy

PATCH PUT /api/v1/escalation_policies/{id}

Updates the given attributes of a policy. Omitted attributes are left alone. Rules and targets are not touched here; use the nested endpoints. The response is the summary shape, without rules. Scope and routing work as on create. Sending `[]` for `site_ids`, `device_roles`, `alert_types` or `check_types` clears that constraint; omitting it preserves its stored value. An explicit `minimum_severity: null` lowers the floor back to any severity. `routing_order: 0` restores the default order; omission preserves the stored order.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.

Request body

Policy attributes

escalation_policy object required
name string required
Policy name.
alert_types array of string
Restrict paging to persisted alert types (e.g. `agent_offline`, `check_http`). An empty list clears the constraint; omission on update preserves it.
check_types array of string
Restrict paging to the `check_` alert-type suffix (e.g. `http`). An empty list clears the constraint; omission on update preserves it. All non-empty dimensions must match.
custom_alert_types array of string
Alert class identifiers the product does not emit (e.g. an SNMP trap rule's class). Declared here they are admitted into `alert_types` on save; a non-catalog value in `alert_types` itself is rejected. `check_types` has no equivalent: every check alert carries a registry type, so `check_types` accepts registry entries only.
description string
Free-form description.
device_roles array of string
Restrict paging to these device roles (e.g. `router`, `access_point`). An empty list clears the constraint.
handoff_notifications_mode string
When handoff notifications are sent.
minimum_severity integer
Lowest alert severity this policy pages for (`1` = warning, `2` = critical). Omit on create or send `null` for every severity; omission on update preserves the floor.
repeat_count integer
How many hours the policy keeps paging an unacknowledged alert before the chain stops (paging ceiling; default 24).
routing_order integer
Nonnegative automatic routing priority (default 0 on create). Lower values win, then creation time and ID. Omission on update preserves it.
site_ids array of uuid
Restrict paging to alerts at these sites. Every id must belong to the caller's organization. An empty list clears the constraint.

Responses

200 application/json
The updated policy
400 application/json
The `escalation_policy` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No policy with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/escalation_policies/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "escalation_policy": {
    "alert_types": [
      "string"
    ],
    "check_types": [
      "string"
    ],
    "custom_alert_types": [
      "string"
    ],
    "description": "string",
    "device_roles": [
      "string"
    ],
    "handoff_notifications_mode": "when_in_use_by_service",
    "minimum_severity": 1,
    "name": "string",
    "repeat_count": 0,
    "routing_order": 0,
    "site_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ]
  }
}'

Response 200

{
  "data": {
    "alert_types": [
      "string"
    ],
    "check_types": [
      "string"
    ],
    "description": "string",
    "device_roles": [
      "string"
    ],
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "minimum_severity": 1,
    "name": "string",
    "repeat_count": 0,
    "routing_order": 0,
    "site_ids": [
      "550e8400-e29b-41d4-a716-446655440000"
    ]
  }
}

Delete an escalation policy

DELETE /api/v1/escalation_policies/{id}

Deletes a policy along with its rules and their targets.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.

Responses

200 application/json
The policy was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No policy with that ID in this organization
422 application/json
The policy could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/escalation_policies/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Add an escalation rule

POST /api/v1/escalation_policies/{id}/rules

Adds a rule (one escalation step) to a policy. Attributes are nested under a `rule` key; `escalation_policy_id` is taken from the path and ignored in the body. Rules fire in `position` order: the next position is notified once `timeout_minutes` elapses without the alert being acknowledged. A rule notifies nobody until targets are attached to it. Omit `position` to append the rule as the policy's last step. Supplying one places the rule at that step; a step another rule already uses answers 422, because steps are never renumbered (an in-flight escalation tracks the position it is on), move a rule with the update endpoint instead.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.

Request body

Rule attributes

rule object required
timeout_minutes integer required
Minutes to wait before escalating.
position integer
Step order within the policy. Omit to append the rule as the last step. A position another step already uses is rejected: steps are never renumbered, because in-flight escalations track them as literal positions, use the update endpoint to move a rule.

Responses

201 application/json
The created rule
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No policy with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/escalation_policies/$ID/rules \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "rule": {
    "position": 0,
    "timeout_minutes": 0
  }
}'

Response 201

{
  "data": {
    "escalation_policy_id": "550e8400-e29b-41d4-a716-446655440000",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "position": 0,
    "targets": [
      {
        "escalation_rule_id": "550e8400-e29b-41d4-a716-446655440000",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "role": "string",
        "schedule_id": "550e8400-e29b-41d4-a716-446655440000",
        "target_type": "schedule",
        "user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "timeout_minutes": 0
  }
}

Update an escalation rule

PATCH /api/v1/escalation_policies/{id}/rules/{rule_id}

Updates a rule's `position` or `timeout_minutes`. Attributes are nested under a `rule` key; omitted attributes are left alone. The rule must belong to the policy named in the path, otherwise the request answers 404.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.
rule_id string in path required
Escalation rule ID within the policy.

Request body

Rule attributes

rule object required
timeout_minutes integer required
Minutes to wait before escalating.
position integer
Step order within the policy. Omit to append the rule as the last step. A position another step already uses is rejected: steps are never renumbered, because in-flight escalations track them as literal positions, use the update endpoint to move a rule.

Responses

200 application/json
The updated rule
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The policy or the rule does not exist here
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/escalation_policies/$ID/rules/$RULE_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "rule": {
    "position": 0,
    "timeout_minutes": 0
  }
}'

Response 200

{
  "data": {
    "escalation_policy_id": "550e8400-e29b-41d4-a716-446655440000",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "position": 0,
    "targets": [
      {
        "escalation_rule_id": "550e8400-e29b-41d4-a716-446655440000",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z",
        "role": "string",
        "schedule_id": "550e8400-e29b-41d4-a716-446655440000",
        "target_type": "schedule",
        "user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "timeout_minutes": 0
  }
}

Delete an escalation rule

DELETE /api/v1/escalation_policies/{id}/rules/{rule_id}

Removes an escalation step from a policy along with its targets. The rule must belong to the policy named in the path, otherwise the request answers 404. Success is 200 with `{"data": {"deleted": true}}`, not 204.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.
rule_id string in path required
Escalation rule ID within the policy.

Responses

200 application/json
The rule was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The policy or the rule does not exist here
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/escalation_policies/$ID/rules/$RULE_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Add a target to an escalation rule

POST /api/v1/escalation_policies/{id}/rules/{rule_id}/targets

Attaches a notification target to a rule. Attributes are nested under a `target` key. `target_type` selects which id field is read: `user` reads `user_id`, `schedule` reads `schedule_id`, and `role` reads `role`. The referenced user or schedule must belong to the caller's organization; a cross-organization id is rejected with 422 rather than paging a stranger. For `target_type` `role`, `role` must be one of `owner`, `admin`, `executive`, `technician` or `viewer`, the retired `member` role is rejected with 422.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.
rule_id string in path required
Escalation rule ID within the policy.

Request body

Target attributes

target object required
target_type string required
Kind of target.
role string
Role to notify, for `role` targets. The retired `member` role is rejected.
schedule_id uuid
Schedule to notify, for `schedule` targets.
user_id uuid
User to notify, for `user` targets.

Responses

201 application/json
The created target
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The policy or the rule does not exist here
422 application/json
Validation failed, or the target does not name a user or schedule in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/escalation_policies/$ID/rules/$RULE_ID/targets \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "target": {
    "role": "owner",
    "schedule_id": "550e8400-e29b-41d4-a716-446655440000",
    "target_type": "schedule",
    "user_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}'

Response 201

{
  "data": {
    "escalation_rule_id": "550e8400-e29b-41d4-a716-446655440000",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "role": "string",
    "schedule_id": "550e8400-e29b-41d4-a716-446655440000",
    "target_type": "schedule",
    "user_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Remove a target from an escalation rule

DELETE /api/v1/escalation_policies/{id}/rules/{rule_id}/targets/{target_id}

Detaches a notification target from a rule. The whole chain is checked: the rule must belong to the policy and the target to the rule, otherwise the request answers 404. Success is 200 with `{"data": {"deleted": true}}`.

Requires scope config:write

Parameters

id string in path required
Escalation policy ID.
rule_id string in path required
Escalation rule ID within the policy.
target_id string in path required
Escalation target ID within the rule.

Responses

200 application/json
The target was removed
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The policy, rule or target does not exist here
422 application/json
The target could not be removed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/escalation_policies/$ID/rules/$RULE_ID/targets/$TARGET_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Webhooks

Outbound event delivery to your endpoints.

List webhook endpoints

GET /api/v1/webhook_endpoints

Lists every outbound webhook endpoint in the authenticated organization. This endpoint does not paginate and omits `meta`. The signing `secret` is write-only: it is returned once, by `create`, and never appears here or on `show`/`update`.

Requires scope webhooks:manage

Responses

200 application/json
Webhook endpoints
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/webhook_endpoints \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "active": true,
      "consecutive_failures": 0,
      "events": [
        "alert.triggered",
        "alert.resolved"
      ],
      "id": "dd0e8400-e29b-41d4-a716-446655440007",
      "name": "NOC bus",
      "url": "https://hooks.example.com/towerops"
    }
  ]
}

Create a webhook endpoint

POST /api/v1/webhook_endpoints

Subscribes an HTTPS endpoint to organization events. `events` names the subscriptions; only those events are delivered to it. The catalog, with a description and an example payload per event, is `GET /api/v1/webhook_events`. This is the only response that carries `secret`, the key used for the `x-towerops-signature` header on every delivery. It is write-only afterwards: store it now, because no later read returns it.

Requires scope webhooks:manage

Request body

Endpoint attributes

webhook_endpoint object required
name string required
Endpoint name.
url string required
Destination URL. Must be HTTPS.
active boolean
Whether deliveries are attempted.
events array of string
Events to subscribe to.
secret string
Signing secret for the `x-towerops-signature` header. Write-only.

Responses

201 application/json
The created endpoint, including its signing secret
400 application/json
The `webhook_endpoint` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/webhook_endpoints \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "webhook_endpoint": {
    "active": true,
    "events": [
      "string"
    ],
    "name": "string",
    "secret": "string",
    "url": "string"
  }
}'

Response 201

{
  "data": {
    "active": true,
    "consecutive_failures": 0,
    "events": [
      "alert.triggered",
      "alert.resolved"
    ],
    "id": "dd0e8400-e29b-41d4-a716-446655440007",
    "name": "NOC bus",
    "secret": "string",
    "url": "https://hooks.example.com/towerops"
  }
}

Retrieve a webhook endpoint

GET /api/v1/webhook_endpoints/{id}

Gets one endpoint by ID, including its delivery health: `consecutive_failures`, `last_delivery_at` and `last_delivery_status`. The signing secret is not returned.

Requires scope webhooks:manage

Parameters

id string in path required
Webhook endpoint ID.

Responses

200 application/json
The endpoint
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No endpoint with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/webhook_endpoints/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "active": true,
    "consecutive_failures": 0,
    "events": [
      "alert.triggered",
      "alert.resolved"
    ],
    "id": "dd0e8400-e29b-41d4-a716-446655440007",
    "name": "NOC bus",
    "url": "https://hooks.example.com/towerops"
  }
}

Update a webhook endpoint

PATCH PUT /api/v1/webhook_endpoints/{id}

Updates the given attributes of an endpoint. Omitted attributes are left alone, and `events` replaces the subscription list wholesale rather than merging with it. Setting `active` back to true is how an endpoint the circuit breaker deactivated is put back into service. A rotated `secret` may be sent here, but is never echoed back.

Requires scope webhooks:manage

Parameters

id string in path required
Webhook endpoint ID.

Request body

Endpoint attributes

webhook_endpoint object required
name string required
Endpoint name.
url string required
Destination URL. Must be HTTPS.
active boolean
Whether deliveries are attempted.
events array of string
Events to subscribe to.
secret string
Signing secret for the `x-towerops-signature` header. Write-only.

Responses

200 application/json
The updated endpoint
400 application/json
The `webhook_endpoint` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No endpoint with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/webhook_endpoints/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "webhook_endpoint": {
    "active": true,
    "events": [
      "string"
    ],
    "name": "string",
    "secret": "string",
    "url": "string"
  }
}'

Response 200

{
  "data": {
    "active": true,
    "consecutive_failures": 0,
    "events": [
      "alert.triggered",
      "alert.resolved"
    ],
    "id": "dd0e8400-e29b-41d4-a716-446655440007",
    "name": "NOC bus",
    "url": "https://hooks.example.com/towerops"
  }
}

Delete a webhook endpoint

DELETE /api/v1/webhook_endpoints/{id}

Unsubscribes and removes an endpoint along with its delivery history.

Requires scope webhooks:manage

Parameters

id string in path required
Webhook endpoint ID.

Responses

200 application/json
The endpoint was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No endpoint with that ID in this organization
422 application/json
The endpoint could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/webhook_endpoints/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

List deliveries for an endpoint

GET /api/v1/webhook_endpoints/{id}/deliveries

Lists delivery attempts for one endpoint, newest first, paginated like every other collection: `page`/`per_page` (default 100, max 100) or the `?cursor=` returned under `meta.next_cursor`. Each entry carries the event name, the delivery status, the attempt count, the request payload sent, and the HTTP status, response body (truncated to 2000 characters) or transport error the receiver produced.

Requires scope webhooks:manage

Parameters

id string in path required
Webhook endpoint ID.
cursor string in query
Opaque cursor from `meta.next_cursor`; overrides `page`/`per_page`.
page integer in query
Page number, default 1.
per_page integer in query
Rows per page, default 100, max 100.

Responses

200 application/json
Deliveries
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No endpoint with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/webhook_endpoints/$ID/deliveries \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=VALUE" \
  --data-urlencode "per_page=VALUE"

Response 200

{
  "data": [
    {
      "attempt_count": 0,
      "created_at": "2026-01-15T19:44:25Z",
      "delivered_at": "2026-01-15T19:44:25Z",
      "endpoint_id": "550e8400-e29b-41d4-a716-446655440000",
      "error": "string",
      "event": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "last_attempt_at": "2026-01-15T19:44:25Z",
      "payload": null,
      "response_body": "string",
      "response_status": 0,
      "status": "string",
      "test": true
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Replay a delivery

POST /api/v1/webhook_endpoints/{id}/deliveries/{delivery_id}/replay

Re-sends a previously recorded delivery to its endpoint, the usual fix after a receiver outage. The replay is signed afresh and answers with the delivery's updated state; the response is 200, not 201. The delivery must belong to the endpoint named in the path: a delivery id that exists but belongs to another endpoint answers 404, exactly like an unknown id. A delivery that is still being attempted (queued, sending, or waiting to retry) answers 409 and is left alone; replay it once that attempt finishes.

Requires scope webhooks:manage

Parameters

delivery_id string in path required
Delivery ID belonging to that endpoint.
id string in path required
Webhook endpoint ID.

Responses

200 application/json
The replayed delivery
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The endpoint does not exist here, or the delivery is not one of its own
409 application/json
The delivery is still being attempted
422 application/json
The delivery could not be replayed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/webhook_endpoints/$ID/deliveries/$DELIVERY_ID/replay \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "attempt_count": 0,
    "created_at": "2026-01-15T19:44:25Z",
    "delivered_at": "2026-01-15T19:44:25Z",
    "endpoint_id": "550e8400-e29b-41d4-a716-446655440000",
    "error": "string",
    "event": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "last_attempt_at": "2026-01-15T19:44:25Z",
    "payload": null,
    "response_body": "string",
    "response_status": 0,
    "status": "string",
    "test": true
  }
}

Send a test event

POST /api/v1/webhook_endpoints/{id}/test

Queues a delivery built from the catalog's sample payload for `event` and returns its id. The worker sends it through the normal signed path, so the request carries the same `x-towerops-signature`, `x-towerops-timestamp` and `x-towerops-delivery` headers as a real delivery and lands in the delivery log with its real outcome. `event` must be one the endpoint is subscribed to. A test delivery never increments the endpoint's failure counter or trips its circuit breaker, and it delivers to a disabled endpoint the same way, that is what the test is for. Available while the send-test feature flag is enabled for the organization; otherwise the route answers 404.

Requires scope webhooks:manage

Parameters

id string in path required
Webhook endpoint ID.

Request body

The catalog event to send

event string required
Event name the endpoint is subscribed to.

Responses

202 application/json
The queued delivery's id
400 application/json
The 'event' parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The endpoint does not exist here, or the feature is not enabled
422 application/json
The event is unknown or the endpoint is not subscribed to it
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/webhook_endpoints/$ID/test \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "string"
}'

Response 202

{
  "delivery_id": "string"
}

List the webhook event catalog

GET /api/v1/webhook_events

Every event an endpoint can subscribe to, with when it fires and an example `data` payload. The same list validates `events` on create/update, renders the Settings → Webhooks picker and the Help → Webhooks section, and generates the catalog in `docs/features/webhooks.md`, so it cannot drift from what producers emit. The catalog is product metadata, not tenant data: any authenticated API token can read it, whatever its scopes. Creating, reading or changing endpoints still requires `webhooks:manage`.

Responses

200 application/json
Event catalog
401 application/json
Missing, malformed, expired or revoked credentials
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/webhook_events \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "description": "string",
      "example": null,
      "name": "string",
      "payload_version": 0
    }
  ]
}

Inbound Webhooks

Endpoints TowerOps exposes for other systems to post to. Each verifies a shared secret of its own, an HMAC signature or a per-organization token rather than an API token, and each answers immediately while the payload is processed in the background.

Trigger a mass agent update

POST /api/v1/webhooks/agent-release

Inbound endpoint for CI to announce a new agent release, which broadcasts an update instruction to every connected agent. Two checks, both keyed on the same configured agent webhook secret and neither of them an API token. First, the route requires `Authorization: Bearer <agent webhook secret>`. Then this action requires an `x-agent-webhook-signature` header of the form `t=<unix>,v1=<hex>`, whose `v1` must be a constant-time match for a HMAC-SHA256 of `"<t>.<raw body>"`; a timestamp more than 300 seconds away from the server clock in either direction is refused so a captured request cannot be replayed (the same tolerance the Stripe webhook applies, a few seconds of CI clock skew are inside the window, not a rejection). A missing, repeated or unparseable signature header is a `401`, never a `500`. The request body is not read, it carries no fields, and the release to broadcast is resolved by the worker, but whatever bytes are sent are part of the signed payload. The broadcast is enqueued as a background job and acknowledged immediately. Rate limited to the webhook bucket, 300 requests per minute per caller IP, applied before the signature is checked.

Responses

200 application/json
The broadcast was enqueued: `{"data": {"status": "ok"}}`
401 application/json
The `Authorization` header is missing or does not carry the webhook secret, or the `x-agent-webhook-signature` header is absent, unparseable, outside the 300-second window or does not match
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The background job could not be enqueued, or no agent webhook secret is configured on this deployment

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/agent-release

Response 200

{
  "data": {
    "status": "ok"
  }
}

Receive a Gaiia event

POST /api/v1/webhooks/gaiia/{organization_id}

Inbound endpoint for real-time subscriber and network updates from Gaiia. Verification uses the per-organization secret stored on the organization's `gaiia` integration: the `x-gaiia-webhook-signature` header is checked against a constant-time HMAC over the raw request body, with a 300-second replay window. Header parsing and the millisecond `t=` scale Gaiia stamps live in the Gaiia client library. The digest covers the raw bytes, so re-encoding the JSON invalidates it. No API token is used; the organization comes from the URL and the secret from its integration. The event is enqueued as a background job and acknowledged immediately, nothing in the response reflects the outcome of processing it. The event name is read from `eventName`, falling back to `event`. The payload is read from `payload`, falling back to `data`, and the entity ID from `objectId`, falling back to the payload's own `id`.

Parameters

organization_id string in path required
Organization whose `gaiia` integration holds the webhook secret.

Responses

200 application/json
The event was accepted for background processing: `{"data": {"status": "ok"}}`
400 application/json
Neither `eventName` nor `event` is present in the body
401 application/json
The signature does not match, is outside the replay window, or the `x-gaiia-webhook-signature` header is absent, and, deliberately indistinguishably, the organization has no `gaiia` integration or no webhook secret configured on it
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The background job could not be enqueued

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/gaiia/$ORGANIZATION_ID

Response 200

{
  "data": {
    "status": "ok"
  }
}

Accept a signed inbound event

POST /api/v1/webhooks/inbound/{organization_id}

Raises or resolves an alert, appends an alert note, or records a device status for the organization. Authenticated by either an `x-towerops-signature`/`x-towerops-timestamp` HMAC over the raw body against the org's `inbound_alerts` integration `webhook_key`, or an API token (a superuser token works across organizations). Replaying a caller-supplied `event_id` is idempotent: the recorded outcome returns again and nothing re-acts.

Parameters

organization_id string in path required
Organization the event acts on.

Request body

Event envelope (`event`, caller-supplied `event_id`, and per-event fields)

event string required
`alert.raise`, `alert.resolve`, `alert.note` or `device.status`.
event_id string required
Caller-supplied idempotency key; a replay returns the recorded outcome.
alert_id string
Alert UUID; required by alert.note.
alert_type string
Alert class for alert.raise/alert.resolve; defaults to `external`.
body string
Note text for alert.note.
device_name string
Device name; required by alert.raise, alert.resolve and device.status.
message string
Alert message for alert.raise.
severity integer
Alert severity for alert.raise; 1 or 2, default 1.
status string
`up` or `down` for device.status.

Responses

202 application/json
Recorded outcome (`alert_id`, `note_id`, `device_id` or `status`)
400 application/json
Malformed event (`invalid_event`) or unresolvable target (`device_not_found`, `alert_not_found`)
401 application/json
All authentication failures are indistinguishable: bad or stale signature, missing or unknown org secret, invalid token, wrong-org token
403 application/json
A Bearer token without the write scope the event needs
404 application/json
The `inbound_alert_webhook` feature has no enabled gate for this caller
409 application/json
A `device_name` that matches more than one device (send `device_id`), or a duplicate `event_id` still being processed
422 application/json
The event's fields failed validation (e.g. an oversized note body)
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/inbound/$ORGANIZATION_ID \
  -H "Content-Type: application/json" \
  -d '{
  "device_name": "site-42-ups",
  "event": "alert.raise",
  "event_id": "ups-7-site-42-20261029",
  "message": "Site 42 mains power lost",
  "severity": 1
}'

Response 202

{
  "alert_id": "9f6d767f-e73d-47fe-9438-3bbb76e6ded7"
}

Receive a MikroTik DHCP lease event

POST /api/v1/webhooks/mikrotik/{organization_id}/dhcp

Inbound endpoint for a MikroTik router's DHCP lease script. Authentication is the per-organization shared secret stored on the organization's `mikrotik` integration, presented in the `X-TowerOps-Token` header and compared in constant time. There is no signature and no API token. `bound` is required and is sent by the router as a string: `"1"` records an assignment, `"0"` a release, and anything else is rejected. `bound` and every other parameter are forwarded to the worker as the payload. The event is enqueued and acknowledged immediately so the router's `/tool fetch` does not block the lease state machine. Identical to the `pppoe` endpoint apart from the source it records, `dhcp` here, `pppoe` there.

Parameters

organization_id string in path required
Organization whose `mikrotik` integration holds the shared token.

Responses

200 application/json
The event was accepted for background processing: `{"data": {"status": "ok"}}`
400 application/json
`bound` is missing or is neither `"1"` nor `"0"`
401 application/json
The `X-TowerOps-Token` header is absent or does not match the integration's token, and, deliberately indistinguishably, the organization has no `mikrotik` integration, has it disabled, or has no webhook token configured on it
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The background job could not be enqueued

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/mikrotik/$ORGANIZATION_ID/dhcp

Response 200

{
  "data": {
    "status": "ok"
  }
}

Receive a MikroTik PPPoE event

POST /api/v1/webhooks/mikrotik/{organization_id}/pppoe

Inbound endpoint for a MikroTik router's PPPoE on-up and on-down scripts. Authentication is the per-organization shared secret stored on the organization's `mikrotik` integration, presented in the `X-TowerOps-Token` header and compared in constant time. There is no signature and no API token. `bound` is required and is sent by the router as a string: `"1"` records an assignment, `"0"` a release, and anything else is rejected. `bound` and every other parameter are forwarded to the worker as the payload. The event is enqueued and acknowledged immediately so the router's `/tool fetch` does not block the PPP state machine. Identical to the `dhcp` endpoint apart from the source it records, `pppoe` here, `dhcp` there.

Parameters

organization_id string in path required
Organization whose `mikrotik` integration holds the shared token.

Responses

200 application/json
The event was accepted for background processing: `{"data": {"status": "ok"}}`
400 application/json
`bound` is missing or is neither `"1"` nor `"0"`
401 application/json
The `X-TowerOps-Token` header is absent or does not match the integration's token, and, deliberately indistinguishably, the organization has no `mikrotik` integration, has it disabled, or has no webhook token configured on it
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The background job could not be enqueued

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/mikrotik/$ORGANIZATION_ID/pppoe

Response 200

{
  "data": {
    "status": "ok"
  }
}

Receive a PagerDuty V3 event

POST /api/v1/webhooks/pagerduty/{organization_id}

Inbound endpoint for PagerDuty V3 webhook events, which is how an incident acknowledged or resolved in PagerDuty is reflected back onto the TowerOps alert. Verification uses the per-organization signature secret stored on the organization's enabled `pagerduty` integration: every `v1=<hex>` value in the `x-pagerduty-signature` header (the header may carry several, comma-separated, during secret rotation) is compared in constant time against a HMAC-SHA256 of the raw request body. The integration's own organization must also match the one in the URL. The digest covers the raw bytes, so re-encoding the JSON invalidates it. No API token is used. The event is enqueued as a background job and acknowledged immediately, nothing in the response reflects the outcome of processing it. Only `incident.resolved` and `incident.acknowledged` are acted on. Any other `event.event_type`, and a body without `event.event_type` and `event.data`, is still answered `200 accepted` and dropped, so PagerDuty does not retry events this integration has no use for.

Parameters

organization_id string in path required
Organization whose `pagerduty` integration holds the signature secret.

Responses

200 application/json
The event was accepted for background processing, or recognised and deliberately ignored: `{"data": {"status": "accepted"}}`
400 application/json
The request had no body to verify a signature over
401 application/json
No `x-pagerduty-signature` value matches the HMAC of the body, and, deliberately indistinguishably, the organization has no enabled `pagerduty` integration, no webhook secret configured on it, or the integration belongs to a different organization than the URL names
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The background job could not be enqueued

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/pagerduty/$ORGANIZATION_ID

Response 200

{
  "data": {
    "status": "ok"
  }
}

Receive a Quo communication

POST /api/v1/webhooks/quo/{organization_id}

Quo sends version 2026-03-30 deliveries here after provisioning through Organization Settings. HMAC-SHA256 covers webhook-id.webhook-timestamp.raw-body; webhook-signature contains space-separated v1 base64 candidates. The per-org whsec_ key is base64-decoded before verification. Timestamps must be within 300 seconds in either direction. Only enabled integrations accept delivery. Incoming message.received, call.completed and call.voicemail.completed events are queued; other event types are acknowledged without capture. Delivery ids are idempotent per organization. No API token is used.

Parameters

organization_id string in path required
Organization owning the enabled Quo integration.

Responses

200 application/json
Accepted for asynchronous processing
400 application/json
Malformed event
401 application/json
Webhook verification failed (all verification failures are indistinguishable)
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
Failed to enqueue

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/quo/$ORGANIZATION_ID

Response 200

{
  "data": {
    "status": "ok"
  }
}

Receive Slack interactions and slash commands

POST /api/v1/webhooks/slack

Slack sends application/x-www-form-urlencoded block_actions or /towerops status. The v0 HMAC covers the exact raw body and timestamp (within 300 seconds). Valid requests are queued before Slack's three-second deadline; completion is sent privately with chat.postEphemeral, never to a supplied response_url. Workspace membership and action permission are checked again during processing.

Responses

200 application/json
Ephemeral acknowledgement
401 application/json
Invalid signature
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
503 application/json
Queue unavailable

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/slack

Response 200

{
  "data": {
    "status": "ok"
  }
}

Receive a Stripe event

POST /api/v1/webhooks/stripe

Inbound endpoint for Stripe billing events. Verification is a HMAC-SHA256 over the raw request body, keyed on the configured Stripe webhook secret and read from the `stripe-signature` header (`t=<unix>,v1=<hex>[,v1=<hex>…]`; a secret rotation makes Stripe send several `v1` values), every candidate is compared in constant time and any match accepts the request. The timestamp must be within 300 seconds, so a captured request cannot be replayed later. The signature covers the raw bytes: re-encoding the JSON invalidates it. No API token is used. The event is enqueued as a background job and acknowledged immediately, nothing in the response reflects the outcome of processing it. The body is the Stripe event object as Stripe sends it and is forwarded to the worker unchanged.

Responses

200 application/json
The event was accepted for background processing: `{"data": {"received": true}}`
400 application/json
The body is not valid JSON, or the `stripe-signature` header is missing, unparseable or outside the 300-second tolerance
401 application/json
The signature does not match the webhook secret
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header
500 application/json
The background job could not be enqueued

Request

curl -X POST https://staging.towerops.net/api/v1/webhooks/stripe

Response 200

{
  "data": {
    "status": "ok"
  }
}

Mobile

The TowerOps mobile app's own API. Authenticated with a mobile session token and predating the `data` envelope, so each endpoint answers a bare key; the shapes are frozen because shipped app versions depend on them.

Retrieve an alert

GET /api/v1/mobile/alerts/{id}

Gets one alert by ID, in the same shape `list_alerts` rows carry, including `site_id`, `site_name`, `subscriber_impact` and `assigned_to_email`. This is the deep-link target a push notification's `alert_id` resolves against. An alert outside the session's organizations, or at a site outside the member's site restriction, answers 404 rather than 403 so the id's existence is never confirmed, the same rule `get_device` applies.

Parameters

id string in path required
Alert ID.

Responses

200 application/json
The alert
401 application/json
The bearer token is missing, malformed, expired or revoked
404 application/json
No alert with that ID, or the alert belongs to an organization or site outside the session's access: `Alert not found`
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl https://staging.towerops.net/api/v1/mobile/alerts/$ID \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "alert_type": "string",
  "assigned_to_email": "string",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "string",
  "site_id": "550e8400-e29b-41d4-a716-446655440000",
  "site_name": "string",
  "subscriber_impact": null,
  "triggered_at": "2026-01-15T19:44:25Z"
}

Acknowledge an alert

POST /api/v1/mobile/alerts/{id}/acknowledge

Acknowledges the alert for the session's user, attributed the same way the web action bar and the REST acknowledge record it. Gated on `:field_brief`; an alert outside the caller's organization or site restriction answers 404.

Parameters

id string in path required
Alert ID.

Responses

200 application/json
The acknowledged alert
401 application/json
The bearer token is missing, malformed, expired or revoked
404 application/json
No such alert, or :field_brief is off for this caller
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/alerts/$ID/acknowledge \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "alert_type": "string",
  "assigned_to_email": "string",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "string",
  "site_id": "550e8400-e29b-41d4-a716-446655440000",
  "site_name": "string",
  "subscriber_impact": null,
  "triggered_at": "2026-01-15T19:44:25Z"
}

Add a note to an alert

POST /api/v1/mobile/alerts/{id}/notes

Appends an operator note to the alert's thread, attributed to the session's user; it appears in the web timeline immediately. Gated on `:field_brief`; an alert outside the caller's organization or site restriction answers 404. The body is `{"note": {"body": "..."}}`, the same shape the REST note endpoint takes.

Parameters

id string in path required
Alert ID.

Responses

201 application/json
The stored note
401 application/json
The bearer token is missing, malformed, expired or revoked
404 application/json
No such alert, or :field_brief is off for this caller
422 application/json
A non-empty `note.body` string is required
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/alerts/$ID/notes \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 201

{
  "author_email": "string",
  "body": "string",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "inserted_at": "2026-01-15T19:44:25Z"
}

Delete the account

DELETE /api/v1/mobile/auth/account

Permanently deletes the signed-in user's account. App Store guideline 5.1.1(v) requires this from inside the app, so it cannot live only in the web settings page. Send `password` when the account has one and `confirmation_code` when it does not, `GET /api/v1/mobile/auth/session` reports which through `password_required`, and `POST /api/v1/mobile/auth/account/confirmation` emails the code. An account with TOTP enrolled must also send `totp_code`; the session reports that through `totp_required`. `delete_organizations` (default `false`) is the way out for the last owner of an organization who has nobody to hand it to; it destroys every site, device, alert and other member's access along with it. Every refusal is a machine-readable code rather than prose, so the app can branch: it reveals its password field on `invalid_password`, which is how a client holding a stale `password_required` recovers. A wrong credential answers `403`, never `401`, the app signs itself out on any `401`, so a typo would otherwise log the user out. The `409` bodies also carry `organizations`, the names the app shows before asking again. Deleting the user cascades to this mobile session, so there is nothing left to revoke afterwards. Rate limited to the 10/min auth bucket, not the API one, because it is a credential check.

Request body

The password or emailed confirmation code, and whether to destroy solely-owned organizations

confirmation_code string
Code from `POST /api/v1/mobile/auth/account/confirmation`.
delete_organizations boolean
Whether to destroy organizations the user solely owns.
password string
Account password. Required when the account has one.
totp_code string
Authenticator code. Required when the account has TOTP enabled; recovery codes are not accepted.

Responses

200 application/json
The account was deleted: `{"success": true}`
401 application/json
The bearer token is missing, malformed, expired or revoked. Credential failures answer `403`, not this
403 application/json
`invalid_password`, `confirmation_code_required`, `invalid_confirmation_code`, `totp_code_required` or `invalid_totp_code`
409 application/json
`sole_owner` or `active_subscription`, each with the blocking organization names under `organizations`
429 application/json
Rate limit exceeded (10/min auth bucket)

Request

curl -X DELETE https://staging.towerops.net/api/v1/mobile/auth/account \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "confirmation_code": "string",
  "delete_organizations": true,
  "password": "string",
  "totp_code": "string"
}'

Response 200

{
  "sent": true,
  "success": true
}

Email an account-deletion code

POST /api/v1/mobile/auth/account/confirmation

Emails a short confirmation code for deleting an account that has no password, for use as `confirmation_code` on `DELETE /api/v1/mobile/auth/account`. Takes no body. Only valid for a passwordless account: one with a password is challenged with that instead and answers `409 password_required` rather than sending a code that would never be checked. `{"sent": true}` is what tells the app to stop offering to send and start asking for the code, so a queue failure is reported as `500 could_not_send_code` rather than a success, otherwise someone waits for mail that is never coming with no way back. Rate limited to the 10/min auth bucket.

Responses

200 application/json
The code was emailed: `{"sent": true}`
401 application/json
The bearer token is missing, malformed, expired or revoked
409 application/json
The account has a password and must be challenged with it: `password_required`
429 application/json
Rate limit exceeded (10/min auth bucket)
500 application/json
The code could not be queued for delivery: `could_not_send_code`

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/auth/account/confirmation \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "sent": true,
  "success": true
}

Log in with email and password

POST /api/v1/mobile/auth/login

Email and password log-in for the mobile app: the JSON sibling of the web session controller, sharing its throttle buckets and its log-in attempt recording. Two different `200` bodies come out of this endpoint: * without a second factor, the session payload, `session_token`, `expires_at`, `absolute_expires_at` (the one-year limit the slide cannot pass) and `user`; * with TOTP enabled, `{"totp_required": true, "challenge_token": "…"}`, which must be presented to `POST /api/v1/mobile/auth/login/totp` within 300 seconds. A wrong password, an unknown address and a correct password on an unconfirmed account are deliberately indistinguishable: all three answer `401` with the same sentence, and the unconfirmed case quietly re-sends the confirmation email. No sudo/re-authentication concept exists for mobile sessions, so none is granted. Unauthenticated, and rate limited to the 10/min auth bucket; per-IP and per-user log-in throttles answer `429` with a `Retry-After` header.

Request body

Credentials and this device's details

email string required
Account email.
password string required
Account password.
app_version string
App version to record on the session.
device_name string
Device name to show in session lists.
device_os string
OS to record on the session.
push_platform string
Push platform the token belongs to.
push_token string
APNs/FCM token.

Responses

200 application/json
Either the session payload, or a TOTP challenge when the account has a second factor
400 application/json
No `email` in the body: `Missing email parameter`
401 application/json
Wrong password, unknown address, or an unconfirmed account: `Invalid email or password, or your email is not confirmed yet.`
422 application/json
Credentials were accepted but the session could not be created: `Failed to create session`
429 application/json
Too many attempts. Carries `Retry-After`; the body names the wait in seconds

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/auth/login \
  -H "Content-Type: application/json" \
  -d '{
  "app_version": "string",
  "device_name": "string",
  "device_os": "string",
  "email": "string",
  "password": "string",
  "push_platform": "string",
  "push_token": "string"
}'

Response 200

{
  "absolute_expires_at": "2027-01-15T19:44:25Z",
  "expires_at": "2026-02-15T19:44:25Z",
  "session_token": "mst_9f2c…",
  "user": {
    "email": "noc@example.com",
    "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
  }
}

Complete a two-factor log-in

POST /api/v1/mobile/auth/login/totp

Second leg of a 2FA log-in. Send the `challenge_token` returned by `POST /api/v1/mobile/auth/login` together with `code`; both are required and both are strings. The challenge is a signed token with a 300-second life and a salt used nowhere else, so it cannot be presented at any other endpoint. Recovery codes are accepted as well as authenticator codes, someone who has lost their authenticator must still be able to reach their alerts, and the log-in history records which factor was actually used. `device_name`, `device_os`, `app_version`, `push_token` and `push_platform` are optional and are recorded on the new session exactly as on the password path. Unauthenticated, and rate limited to the 10/min auth bucket, with an additional per-user TOTP throttle that answers `429` with `Retry-After`.

Request body

The challenge token and the authenticator or recovery code

challenge_token string required
The `challenge_token` the login step returned alongside `totp_required`.
code string required
Six-digit authenticator code, or a backup code.
app_version string
App version to record on the session.
device_name string
Device name to show in session lists.
device_os string
OS to record on the session.
push_platform string
Push platform the token belongs to.
push_token string
APNs/FCM token.

Responses

200 application/json
The minted session token, its expiry and the user
400 application/json
`challenge_token` or `code` missing, or not a string: `Missing challenge_token or code`
401 application/json
`expired_challenge` when the challenge is older than 300 seconds, `invalid_challenge` when it does not verify, `invalid_code` when the second factor is wrong
422 application/json
The second factor was accepted but the session could not be created: `Failed to create session`
429 application/json
Too many attempts against this account's second factor. Carries `Retry-After`

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/auth/login/totp \
  -H "Content-Type: application/json" \
  -d '{
  "app_version": "string",
  "challenge_token": "string",
  "code": "string",
  "device_name": "string",
  "device_os": "string",
  "push_platform": "string",
  "push_token": "string"
}'

Response 200

{
  "absolute_expires_at": "2027-01-15T19:44:25Z",
  "expires_at": "2026-02-15T19:44:25Z",
  "session_token": "mst_9f2c…",
  "user": {
    "email": "noc@example.com",
    "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
  }
}

Complete a QR login

POST /api/v1/mobile/auth/qr/complete

Consumes a QR login challenge and mints a mobile session bound to this device. The session token is returned as `session_token`, together with `expires_at` and the user's `id` and `email`. `expires_at` is a sliding 90-day expiry capped at `absolute_expires_at`, one year from creation (issue 2179); the app can read both off the session response. `token` is required. `device_name`, `device_os`, `app_version`, `push_token` and `push_platform` are optional and are recorded on the session row, which is what the session list and push delivery read. A successful login is recorded in the account's log-in history with method `mobile_qr`. `Invalid or expired token` covers expired, already-consumed and forged tokens alike. Unauthenticated, and rate limited to the 10/min auth bucket.

Request body

The challenge token and this device's details

token string required
Challenge token from the QR code.
app_version string
App version to record on the session.
device_name string
Human-readable device name to show in session lists.
device_os string
OS to record on the session, e.g. `iOS 17.2`.
push_platform string
Push platform the token belongs to.
push_token string
APNs/FCM token for push notifications.

Responses

200 application/json
The minted session token, its expiry and the user
400 application/json
No `token` in the body: `Missing token parameter`
401 application/json
Unknown, consumed or expired token: `Invalid or expired token`
422 application/json
The session could not be created: `Failed to create session`, with per-field messages under `details`
429 application/json
Rate limit exceeded (10/min auth bucket)

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/auth/qr/complete \
  -H "Content-Type: application/json" \
  -d '{
  "app_version": "string",
  "device_name": "string",
  "device_os": "string",
  "push_platform": "string",
  "push_token": "string",
  "token": "string"
}'

Response 200

{
  "absolute_expires_at": "2027-01-15T19:44:25Z",
  "expires_at": "2026-02-15T19:44:25Z",
  "session_token": "mst_9f2c…",
  "user": {
    "email": "noc@example.com",
    "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
  }
}

Verify a QR login challenge

POST /api/v1/mobile/auth/qr/verify

Checks that a QR login token scanned from the desktop log-in screen is still live, not expired and not already consumed. It does not create a session; call `POST /api/v1/mobile/auth/qr/complete` for that. The success body is only `{"valid": true}`. The associated email is deliberately withheld: answering with it would turn a guessed or captured QR token into an email-enumeration oracle. Unauthenticated, and rate limited to the 10/min auth bucket. Failures are a bare `{"error": "…"}` string, not the v1 error envelope.

Request body

The scanned challenge token

token string required
Challenge token from the QR code.

Responses

200 application/json
The challenge is still live
400 application/json
No `token` in the body: `Missing token parameter`
401 application/json
Unknown, consumed or expired token: `{"valid": false, "error": "Invalid or expired token"}`
429 application/json
Rate limit exceeded (10/min auth bucket)

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/auth/qr/verify \
  -H "Content-Type: application/json" \
  -d '{
  "token": "string"
}'

Response 200

{
  "valid": true
}

Register an account

POST /api/v1/mobile/auth/register

Creates an account and its first organization from the mobile app. `email` and `password` are required; `first_name`, `last_name`, `organization_name`, `privacy_policy_consent`, `terms_of_service_consent` and `timezone` are read from the body, and `timezone` defaults to `UTC`. Deliberately never returns a session: `confirmed_at` gates log-in, so a new account has to open its confirmation email regardless, and a token minted here would be rejected by the very next request. The `201` body is `{"status": "confirmation_required", "email": "…"}`. An address that is already taken answers exactly like a success, without that, this endpoint is an account-enumeration oracle, and the existing holder is quietly emailed instead. Unauthenticated, and rate limited to the 10/min auth bucket.

Request body

Account and first-organization details

email string required
Account email.
password string required
Account password.
first_name string
Given name.
last_name string
Family name.
organization_name string
Name for the organization to create.
privacy_policy_consent boolean
Whether the user accepted the privacy policy.
terms_of_service_consent boolean
Whether the user accepted the terms of service.
timezone string
IANA timezone for the account. Defaults to `UTC`.

Responses

201 application/json
Registration accepted: `{"status": "confirmation_required", "email": "…"}`. Also the answer when the address was already taken
400 application/json
No `email` in the body: `Missing email parameter`
422 application/json
`Validation failed`, with per-field messages under `details`; or `We couldn't finish setting up your organization. Please try again.` when the organization could not be created
429 application/json
Rate limit exceeded (10/min auth bucket)

Request

curl -X POST https://staging.towerops.net/api/v1/mobile/auth/register \
  -H "Content-Type: application/json" \
  -d '{
  "email": "string",
  "first_name": "string",
  "last_name": "string",
  "organization_name": "string",
  "password": "string",
  "privacy_policy_consent": true,
  "terms_of_service_consent": true,
  "timezone": "string"
}'

Response 201

{
  "email": "new.engineer@example.com",
  "status": "confirmation_required"
}

Retrieve the current session

GET /api/v1/mobile/auth/session

Returns the session this bearer token belongs to and the user behind it. The app has no other way to re-read `alerts_enabled`, `push_registered` or `user.email`: the email is returned exactly once at log-in and the push-notification preference lives only on this row. `password_required` tells the app whether to ask for a password before deleting the account, a magic-link account has none, and `totp_required` whether to ask for an authenticator code alongside it. Answered unwrapped, with no `data` envelope.

Responses

200 application/json
The current session and its user
401 application/json
The bearer token is missing, malformed, expired or revoked
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl https://staging.towerops.net/api/v1/mobile/auth/session \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "absolute_expires_at": "2026-01-15T19:44:25Z",
  "alerts_enabled": true,
  "app_version": "string",
  "device_name": "string",
  "device_os": "string",
  "expires_at": "2026-01-15T19:44:25Z",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "last_used_at": "2026-01-15T19:44:25Z",
  "password_required": true,
  "push_platform": "string",
  "push_registered": true,
  "totp_required": true,
  "user": {
    "email": "noc@example.com",
    "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
  }
}

Update the current session

PATCH /api/v1/mobile/auth/session

Updates this session's push registration and alert opt-in. The app calls it after every APNs/FCM registration, because device tokens rotate and the log-in endpoints are the only other write path. Only `push_token`, `push_platform` and `alerts_enabled` are read; all three are optional and anything else in the body is ignored. Answers the same body as `GET /api/v1/mobile/auth/session`.

Request body

The session fields to change

alerts_enabled boolean
Whether to send alert pushes to this device.
push_platform string
Push platform the token belongs to.
push_token string
New APNs/FCM token.

Responses

200 application/json
The updated session
401 application/json
The bearer token is missing, malformed, expired or revoked
404 application/json
The session row disappeared between authentication and the write: `Session not found`
422 application/json
`Failed to update session`, with per-field messages under `details`
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl -X PATCH https://staging.towerops.net/api/v1/mobile/auth/session \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "alerts_enabled": true,
  "push_platform": "string",
  "push_token": "string"
}'

Response 200

{
  "absolute_expires_at": "2026-01-15T19:44:25Z",
  "alerts_enabled": true,
  "app_version": "string",
  "device_name": "string",
  "device_os": "string",
  "expires_at": "2026-01-15T19:44:25Z",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "last_used_at": "2026-01-15T19:44:25Z",
  "password_required": true,
  "push_platform": "string",
  "push_registered": true,
  "totp_required": true,
  "user": {
    "email": "noc@example.com",
    "id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30"
  }
}

Revoke the current session

DELETE /api/v1/mobile/auth/session

Logs out: revokes the mobile session this bearer token belongs to. Other sessions on other devices are untouched. Answers `{"success": true}`.

Responses

200 application/json
The session was revoked: `{"success": true}`
401 application/json
The bearer token is missing, malformed, expired or revoked
429 application/json
Rate limit exceeded (1000/min API bucket)
500 application/json
The session could not be revoked: `Failed to revoke session`

Request

curl -X DELETE https://staging.towerops.net/api/v1/mobile/auth/session \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "sent": true,
  "success": true
}

Retrieve a device

GET /api/v1/mobile/devices/{id}

Gets one device with its SNMP detail, sensors and interfaces. Access is checked against the device's own organization, so an ID from another organization is refused rather than returned, and refused as `404`, not `403`, so the response cannot confirm that a foreign device ID exists. Answered unwrapped: the device object is the whole body, with no `data` envelope and no wrapper key.

Parameters

id string in path required
Device ID.

Responses

200 application/json
The device
401 application/json
The bearer token is missing, malformed, expired or revoked
404 application/json
No device with that ID, or the device belongs to an organization the user is not a member of: `Device not found`
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl https://staging.towerops.net/api/v1/mobile/devices/$ID \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "ip_address": "string",
  "name": "string",
  "status": "string"
}

Field brief for a device

GET /api/v1/mobile/devices/{id}/brief

Everything a climber needs before a site visit in one call: the device, its site's contact and access metadata (contact, access notes, gate code, power feed, landlord, circuit ID), the device's last five alerts, the last config change recorded against it, and the note thread on the alert it anchors. Gated on `:field_brief`; a device outside the caller's organizations or site restriction answers 404.

Parameters

id string in path required
Device ID.

Responses

200 application/json
The device's field brief
401 application/json
The bearer token is missing, malformed, expired or revoked
404 application/json
No such device, or :field_brief is off for this caller
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl https://staging.towerops.net/api/v1/mobile/devices/$ID/brief \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "alerts": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "message": "string",
      "occurred_at": "2026-01-15T19:44:25Z",
      "severity": 0,
      "status": "string"
    }
  ],
  "device": null,
  "last_config_change": null,
  "notes": {
    "alert_id": "550e8400-e29b-41d4-a716-446655440000",
    "items": [
      {
        "author_email": "string",
        "body": "string",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "inserted_at": "2026-01-15T19:44:25Z"
      }
    ]
  },
  "site": null
}

List the user's organizations

GET /api/v1/mobile/organizations

Lists every organization the mobile session's user belongs to, each with rollup counts (`sites_count`, `device_count`, `active_alerts_count`). The mobile API does not use the v1 `data` envelope: the body is a bare `organizations` key, and failures are a bare `{"error": "…"}` string rather than the v1 error envelope. Both shapes are frozen, shipped app versions read them directly.

Responses

200 application/json
Organizations for the session's user
401 application/json
The bearer token is missing, malformed, expired or revoked
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl https://staging.towerops.net/api/v1/mobile/organizations \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "organizations": [
    {
      "active_alerts_count": 0,
      "device_count": 0,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "string",
      "sites_count": 0
    }
  ]
}

List alerts in an organization

GET /api/v1/mobile/organizations/{organization_id}/alerts

Lists alerts for one organization, newest first. History is capped at the last seven days, except that an alert which is still unresolved stays visible however old it is. Answers a bare `alerts` key. An alert's `status` comes from the same `Alert.status/1` the GraphQL API uses, so "snoozed" outranks "acknowledged" here too.

Parameters

organization_id string in path required
Organization ID.
device_id string in query
Only alerts raised against this device.
limit string in query
How many alerts to return. Defaults to 50; anything outside 1..200, or unparseable, falls back to 50.
severity string in query
Only alerts at this effective severity, the same value each row's `severity` integer carries: `critical` (2), `warning` (1) or `info` (null). The integers work too. An unrecognized value leaves the list unfiltered.
site_id string in query
Only alerts for devices at this site.
status string in query
Only alerts in this status, e.g. `active`, `acknowledged`, `resolved`.

Responses

200 application/json
Alerts for the organization
401 application/json
The bearer token is missing, malformed, expired or revoked
403 application/json
The user has no membership in this organization: `Access denied to this organization`
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl -G https://staging.towerops.net/api/v1/mobile/organizations/$ORGANIZATION_ID/alerts \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN" \
  --data-urlencode "device_id=VALUE" \
  --data-urlencode "limit=50" \
  --data-urlencode "severity=VALUE" \
  --data-urlencode "site_id=VALUE" \
  --data-urlencode "status=VALUE"

Response 200

{
  "alerts": [
    {
      "alert_type": "string",
      "assigned_to_email": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "message": "string",
      "site_id": "550e8400-e29b-41d4-a716-446655440000",
      "site_name": "string",
      "subscriber_impact": null,
      "triggered_at": "2026-01-15T19:44:25Z"
    }
  ]
}

List devices in an organization

GET /api/v1/mobile/organizations/{organization_id}/devices

Lists the devices of one organization, ordered by display order then name, with SNMP-derived status and uptime. The list key is `device`, singular, a frozen mobile wire key. Query parameters other than these are ignored.

Parameters

organization_id string in path required
Organization ID.
limit integer in query
Return at most this many devices, capped at 500. A value that does not parse means no limit.
site_id string in query
Only devices at this site.
status string in query
Only devices in this status, e.g. `up`, `down`, `unknown`.

Responses

200 application/json
Devices in the organization, under the singular `device` key
401 application/json
The bearer token is missing, malformed, expired or revoked
403 application/json
The user has no membership in this organization: `Access denied to this organization`
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl -G https://staging.towerops.net/api/v1/mobile/organizations/$ORGANIZATION_ID/devices \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN" \
  --data-urlencode "limit=100" \
  --data-urlencode "site_id=VALUE" \
  --data-urlencode "status=VALUE"

Response 200

{
  "device": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "ip_address": "string",
      "name": "string",
      "site_id": "550e8400-e29b-41d4-a716-446655440000",
      "site_name": "string",
      "status": "string"
    }
  ]
}

List sites in an organization

GET /api/v1/mobile/organizations/{organization_id}/sites

Lists the sites of one organization the session's user has access to, each with its device count and its count of devices currently down. Answers a bare `sites` key. The per-site down count is named `equipment_down_count`, a frozen mobile wire key that predates the rename to "devices" and that legacy clients still read.

Parameters

organization_id string in path required
Organization ID.

Responses

200 application/json
Sites in the organization
401 application/json
The bearer token is missing, malformed, expired or revoked
403 application/json
The user has no membership in this organization: `Access denied to this organization`
429 application/json
Rate limit exceeded (1000/min API bucket)

Request

curl https://staging.towerops.net/api/v1/mobile/organizations/$ORGANIZATION_ID/sites \
  -H "Authorization: Bearer MOBILE_SESSION_TOKEN"

Response 200

{
  "sites": [
    {
      "device_count": 0,
      "equipment_down_count": 0,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "location": "string",
      "name": "string"
    }
  ]
}

Account

Per-user account endpoints served from the browser session.

Export all account data

GET /api/v1/account/data

Returns everything TowerOps holds about the signed-in user, profile, memberships and their organizations, devices and the last 90 days of alerts in the organizations they own, private assistant conversations and their completed turns, consent records including revocations, sign-in attempt history with IP and GeoIP, MFA enrolment (never TOTP secrets), the paging log, session and token metadata (never token values), and up to 1000 of the user's audit-trail entries, for GDPR Article 15 (Right to Access). The export itself is recorded in the audit trail. The devices section is capped at 10,000 rows, the alerts section at 100,000, login history and the paging log at 10,000 each, and the audit trail at 1,000 entries; the endpoint is rate limited to 5 requests per minute, so one export can neither serialize an unbounded fleet nor be rerun in a tight loop. When a cap is hit, `export_info.truncated` names the section and `export_info.totals` carries the real row counts, so the file never claims completeness it does not have. This endpoint authenticates with the browser session cookie, not an API token: it sits in the browser pipeline, so a request without a session is redirected to the log-in page rather than answered with `401`. The body is the export object itself, no `data` envelope, served as a download with `Content-Disposition: attachment; filename="towerops-data-<user_id>-<unix_timestamp>.json"`.

Responses

200 application/json
The full data export, unwrapped, as a file download
403 application/json
The user's email address is not confirmed yet: a bare `{"error": "Email address must be confirmed before exporting account data"}`, not the v1 error envelope

Request

curl https://staging.towerops.net/api/v1/account/data

Response 200

{
  "alerts": null,
  "assistant_conversations": null,
  "audit_logs": null,
  "devices": null,
  "export_info": null,
  "organizations": null,
  "profile": null
}

OAuth

The authorization-code and client-credentials grants partner apps exchange for a scoped bearer token.

Start the authorization code flow

GET /api/oauth/authorize

Validates the partner app and its registered redirect URI, then redirects to that callback with a single-use authorization code (`?code=…`) and the caller's `state`. An unknown client id, a revoked app, or a redirect URI that is not byte-for-byte in the app's allowlist returns an error here and **never** a redirect, redirecting an untrusted URI with an error would leak the request to an attacker-controlled endpoint. Once the redirect URI is proven, an unsupported `response_type` or a scope outside the app's grant redirects with the RFC 6749 `error` parameter instead.

Parameters

client_id string in query required
Partner app client id (`tpa_…`).
code_challenge string in query required
RFC 7636 PKCE challenge, `BASE64URL(SHA-256(code_verifier))`. Required; a request without one redirects with `invalid_request`.
code_challenge_method string in query required
Must be `S256`; plain transforms are not accepted.
redirect_uri string in query required
One of the app's registered redirect URIs, byte-for-byte.
response_type string in query required
Must be `code`.
scope string in query
Space-separated subset of the app's granted scopes. Omit to request all of them.
state string in query
Opaque value echoed back on the redirect.

Responses

302
Redirect to the registered callback with `code` and `state`, or with an RFC 6749 `error`.
400 application/json
`invalid_client`, `unauthorized_client` or `invalid_redirect_uri`; no `Location` header

Request

curl -G https://staging.towerops.net/api/oauth/authorize \
  --data-urlencode "client_id=VALUE" \
  --data-urlencode "code_challenge=VALUE" \
  --data-urlencode "code_challenge_method=VALUE" \
  --data-urlencode "redirect_uri=VALUE" \
  --data-urlencode "response_type=code" \
  --data-urlencode "scope=VALUE" \
  --data-urlencode "state=VALUE"

Exchange credentials or an authorization code for an access token

POST /api/oauth/token

The OAuth 2.0 client credentials grant (RFC 6749 §4.4) and authorization code grant (§4.1.3) for partner apps. Send the client id and secret either as HTTP Basic credentials or as form parameters; the header wins when both are present (§2.3.1). The returned token carries exactly the scopes the organization granted the app at install time (or the narrower scopes approved in the authorization request), never scopes the request asks for, and expires, so a partner refreshes rather than stores it. Rate limited to the auth bucket (10/min per IP) because it accepts credentials.

Request body

Client credentials or an authorization code, form-encoded or JSON

grant_type string required
The grant to run.
client_id string
Partner app client id (`tpa_…`). Omit when using HTTP Basic.
client_secret string
Partner app client secret (`tps_…`). Omit when using HTTP Basic.
code string
Single-use authorization code (`towerops_ac_…`) from `GET /api/oauth/authorize`. Required for `authorization_code`.
code_verifier string
The PKCE code_verifier matching the code_challenge the authorization was issued with. Required when the authorization request carried a code_challenge (RFC 7636).
redirect_uri string
The exact redirect URI used in the authorization request. Required for `authorization_code`.
refresh_token string
A rotating refresh token (`towerops_rt_…`) for the `refresh_token` grant. Each successful exchange returns its successor; the presented token is revoked in the same transaction.

Responses

200 application/json
A short-lived bearer token
400 application/json
`invalid_request` when credentials or an authorization code are missing, `unsupported_grant_type` for any grant other than `client_credentials` or `authorization_code`, and `invalid_grant` for an unknown, expired, consumed or redirect-mismatched code
401 application/json
`invalid_client` for bad credentials, `unauthorized_client` when the app is disabled or uninstalled. Both carry `WWW-Authenticate: Basic realm="towerops"` when the request used HTTP Basic (§5.2)
429 application/json
Rate limit exceeded

Request

curl -X POST https://staging.towerops.net/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
  "client_id": "tpa_9f2c…",
  "client_secret": "tps_4a7e…",
  "grant_type": "client_credentials"
}'

Response 200

{
  "access_token": "towerops_pat_…",
  "expires_in": 3600,
  "scope": "devices:read sites:read",
  "token_type": "Bearer"
}

Specification

The machine-readable description of this API, and the page you are reading.

Fetch the OpenAPI description

GET /api/v1/openapi.json

The OpenAPI 3 description of everything on this page, for generating a client. Public and unauthenticated: a generator should not need a token to read the contract.

Responses

200 application/vnd.oai.openapi+json
The spec

Request

curl https://staging.towerops.net/api/v1/openapi.json

Response 200

{
  "components": null,
  "info": null,
  "openapi": "string",
  "paths": null,
  "servers": []
}

Read the API reference

GET /docs/api

This page. Rendered from the OpenAPI description above, so it cannot drift from the endpoints the application actually serves.

Responses

200 text/html
The reference page

Request

curl https://staging.towerops.net/docs/api

Response 200

"string"

API Tokens

List API tokens

GET /api/v1/api_tokens

The organization's API tokens, newest first. Secrets are never returned.

Requires scope tokens:manage

Responses

200 application/json
Tokens
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token's user is not an organization owner or admin
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/api_tokens \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "created_at": "2026-01-15T19:44:25Z",
      "created_by_email": "string",
      "expires_at": "2026-01-15T19:44:25Z",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "last_used_at": "2026-01-15T19:44:25Z",
      "name": "string",
      "scopes": [
        "string"
      ],
      "site_ids": [
        "string"
      ]
    }
  ]
}

Create an API token

POST /api/v1/api_tokens

Mints a token for the calling user in this organization and returns it with `201`, the only response that carries `token`. `scopes` must be a subset of the calling token's own scopes, and the new token never outlives the one that minted it: when the calling token has an expiry, that expiry is used unless an earlier `expires_at` is given, and a later one is rejected. `site_ids` is delegated under the same rule: a site-restricted caller must supply a non-empty list contained within its own sites. Omitted, `null`, or empty `site_ids` would create an unrestricted child and are rejected with `422`. An unrestricted caller may create an unrestricted child.

Requires scope tokens:manage

Request body

Token

api_token object required
name string required
Token name.
scopes array of string required
Scopes to grant; must be a subset of the calling token's own scopes.
expires_at date-time
Optional expiry. Never later than the calling token's own expiry, when the calling token expires, the new token takes that expiry unless an earlier one is given.
site_ids array of string
Optional site restriction: empty/absent means the whole organization. Every id must name a site in this organization and, when the calling membership is itself site-scoped, stay inside that list.

Responses

201 application/json
The created token
400 application/json
The `api_token` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token's user is not an organization owner or admin
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/api_tokens \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "api_token": {
    "expires_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "scopes": [
      "string"
    ],
    "site_ids": [
      "string"
    ]
  }
}'

Response 201

{
  "data": {
    "expires_at": "2026-01-15T19:44:25Z",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "string",
    "scopes": [
      "string"
    ],
    "site_ids": [
      "string"
    ],
    "token": "string"
  }
}

Revoke an API token

DELETE /api/v1/api_tokens/{id}

Revokes one of the organization's tokens immediately. Returns `204`.

Requires scope tokens:manage

Parameters

id string in path required
Token ID.

Responses

204
Revoked
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token's user is not an organization owner or admin
404 application/json
No token with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/api_tokens/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Config Backups

Latest configuration backup per device

GET /api/v1/config_backups

Every device in the organization with its latest backup's time, version id and hash (`null` when the device has never been backed up), the fleet view of "is the backup current".

Requires scope backups:read

Responses

200 application/json
Devices
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/config_backups \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "device_id": "550e8400-e29b-41d4-a716-446655440000",
      "device_name": "string",
      "latest_backup_at": "2026-01-15T19:44:25Z",
      "latest_config_hash": "string",
      "latest_version_id": "550e8400-e29b-41d4-a716-446655440000"
    }
  ]
}

A device's backup versions

GET /api/v1/devices/{device_id}/config_backups

Up to 100 backup versions for one device, newest first. Metadata only.

Requires scope backups:read

Parameters

device_id string in path required
Device ID.

Responses

200 application/json
Versions
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/devices/$DEVICE_ID/config_backups \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "backed_up_at": "2026-01-15T19:44:25Z",
      "config_hash": "string",
      "config_size_bytes": 0,
      "device_id": "550e8400-e29b-41d4-a716-446655440000",
      "format": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "lines_added": 0,
      "lines_removed": 0,
      "model": "string",
      "os_version": "string",
      "sections_changed": [
        "string"
      ],
      "trigger_source": "string",
      "vendor": "string"
    }
  ]
}

Request a configuration backup

POST /api/v1/devices/{device_id}/config_backups

Queues a backup of the device through its agent, exactly as the UI's "Back up now" does, and returns the request with `202`. Refused with `409` when a backup is already queued or running for the device and with `422` when no agent can reach it.

Requires scope backups:write

Parameters

device_id string in path required
Device ID.

Responses

202 application/json
Queued request
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No device with that ID in this organization
409 application/json
A backup is already in progress
422 application/json
No agent can reach the device
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/devices/$DEVICE_ID/config_backups \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 202

{
  "data": {
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "requested_at": "2026-01-15T19:44:25Z",
    "source": "string",
    "status": "string"
  }
}

Download a device's masked backup history ZIP (Beta)

GET /api/v1/devices/{device_id}/config_backups/export

Up to 100 versions per page, newest first. X-Page and X-Total-Pages identify the page. Follow all pages for the retained history. Same rollout and audit requirements as the version download. Empty history returns 404.

Requires scope backups:read

Parameters

device_id string in path required
page integer in query
History page, starting at 1.

Responses

200 application/zip
Masked backup history
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
Feature disabled or no retained text
500 application/json
Audit write failed, no download

Request

curl -G https://staging.towerops.net/api/v1/devices/$DEVICE_ID/config_backups/export \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "page=VALUE"

Response 200

"string"

Download a masked configuration version (Beta)

GET /api/v1/devices/{device_id}/config_backups/{version_id}

Masked text only, never restorable. Requires the config_backup_api rollout gate. Every fetch is audited with the credential and version ID. Raw parameters are ignored.

Requires scope backups:read

Parameters

device_id string in path required
version_id string in path required

Responses

200 text/plain
Masked configuration
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
Feature disabled, version missing or text pruned
500 application/json
Audit write failed, no download

Request

curl https://staging.towerops.net/api/v1/devices/$DEVICE_ID/config_backups/$VERSION_ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

"string"

Discovery

List discovered candidates

GET /api/v1/discovery/candidates

Lists discovered device candidates for the authenticated organization, one page at a time, the same merged sightings the Discovered tab shows. With no pagination parameters the first page of 100 is returned and `meta` is always present.

Requires scope discovery:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`. Carries both the page and the page size, so it takes precedence over `page` and `per_page`. A cursor that does not decode is rejected with `bad_request`.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.

Responses

200 application/json
Discovered candidates
400 application/json
The `cursor` parameter did not decode
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/discovery/candidates \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=50"

Response 200

{
  "data": [
    {
      "capabilities": [
        "bridge",
        "router"
      ],
      "device_count": 1,
      "device_type": "switch",
      "discovered_by": [
        {
          "device_id": "9d1a5c3e-1f2b-4c8a-9e7d-2b6f4a8c1d30",
          "device_name": "edge-router",
          "interface": "ether1",
          "timestamp": "2026-09-23T12:00:00Z"
        }
      ],
      "hostname": "core-sw-01",
      "identifier": {
        "type": "mac",
        "value": "aa:bb:cc:dd:ee:ff"
      },
      "ip_addresses": [
        "10.0.0.2"
      ],
      "last_seen": "2026-09-23T12:00:00Z",
      "mac_addresses": [
        "aa:bb:cc:dd:ee:ff"
      ],
      "manufacturer": "MikroTik",
      "platform": "CRS326",
      "protocols_used": [
        "lldp",
        "arp"
      ],
      "source_count": 2
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Add a discovered candidate

POST /api/v1/discovery/candidates/add

Creates a managed device from a discovered candidate, prefilled with the identity discovery inferred: the hostname becomes the device name, the first sighted IP becomes `ip_address`, and the inferred class becomes `device_role`. SNMP discovery is enqueued for the new device in the background. A candidate whose only identifier is a MAC or hostname has no IP to monitor and is rejected with `unprocessable_entity`. An IP that already belongs to a device anywhere in the organization is rejected with `conflict`.

Requires scope discovery:write

Request body

Candidate identifier

identifier object required
type string required
The identifier kind.
value string required
The identifier value, as returned in a candidate's `identifier`.

Responses

201 application/json
The created device
400 application/json
The `identifier` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No candidate with that identifier
409 application/json
A device with this IP address already exists
422 application/json
The candidate cannot be added
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/discovery/candidates/add \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "identifier": {
    "type": "mac",
    "value": "string"
  }
}'

Response 201

{
  "data": {
    "device_role": "router",
    "id": "770e8400-e29b-41d4-a716-446655440002",
    "inserted_at": "2026-01-15T19:44:25Z",
    "ip_address": "10.0.0.1",
    "monitoring_enabled": true,
    "name": "Core Router",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "snmp_enabled": true,
    "snmp_version": "2c"
  }
}

Ignore a discovered candidate

POST /api/v1/discovery/candidates/ignore

Records an operator decision dismissing a discovered candidate. Ignored identities stay ignored across re-sightings and stop appearing in review lists.

Requires scope discovery:write

Request body

Candidate identifier

identifier object required
type string required
The identifier kind.
value string required
The identifier value, as returned in a candidate's `identifier`.

Responses

200 application/json
The candidate was ignored
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No candidate with that identifier
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/discovery/candidates/ignore \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "identifier": {
    "type": "mac",
    "value": "string"
  }
}'

Response 200

{
  "data": {
    "ignored": true
  }
}

RF

Create an RF planning link

POST /api/v1/rf/links

Creates a point-to-point link. Each end is `end_*_radio_id` or the `end_*_latitude`/`longitude`/`height_agl_m` point triple, never both.

Requires scope rf:write

Request body

Link attributes

link object required
end_a_ground_elevation_m number
End A ground elevation in metres (bare point).
end_a_height_agl_m number
End A mount height in metres (bare point).
end_a_latitude number
End A latitude (bare point).
end_a_longitude number
End A longitude (bare point).
end_a_radio_id uuid
Radio anchoring end A.
end_b_ground_elevation_m number
End B ground elevation in metres (bare point).
end_b_height_agl_m number
End B mount height in metres (bare point).
end_b_latitude number
End B latitude (bare point).
end_b_longitude number
End B longitude (bare point).
end_b_radio_id uuid
Radio anchoring end B.
frequency_mhz number
Link frequency in MHz.
name string
Link name.
notes string
Free-form notes.
plan_id string
Plan to place the link in; defaults to its radios' plan.
polarization string
horizontal, vertical or circular.

Responses

201 application/json
The created link
400 application/json
The `link` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/rf/links \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "link": {
    "end_a_ground_elevation_m": 0.0,
    "end_a_height_agl_m": 0.0,
    "end_a_latitude": 0.0,
    "end_a_longitude": 0.0,
    "end_a_radio_id": "550e8400-e29b-41d4-a716-446655440000",
    "end_b_ground_elevation_m": 0.0,
    "end_b_height_agl_m": 0.0,
    "end_b_latitude": 0.0,
    "end_b_longitude": 0.0,
    "end_b_radio_id": "550e8400-e29b-41d4-a716-446655440000",
    "frequency_mhz": 0.0,
    "name": "string",
    "notes": "string",
    "plan_id": "string",
    "polarization": "string"
  }
}'

Response 201

{
  "data": {
    "distance_m": 0.0,
    "end_a_height_agl_m": 0.0,
    "end_a_latitude": 0.0,
    "end_a_longitude": 0.0,
    "end_a_radio_id": "550e8400-e29b-41d4-a716-446655440000",
    "end_b_height_agl_m": 0.0,
    "end_b_latitude": 0.0,
    "end_b_longitude": 0.0,
    "end_b_radio_id": "550e8400-e29b-41d4-a716-446655440000",
    "frequency_mhz": 0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "notes": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "passes": true,
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "existing",
    "updated_at": "2026-01-15T19:44:25Z",
    "worst_clearance_pct": 0.0
  }
}

Qualify a point for service

GET /api/v1/rf/qualify

Ranks the top radios by composite-predicted RSSI at `sm_height` metres, runs exact path profiles for the top candidates, and reports the LOS/NLOS verdict, required mount height, downlink and uplink RSSI, and the expected MCS for each profiled candidate. Locate the point by `lat`/`lon` or by `address` (geocoded). A request needs exactly one of the two forms; the `sm_height` default is the planner's standard roofline assumption.

Requires scope rf:read

Parameters

address string in query
Free-form address, geocoded instead of `lat`/`lon`.
lat number in query
Point latitude, WGS84 decimal degrees. Requires `lon`.
lon number in query
Point longitude, WGS84 decimal degrees. Requires `lat`.
sm_height number in query
Receiver height above ground in metres (default 6).

Responses

200 application/json
Qualification result
400 application/json
Neither `lat`/`lon` nor `address` was usable
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
The address did not geocode
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/rf/qualify \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "address=VALUE" \
  --data-urlencode "lat=VALUE" \
  --data-urlencode "lon=VALUE" \
  --data-urlencode "sm_height=VALUE"

Response 200

{
  "data": {
    "address": "string",
    "candidates": [
      {
        "ap_label": "string",
        "distance_m": 0.0,
        "downlink_rssi_dbm": 0.0,
        "exact?": true,
        "frequency_mhz": 0,
        "mcs": "string",
        "name": "string",
        "probe_rssi_dbm": 0.0,
        "radio_id": "550e8400-e29b-41d4-a716-446655440000",
        "required_sm_height_m": 0.0,
        "serving?": true,
        "sm_label": "string",
        "sm_slug": "string",
        "throughput_mbps": 0.0,
        "uplink_rssi_dbm": 0.0,
        "verdict": "los",
        "worst_cause": "terrain",
        "worst_clearance_pct": 0.0
      }
    ],
    "composite_id": "550e8400-e29b-41d4-a716-446655440000",
    "composite_nlos": true,
    "composite_tier_m": 0.0,
    "latitude": 0.0,
    "longitude": 0.0,
    "provider": "google",
    "sm_height_m": 0.0
  }
}

List RF planning radios

GET /api/v1/rf/radios

Lists the authenticated organization's planner radios, one page at a time. `tower_id` narrows to one tower; `plan` scopes the list, `plan=none` is the running network, a plan id adds that plan's rows.

Requires scope rf:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`; carries page and page size, taking precedence over `page`/`per_page`.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.
plan string in query
`none` for the running network, a plan ID to add its rows.
tower_id string in query
Filter to one tower ID.

Responses

200 application/json
RF radios
400 application/json
The `cursor` parameter did not decode
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/rf/radios \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=VALUE" \
  --data-urlencode "per_page=VALUE" \
  --data-urlencode "plan=VALUE" \
  --data-urlencode "tower_id=VALUE"

Response 200

{
  "data": [
    {
      "antenna_slug": "string",
      "azimuth_deg": 0.0,
      "cable_loss_db": 0.0,
      "channel_width_mhz": 0,
      "device_id": "550e8400-e29b-41d4-a716-446655440000",
      "downtilt_deg": 0.0,
      "frequency_mhz": 0,
      "height_above_rooftop_m": 0.0,
      "height_agl_m": 0.0,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "name": "string",
      "organization_id": "550e8400-e29b-41d4-a716-446655440000",
      "plan_id": "550e8400-e29b-41d4-a716-446655440000",
      "radio_model_slug": "string",
      "role": "ptmp_ap",
      "status": "existing",
      "tower_id": "550e8400-e29b-41d4-a716-446655440000",
      "tx_power_dbm": 0.0,
      "updated_at": "2026-01-15T19:44:25Z"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Create an RF planning radio

POST /api/v1/rf/radios

Creates a planned radio on a tower of the same organization. The radio joins the tower's plan unless `plan_id` overrides it.

Requires scope rf:write

Request body

Radio attributes

radio object required
antenna_slug string
Antenna pattern key.
azimuth_deg number
Boresight azimuth in degrees true.
cable_loss_db number
Feeder loss in dB.
channel_width_mhz number
Channel width in MHz.
downtilt_deg number
Mechanical downtilt in degrees.
frequency_mhz number
Centre frequency in MHz.
height_above_rooftop_m number
Height above the rooftop in metres.
height_agl_m number
Mount height in metres.
name string
Radio name.
plan_id string
Plan to place the radio in; defaults to the tower's plan.
radio_model_slug string
RadioCatalog model key.
role string
Sector access point or point-to-point backhaul.
tower_id uuid
Tower to mount on.
tx_power_dbm number
Transmit power in dBm.

Responses

201 application/json
The created radio
400 application/json
The `radio`/`tower_id` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The tower is not in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/rf/radios \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "radio": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "channel_width_mhz": 0.0,
    "downtilt_deg": 0.0,
    "frequency_mhz": 0.0,
    "height_above_rooftop_m": 0.0,
    "height_agl_m": 0.0,
    "name": "string",
    "plan_id": "string",
    "radio_model_slug": "string",
    "role": "ptmp_ap",
    "tower_id": "550e8400-e29b-41d4-a716-446655440000",
    "tx_power_dbm": 0.0
  }
}'

Response 201

{
  "data": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "channel_width_mhz": 0,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "downtilt_deg": 0.0,
    "frequency_mhz": 0,
    "height_above_rooftop_m": 0.0,
    "height_agl_m": 0.0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "radio_model_slug": "string",
    "role": "ptmp_ap",
    "status": "existing",
    "tower_id": "550e8400-e29b-41d4-a716-446655440000",
    "tx_power_dbm": 0.0,
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Retrieve an RF planning radio

GET /api/v1/rf/radios/{id}

Gets one radio by ID.

Requires scope rf:read

Parameters

id string in path required
Radio ID.

Responses

200 application/json
The radio
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No radio with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/rf/radios/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "channel_width_mhz": 0,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "downtilt_deg": 0.0,
    "frequency_mhz": 0,
    "height_above_rooftop_m": 0.0,
    "height_agl_m": 0.0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "radio_model_slug": "string",
    "role": "ptmp_ap",
    "status": "existing",
    "tower_id": "550e8400-e29b-41d4-a716-446655440000",
    "tx_power_dbm": 0.0,
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Update an RF planning radio

PATCH PUT /api/v1/rf/radios/{id}

Updates the given attributes of a radio. Omitted attributes are left alone.

Requires scope rf:write

Parameters

id string in path required
Radio ID.

Request body

Radio attributes

radio object required
antenna_slug string
Antenna pattern key.
azimuth_deg number
Boresight azimuth in degrees true.
cable_loss_db number
Feeder loss in dB.
channel_width_mhz number
Channel width in MHz.
downtilt_deg number
Mechanical downtilt in degrees.
frequency_mhz number
Centre frequency in MHz.
height_above_rooftop_m number
Height above the rooftop in metres.
height_agl_m number
Mount height in metres.
name string
Radio name.
plan_id string
Plan to place the radio in; defaults to the tower's plan.
radio_model_slug string
RadioCatalog model key.
role string
Sector access point or point-to-point backhaul.
tower_id uuid
Tower to mount on.
tx_power_dbm number
Transmit power in dBm.

Responses

200 application/json
The updated radio
400 application/json
The `radio` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No radio with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/rf/radios/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "radio": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "channel_width_mhz": 0.0,
    "downtilt_deg": 0.0,
    "frequency_mhz": 0.0,
    "height_above_rooftop_m": 0.0,
    "height_agl_m": 0.0,
    "name": "string",
    "plan_id": "string",
    "radio_model_slug": "string",
    "role": "ptmp_ap",
    "tower_id": "550e8400-e29b-41d4-a716-446655440000",
    "tx_power_dbm": 0.0
  }
}'

Response 200

{
  "data": {
    "antenna_slug": "string",
    "azimuth_deg": 0.0,
    "cable_loss_db": 0.0,
    "channel_width_mhz": 0,
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "downtilt_deg": 0.0,
    "frequency_mhz": 0,
    "height_above_rooftop_m": 0.0,
    "height_agl_m": 0.0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "name": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "radio_model_slug": "string",
    "role": "ptmp_ap",
    "status": "existing",
    "tower_id": "550e8400-e29b-41d4-a716-446655440000",
    "tx_power_dbm": 0.0,
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Delete an RF planning radio

DELETE /api/v1/rf/radios/{id}

Deletes a radio and its coverage row.

Requires scope rf:write

Parameters

id string in path required
Radio ID.

Responses

200 application/json
The radio was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No radio with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/rf/radios/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

List RF planning towers

GET /api/v1/rf/towers

Lists the authenticated organization's planner towers, one page at a time. `status` filters `existing`/`planned`; `plan` scopes the list, `plan=none` is the running network (existing rows), a plan id adds that plan's rows; `search` matches names case-insensitively.

Requires scope rf:read

Parameters

cursor string in query
Opaque cursor from `meta.next_cursor`; carries page and page size, taking precedence over `page`/`per_page`.
page integer in query
1-based page number.
per_page integer in query
Page size. Defaults to 100 and is capped at 100.
plan string in query
`none` for the running network, a plan ID to add its rows.
search string in query
Name substring, case-insensitive.
status string in query
Filter: existing or planned.

Responses

200 application/json
RF towers
400 application/json
The `cursor` parameter did not decode
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -G https://staging.towerops.net/api/v1/rf/towers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "cursor=VALUE" \
  --data-urlencode "page=VALUE" \
  --data-urlencode "per_page=VALUE" \
  --data-urlencode "plan=VALUE" \
  --data-urlencode "search=VALUE" \
  --data-urlencode "status=VALUE"

Response 200

{
  "data": [
    {
      "ground_elevation_m": 0.0,
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "inserted_at": "2026-01-15T19:44:25Z",
      "latitude": 0.0,
      "longitude": 0.0,
      "name": "string",
      "notes": "string",
      "organization_id": "550e8400-e29b-41d4-a716-446655440000",
      "plan_id": "550e8400-e29b-41d4-a716-446655440000",
      "site_id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "existing",
      "structure_height_m": 0.0,
      "structure_type": "string",
      "updated_at": "2026-01-15T19:44:25Z"
    }
  ],
  "meta": {
    "next_cursor": null,
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1
  }
}

Create an RF planning tower

POST /api/v1/rf/towers

Creates a planned tower. `plan_id` may name the plan it belongs to; otherwise the organization's catch-all plan takes it.

Requires scope rf:write

Request body

Tower attributes

tower object required
latitude number required
Latitude in WGS84 degrees.
longitude number required
Longitude in WGS84 degrees.
name string required
Tower name.
notes string
Free-form notes.
plan_id string
Plan to place a planned tower in; defaults to the catch-all plan.
structure_height_m number
Structure height in metres.
structure_type string
Structure kind (monopole, lattice, guyed, rooftop, …).

Responses

201 application/json
The created tower
400 application/json
The `tower` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/rf/towers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "tower": {
    "latitude": 0.0,
    "longitude": 0.0,
    "name": "string",
    "notes": "string",
    "plan_id": "string",
    "structure_height_m": 0.0,
    "structure_type": "string"
  }
}'

Response 201

{
  "data": {
    "ground_elevation_m": 0.0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "latitude": 0.0,
    "longitude": 0.0,
    "name": "string",
    "notes": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "existing",
    "structure_height_m": 0.0,
    "structure_type": "string",
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Retrieve an RF planning tower

GET /api/v1/rf/towers/{id}

Gets one tower by ID.

Requires scope rf:read

Parameters

id string in path required
Tower ID.

Responses

200 application/json
The tower
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No tower with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/rf/towers/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "ground_elevation_m": 0.0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "latitude": 0.0,
    "longitude": 0.0,
    "name": "string",
    "notes": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "existing",
    "structure_height_m": 0.0,
    "structure_type": "string",
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Update an RF planning tower

PATCH PUT /api/v1/rf/towers/{id}

Updates the given attributes of a tower. Omitted attributes are left alone.

Requires scope rf:write

Parameters

id string in path required
Tower ID.

Request body

Tower attributes

tower object required
latitude number required
Latitude in WGS84 degrees.
longitude number required
Longitude in WGS84 degrees.
name string required
Tower name.
notes string
Free-form notes.
plan_id string
Plan to place a planned tower in; defaults to the catch-all plan.
structure_height_m number
Structure height in metres.
structure_type string
Structure kind (monopole, lattice, guyed, rooftop, …).

Responses

200 application/json
The updated tower
400 application/json
The `tower` parameter is missing
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No tower with that ID in this organization
422 application/json
Validation failed
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X PATCH https://staging.towerops.net/api/v1/rf/towers/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "tower": {
    "latitude": 0.0,
    "longitude": 0.0,
    "name": "string",
    "notes": "string",
    "plan_id": "string",
    "structure_height_m": 0.0,
    "structure_type": "string"
  }
}'

Response 200

{
  "data": {
    "ground_elevation_m": 0.0,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "inserted_at": "2026-01-15T19:44:25Z",
    "latitude": 0.0,
    "longitude": 0.0,
    "name": "string",
    "notes": "string",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "plan_id": "550e8400-e29b-41d4-a716-446655440000",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "existing",
    "structure_height_m": 0.0,
    "structure_type": "string",
    "updated_at": "2026-01-15T19:44:25Z"
  }
}

Delete an RF planning tower

DELETE /api/v1/rf/towers/{id}

Deletes a tower and its radios' linkage to it.

Requires scope rf:write

Parameters

id string in path required
Tower ID.

Responses

200 application/json
The tower was deleted
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No tower with that ID in this organization
422 application/json
The tower could not be deleted
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X DELETE https://staging.towerops.net/api/v1/rf/towers/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "deleted": true
  }
}

Wireless client count per device

GET /api/v1/wireless_clients/counts

Associated wireless clients per access point / radio, from one grouped query.

Requires scope rf:read

Responses

200 application/json
Counts
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/wireless_clients/counts \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "count": 0,
      "device_id": "550e8400-e29b-41d4-a716-446655440000"
    }
  ]
}

Reports

List reports

GET /api/v1/reports

Every report definition in the organization.

Requires scope reports:read

Responses

200 application/json
Reports
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/reports \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "enabled": true,
      "format": "string",
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "last_run_at": "2026-01-15T19:44:25Z",
      "last_run_status": "string",
      "name": "string",
      "report_type": "uptime_summary",
      "schedule": null
    }
  ]
}

Retrieve a report

GET /api/v1/reports/{id}

One report definition, with its last run time and outcome.

Requires scope reports:read

Parameters

id string in path required
Report ID.

Responses

200 application/json
Report
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No report with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/reports/$ID \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "enabled": true,
    "format": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "last_run_at": "2026-01-15T19:44:25Z",
    "last_run_status": "string",
    "name": "string",
    "report_type": "uptime_summary",
    "schedule": null
  }
}

Generate a report's CSV

GET /api/v1/reports/{id}/csv

Generates the report's CSV for its configured window and returns it as `text/csv`.

Requires scope reports:read

Parameters

id string in path required
Report ID.

Responses

200 text/csv
CSV
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
Revenue report requested by a user who may not see financials
404 application/json
No report with that ID in this organization
422 application/json
The report type cannot be generated
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/reports/$ID/csv \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

"string"

Run a report now

POST /api/v1/reports/{id}/run

Queues the report to run now, outside its schedule, and returns the report with `202`. The run emails its recipients, updates `last_run_at` / `last_run_status`, and fires the `report.ready` webhook. A second request while a run for the same report is still pending queues nothing and answers `409`, so retries cannot send the recipients the report twice.

Requires scope reports:write

Parameters

id string in path required
Report ID.

Responses

202 application/json
Queued
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No report with that ID in this organization
409 application/json
A run for this report is already queued
422 application/json
The report could not be queued
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl -X POST https://staging.towerops.net/api/v1/reports/$ID/run \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 202

{
  "data": {
    "enabled": true,
    "format": "string",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "last_run_at": "2026-01-15T19:44:25Z",
    "last_run_status": "string",
    "name": "string",
    "report_type": "uptime_summary",
    "schedule": null
  }
}

Status Page

Retrieve the status page

GET /api/v1/status_page

The organization's status page: slug, published state, display name, the banner status, components in display order, unresolved incidents and the 20 most recent resolved ones. Components include the effective maintenance overlay; maintenance incidents include their actual occurrence end time. `404` when the organization has none.

Requires scope status_pages:read

Responses

200 application/json
Status page
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
The organization has no status page
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/status_page \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "active_incidents": [
      {
        "body": "string",
        "component_id": "550e8400-e29b-41d4-a716-446655440000",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "maintenance_ends_at": "2026-01-15T19:44:25Z",
        "maintenance_window_id": "550e8400-e29b-41d4-a716-446655440000",
        "resolved_at": "2026-01-15T19:44:25Z",
        "severity": "string",
        "started_at": "2026-01-15T19:44:25Z",
        "status": "string",
        "title": "string"
      }
    ],
    "company_name": "string",
    "components": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "string",
        "position": 0,
        "status": "string"
      }
    ],
    "enabled": true,
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "overall_status": "string",
    "recent_incidents": [
      {
        "body": "string",
        "component_id": "550e8400-e29b-41d4-a716-446655440000",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "maintenance_ends_at": "2026-01-15T19:44:25Z",
        "maintenance_window_id": "550e8400-e29b-41d4-a716-446655440000",
        "resolved_at": "2026-01-15T19:44:25Z",
        "severity": "string",
        "started_at": "2026-01-15T19:44:25Z",
        "status": "string",
        "title": "string"
      }
    ],
    "slug": "string"
  }
}

Subscriber Impact

Subscriber impact of an alert

GET /api/v1/alerts/{id}/subscriber_impact

The subscriber impact snapshot stored on the alert when it opened: affected subscribers, monthly revenue at risk and the device/site breakdown. `impact` is `null` for alert types that carry none. Revenue (`total_mrr` and each level's `mrr`) is included only when the token's user may see financials in the organization (owner, admin, executive); otherwise it is `null`/absent, as in the UI.

Requires scope subscribers:read

Parameters

id string in path required
Alert ID.

Responses

200 application/json
Impact
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
404 application/json
No alert with that ID in this organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/alerts/$ID/subscriber_impact \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": {
    "alert_id": "550e8400-e29b-41d4-a716-446655440000",
    "alert_type": "string",
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "device_name": "string",
    "impact": null,
    "resolved_at": "2026-01-15T19:44:25Z",
    "site_id": "550e8400-e29b-41d4-a716-446655440000",
    "total_mrr": "string",
    "total_subscribers": 0,
    "triggered_at": "2026-01-15T19:44:25Z"
  }
}

Subscriber impact of every open outage

GET /api/v1/subscriber_impact/outages

Every unresolved `device_down` and `site_outage` alert in the organization with its subscriber impact, most subscribers first, the open outage set an integrator needs to answer "what is this costing right now".

Requires scope subscribers:read

Responses

200 application/json
Impacts
401 application/json
Missing, malformed, expired or revoked credentials
403 application/json
The token is valid but lacks the scope this endpoint requires, or the resource belongs to another organization
429 application/json
Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header

Request

curl https://staging.towerops.net/api/v1/subscriber_impact/outages \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response 200

{
  "data": [
    {
      "alert_id": "550e8400-e29b-41d4-a716-446655440000",
      "alert_type": "string",
      "device_id": "550e8400-e29b-41d4-a716-446655440000",
      "device_name": "string",
      "impact": null,
      "resolved_at": "2026-01-15T19:44:25Z",
      "site_id": "550e8400-e29b-41d4-a716-446655440000",
      "total_mrr": "string",
      "total_subscribers": 0,
      "triggered_at": "2026-01-15T19:44:25Z"
    }
  ]
}

Terraform Provider

Manage your TowerOps infrastructure as code using our official Terraform provider. Automate site and device provisioning, version control your network configuration, and integrate TowerOps into your existing infrastructure workflows.

Infrastructure as Code

The Terraform provider uses the same REST API documented above. All resource operations require API token authentication (not browser sessions).

What is Terraform?

Terraform is an infrastructure as code tool that lets you define both cloud and on-premise resources in human-readable configuration files that you can version, reuse, and share. With the TowerOps Terraform provider, you can:

  • Define sites and devices in declarative configuration files
  • Version control your network monitoring infrastructure
  • Automate device provisioning and updates
  • Integrate with CI/CD pipelines for automated deployments
  • Ensure consistent configuration across environments
  • Track infrastructure changes with git history

Getting Started

Installation

The provider is available on the Terraform Registry. Terraform will automatically download it when you run terraform init.

Requirements

  • Terraform >= 1.0
  • TowerOps API token (create in Account Settings → API Tokens)

Configuration

terraform {
  required_providers {
    towerops = {
      source  = "towerops-app/towerops"
      version = "~> 0.1"
    }
  }
}

variable "towerops_api_token" {
  type      = string
  sensitive = true
}

provider "towerops" {
  token   = var.towerops_api_token
  api_url = "https://staging.towerops.net"
}

Example Usage

This example creates a site and two devices. Terraform will automatically manage the lifecycle of these resources, creating, updating, or deleting them as needed to match your configuration.

Key Features

  • Automatic dependency management (devices depend on sites)
  • State tracking to detect configuration drift
  • Plan preview before making changes
  • Rollback support via version control

main.tf

resource "towerops_site" "main_office" {
  name     = "Main Office"
  location = "New York, NY"
}

resource "towerops_device" "core_router" {
  site_id      = towerops_site.main_office.id
  name         = "Core Router"
  ip_address   = "192.168.1.1"
  description  = "Primary gateway"
  snmp_enabled = true
  snmp_version = "2c"
}

resource "towerops_device" "backup_router" {
  site_id      = towerops_site.main_office.id
  name         = "Backup Router"
  ip_address   = "192.168.1.2"
  description  = "Failover gateway"
  snmp_enabled = true
}

Commands

# Initialize and download provider
terraform init

# Preview changes
terraform plan

# Apply configuration
terraform apply

# Destroy all resources
terraform destroy

Available Resources

towerops_site
Manages a physical site/location. Supports name, location, and SNMP community string configuration.
View documentation →
towerops_device
Manages network equipment at a site. Supports IP address, SNMP configuration, monitoring settings, and more.
View documentation →
towerops_check
Manages an HTTP, TCP, DNS or ping service check, attached to a device or standalone.
View documentation →
towerops_agent
Manages an agent token for a remote poller. The token is returned once, on create, and is sensitive.
View documentation →
towerops_schedule
Manages an on-call schedule, its rotation layers and overrides.
View documentation →
towerops_escalation_policy
Manages an alert escalation policy and the rules that decide who is notified.
View documentation →
towerops_maintenance_window
Manages a one-off or recurring maintenance window that suppresses alerting.
View documentation →
towerops_integration
Manages a third-party integration such as PagerDuty or NetBox.
View documentation →

Need Help?

For questions about the Terraform provider, please open an issue on GitHub or contact us at hi@towerops.net.