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/sitesLists 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/sitesCreates 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/devicesLists 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/devicesCreates 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/bulkApplies 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}/checksLists 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}/interfacesLists 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}/metricsReturns 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/checksLists 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/checksCreates 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/alertsLists 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}/acknowledgeMarks 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}/escalatePages 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}/notesRecords 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}/resolveMarks 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}/snoozeSilences 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/agentsLists 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/agentsCreates 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/organizationReturns 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/organizationUpdates 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/membersLists 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/invitationsLists 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/invitationsInvites 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/integrationsLists 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/integrationsConfigures 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}/testRuns 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/coveragesLists 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/coveragesCreates 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}/kmzReturns 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}/recomputeRe-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/activityReturns 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/customersOne 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}/traceThe 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
}
}
Search for a trace subject
GET /api/v1/trace/searchThe `/trace` page's search: subscriber accounts (by name, account id or phone number), inventory items, sites, devices and access points matching `q`, at most 25. A query shorter than two characters matches nothing. An account result that is mirrored in the customer registry carries its `customer_id`, which `GET /api/v1/customers/{id}/trace` takes.
Requires scope
subscribers:read
Parameters
- q string in query required
- Name, account id, phone number, IP, MAC or serial.
Responses
- 200 application/json
- Matches
- 400 application/json
- The `q` 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
- 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/trace/search \
-H "Authorization: Bearer YOUR_API_TOKEN" \
--data-urlencode "q=VALUE"
Response 200
{
"data": [
{
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"id": "550e8400-e29b-41d4-a716-446655440000",
"label": "string",
"matched_by": "phone",
"sublabel": "string",
"type": "account"
}
]
}
Maintenance Windows
Planned windows that suppress alerting.
List maintenance windows
GET /api/v1/maintenance_windowsLists 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_windowsCreates 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/schedulesLists 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/schedulesCreates 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}/layersAdds 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}/membersAdds 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_callResolves 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}/overridesPuts 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_policiesLists 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_policiesCreates 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}/rulesAdds 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}/targetsAttaches 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_endpointsLists 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_endpointsSubscribes 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}/deliveriesLists 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}/replayRe-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}/testQueues 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_eventsEvery 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-releaseInbound 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}/dhcpInbound 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}/pppoeInbound 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/slackSlack 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/stripeInbound 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}/acknowledgeAcknowledges 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}/notesAppends 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/accountPermanently 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/confirmationEmails 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/loginEmail 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/totpSecond 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/completeConsumes 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/verifyChecks 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/registerCreates 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/sessionReturns 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/sessionUpdates 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/sessionLogs 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}/briefEverything 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/organizationsLists 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}/alertsLists 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}/devicesLists 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}/sitesLists 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/dataReturns 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.
Exchange credentials or an authorization code for an access token
POST /api/oauth/tokenThe 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.jsonThe 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/apiThis 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_tokensThe 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_tokensMints 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_backupsEvery 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_backupsUp 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_backupsQueues 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/exportUp 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/candidatesLists 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/addCreates 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/ignoreRecords 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
List RF planning links
GET /api/v1/rf/linksLists the authenticated organization's point-to-point links, one page at a time. `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.
Responses
- 200 application/json
- RF links
- 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/links \
-H "Authorization: Bearer YOUR_API_TOKEN" \
--data-urlencode "cursor=VALUE" \
--data-urlencode "page=VALUE" \
--data-urlencode "per_page=VALUE" \
--data-urlencode "plan=VALUE"
Response 200
{
"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
}
],
"meta": {
"next_cursor": null,
"page": 1,
"per_page": 50,
"total_count": 1,
"total_pages": 1
}
}
Create an RF planning link
POST /api/v1/rf/linksCreates 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
}
}
Retrieve an RF planning link
GET /api/v1/rf/links/{id}Gets one link by ID, including its persisted pass/fail summary.
Requires scope
rf:read
Parameters
- id string in path required
- Link ID.
Responses
- 200 application/json
- The link
- 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 link 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/links/$ID \
-H "Authorization: Bearer YOUR_API_TOKEN"
Response 200
{
"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
}
}
Update an RF planning link
PATCH PUT /api/v1/rf/links/{id}Updates the given attributes of a link and refreshes its persisted profile summary. Omitted attributes are left alone.
Requires scope
rf:write
Parameters
- id string in path required
- Link ID.
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
- 200 application/json
- The updated 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
- 404 application/json
- No link 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/links/$ID \
-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 200
{
"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
}
}
Delete an RF planning link
DELETE /api/v1/rf/links/{id}Deletes a link.
Requires scope
rf:write
Parameters
- id string in path required
- Link ID.
Responses
- 200 application/json
- The link 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 link 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/links/$ID \
-H "Authorization: Bearer YOUR_API_TOKEN"
Response 200
{
"data": {
"deleted": true
}
}
Qualify a point for service
GET /api/v1/rf/qualifyRanks 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/radiosLists 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/radiosCreates 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/towersLists 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/towersCreates 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
}
}
List RF link health
GET /api/v1/rf_linksEvery associated wireless client in the organization with its latest signal, SNR, rates and a health class (`healthy`, `degraded`, `critical`, `unknown`), weakest signal first. `health` narrows the list.
Requires scope
rf:read
Parameters
- health string in query
- Only links in this health class: `healthy`, `degraded` or `critical`.
Responses
- 200 application/json
- RF links
- 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_links \
-H "Authorization: Bearer YOUR_API_TOKEN" \
--data-urlencode "health=VALUE"
Response 200
{
"data": [
{
"device_id": "550e8400-e29b-41d4-a716-446655440000",
"device_name": "string",
"distance": 0.0,
"health": "healthy",
"hostname": "string",
"id": "550e8400-e29b-41d4-a716-446655440000",
"ip_address": "string",
"last_seen_at": "2026-01-15T19:44:25Z",
"mac_address": "string",
"rx_rate": 0.0,
"signal_strength": 0.0,
"snr": 0.0,
"tx_rate": 0.0,
"uptime_seconds": 0
}
]
}
Wireless client count per device
GET /api/v1/wireless_clients/countsAssociated 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/reportsEvery 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}/csvGenerates 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}/runQueues 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_pageThe 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_impactThe 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/outagesEvery 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 →
Full Documentation
Need Help?
For questions about the Terraform provider, please open an issue on GitHub or contact us at hi@towerops.net.