cnHeat-2.0-style automation hooks. Coverage prediction is now a
first-class API resource that Terraform / Ansible / cron can drive.
REST API (under /api/v1, Bearer-token auth scoped to one org)
- GET /coverages list
- POST /coverages create + auto-queue compute
- GET /coverages/:id read (includes status / progress_pct
for polling-based providers)
- PATCH /coverages/:id update editable fields
- DELETE /coverages/:id delete + cleanup
- POST /coverages/:id/recompute requeue, returns 202
- GET /coverages/:id/kmz Google Earth bundle (KML + PNG)
Each response includes the full resource — Terraform read-after-write
reconciles cleanly; status polling hits a stable JSON shape.
Email-on-complete (Towerops.Coverages.Notifier)
- Worker fires deliver_compute_complete on both :ready and :failed
paths. Sends a short text email to every member of the owning
organisation with a link to the show page (or the failure
reason). Mirrors cnHeat 2.0's per-project email alerts.
- Failures are non-fatal: a stuck SMTP relay never blocks the
underlying coverage record from transitioning state.
KMZ export
- Builds a flat KMZ in-memory via :zip.create — doc.kml +
overlay.png, with the LatLonBox set from the coverage's bbox.
XML special chars are escaped. 409 if not yet ready.
Tests (11 new, all green)
- Bearer-token auth happy path + 401 missing
- index / show / create / update / delete + 404 cross-org
- create returns status:'queued' confirming the auto-enqueue
- recompute returns 202 with status:'queued'
Plus docs/terraform-coverages.md showing how to drive the new API
from a third-party restapi provider until the first-party
terraform-provider-towerops gets a coverage resource.
3.2 KiB
Coverages over the Towerops API (Terraform-friendly)
Towerops exposes a resourceful REST API for RF coverage predictions
that's stable enough to drive from Terraform, Ansible, or any
infrastructure-as-code tool. All endpoints live under /api/v1/ and
use Bearer-token auth scoped to a single organization.
Authentication
Mint a token from Settings → API tokens. Send it as:
Authorization: Bearer towerops_<token>
The token is org-scoped — every endpoint reads/writes inside that org only.
Resources
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/coverages |
List all coverages in the org. |
| POST | /api/v1/coverages |
Create a coverage. The compute pipeline is queued automatically. |
| GET | /api/v1/coverages/:id |
Read a single coverage including current status / progress_pct. |
| PATCH | /api/v1/coverages/:id |
Update editable fields (does not auto-recompute). |
| DELETE | /api/v1/coverages/:id |
Delete a coverage and its on-disk rasters. |
| POST | /api/v1/coverages/:id/recompute |
Re-run the compute pipeline. Returns 202 with status: "queued". |
| GET | /api/v1/coverages/:id/kmz |
Download a KMZ overlay for Google Earth (returns 409 until status: "ready"). |
Create payload
{
"coverage": {
"name": "North sector 5 GHz",
"site_id": "1f4...uuid",
"antenna_slug": "rf-elements-twistport-symmetrical-horn-30",
"frequency_ghz": 5.8,
"tx_power_dbm": 22.0,
"height_agl_ft": 100,
"azimuth_deg": 0,
"downtilt_deg": 0,
"radius_mi": 4
}
}
Imperial inputs are accepted via the virtual fields shown above; the schema converts them to SI before validation.
Polling for :ready
Compute is asynchronous. After POST you'll receive status: "queued"; the worker transitions through "computing" to
"ready" (or "failed"). Terraform providers can poll with
GET /api/v1/coverages/:id until status == "ready" before
declaring the resource healthy.
Terraform example
A Terraform provider for Towerops isn't published yet, but a
hand-rolled restapi provider works today:
terraform {
required_providers {
restapi = { source = "Mastercard/restapi", version = "~> 1.18" }
}
}
provider "restapi" {
uri = "https://app.towerops.com"
write_returns_object = true
headers = {
Authorization = "Bearer ${var.towerops_token}"
Content-Type = "application/json"
}
}
resource "restapi_object" "north_sector" {
path = "/api/v1/coverages"
id_attribute = "id"
data = jsonencode({
coverage = {
name = "North sector 5 GHz"
site_id = var.site_id
antenna_slug = "rf-elements-twistport-symmetrical-horn-30"
frequency_ghz = 5.8
tx_power_dbm = 22.0
height_agl_ft = 100
azimuth_deg = 0
downtilt_deg = 0
radius_mi = 4
}
})
}
terraform apply creates the coverage; terraform destroy deletes
it. Updates flow through PATCH. Use terraform_data + a
provisioner if you need to wait on status = "ready" before
proceeding.