API reference
CivicFlow API
A predictable REST API and signed webhooks for incidents, teams, categories and residents — the same API our own apps are built on.
https://api.civicflow.site/v2 v2 · Stable OpenAPI 3.1 On this page
The API is organized around resources with predictable URLs. Requests and responses are JSON over HTTPS, timestamps are ISO 8601 in UTC, and every list endpoint uses the same cursor pagination.
Each workspace has a production and a sandbox environment with separate API keys. Sandbox data is never shown to residents.
- Protocol
- REST over HTTPS (TLS 1.3)
- Format
- JSON, UTF-8
- Specification
- OpenAPI 3.1
- Events
- Signed webhooks
openapi.yaml is available in your workspace under Settings → Developers.
Authenticate with a secret API key sent as a Bearer token. Admins create keys under Settings → Developers; each key has a name, an environment and a set of scopes, and can be rotated or revoked at any time.
curl https://api.civicflow.site/v2/incidents \ -H "Authorization: Bearer $CIVICFLOW_KEY" | Scope | Grants |
|---|---|
incidents:read | Read incidents, history and attachments metadata |
incidents:write | Create, update, assign and resolve incidents |
comments:write | Post internal notes and replies to residents |
attachments:write | Upload and delete attachments |
teams:read | Read teams, members and service areas |
categories:write | Create and update categories and SLA policies |
webhooks:manage | Create, test and delete webhook endpoints |
residents:read | Read and export resident profiles (personal data) |
residents:erase | Anonymize residents to fulfil erasure requests |
Limits apply per workspace and environment, with a short burst allowance on top. Every response includes your current budget; when you exceed it, the API returns 429 with a Retry-After header.
| Plan | Requests / minute | Burst | Access |
|---|---|---|---|
| Starter | 60 | 10 | Read-only endpoints |
| Growth | 600 | 100 | Read and write |
| Enterprise | from 3,000 | Custom | Read and write, custom limits |
Response headers
-
X-RateLimit-Limit - Requests allowed in the current window
-
X-RateLimit-Remaining - Requests left in the current window
-
X-RateLimit-Reset - Unix time when the window resets
-
Retry-After - Seconds to wait before retrying (on 429 only)
List endpoints return up to limit items (default 25, maximum 100) and a next_cursor. Pass it as cursor to fetch the next page; when has_more is false, you have reached the end. Cursors are stable while you page, even as new incidents arrive.
# First pagecurl "https://api.civicflow.site/v2/incidents?limit=50" \ -H "Authorization: Bearer $CIVICFLOW_KEY"# Next page: pass next_cursor from the previous responsecurl "https://api.civicflow.site/v2/incidents?limit=50&cursor=c_8f3a91d2" \ -H "Authorization: Bearer $CIVICFLOW_KEY" CivicFlow uses conventional HTTP status codes. Error bodies always include a machine-readable code, a human-readable message and a request_id to quote when you contact support.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request is malformed — e.g. invalid JSON or an unknown parameter. |
| 401 | unauthenticated | The API key is missing, revoked or invalid. |
| 403 | insufficient_scope | The key is valid but lacks the scope for this action. |
| 404 | not_found | The resource does not exist in this workspace. |
| 409 | conflict | The request conflicts with the current state — e.g. resolving a resolved incident. |
| 422 | validation_failed | Validation failed; see the field errors in details. |
| 429 | rate_limited | Rate limit exceeded; retry after the Retry-After interval. |
| 500 | internal_error | Something went wrong on our side. Safe to retry with backoff. |
{ "error": { "code": "validation_failed", "message": "location is required for category lighting", "details": [{ "field": "location", "issue": "missing" }], "request_id": "req_7c41e09b2a" }} The major version is part of the URL (/v2). Within a version we only make additive changes: new endpoints, new optional parameters, new fields and new event types. Build clients that ignore unknown fields.
- Breaking changes ship only in a new major version.
- Deprecated endpoints keep working for at least 12 months and return
DeprecationandSunsetheaders. - Admins are notified by email and in the workspace, and see deprecated calls in the API usage report.
- API v1 is deprecated and will be retired on 31 March 2027.
Every resource follows the same patterns. The incidents list is documented in full below; the rest is in openapi.yaml.
Incidents
- GET
/v2/incidentsList incidents, filtered and sorted Documented below - POST
/v2/incidentsCreate an incident on behalf of a resident or system - GET
/v2/incidents/{id}Retrieve an incident with its full history - PATCH
/v2/incidents/{id}Update status, priority, category or location - POST
/v2/incidents/{id}/assignAssign an incident to a team or person - POST
/v2/incidents/{id}/resolveResolve an incident with a note and proof photos
Comments
- GET
/v2/incidents/{id}/commentsList internal notes and resident replies - POST
/v2/incidents/{id}/commentsAdd an internal note or reply to the resident - DELETE
/v2/comments/{id}Delete an internal note
Attachments
- GET
/v2/incidents/{id}/attachmentsList photos and files on an incident - POST
/v2/incidents/{id}/attachmentsUpload a photo or file (up to 25 MB) - DELETE
/v2/attachments/{id}Delete an attachment
Teams
- GET
/v2/teamsList teams and field crews - GET
/v2/teams/{id}Retrieve a team with members and service area - PATCH
/v2/teams/{id}Update a team’s members, shifts or service area
Categories
- GET
/v2/categoriesList categories with their SLA policies - POST
/v2/categoriesCreate a category - PATCH
/v2/categories/{key}Update a category or its SLA policy
Webhooks
- GET
/v2/webhooksList webhook endpoints - POST
/v2/webhooksRegister a webhook endpoint for selected events - POST
/v2/webhooks/{id}/testSend a signed test event to an endpoint - DELETE
/v2/webhooks/{id}Delete a webhook endpoint
Residents
- GET
/v2/residents/{id}Retrieve a resident profile - GET
/v2/residents/{id}/exportExport everything held about a resident (access request) - POST
/v2/residents/{id}/eraseAnonymize a resident (erasure request)
GET /v2/incidents
Returns incidents in the workspace, newest first. Requires the incidents:read scope. Combine filters freely — they are joined with AND.
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: open, urgent, assigned or resolved. Comma-separated for several. |
category | string | Category key, e.g. lighting. |
district | string | District key, e.g. north. |
team | string | Only incidents assigned to this team id. |
updated_since | timestamp | Only incidents updated at or after this ISO 8601 timestamp. |
sort | string | created_at, -created_at (default), priority or sla_due_at. |
limit | integer | Items per page, 1–100. Defaults to 25. |
cursor | string | The next_cursor from a previous page. |
Example request
curl "https://api.civicflow.site/v2/incidents?status=open,urgent&district=north&limit=2" \ -H "Authorization: Bearer $CIVICFLOW_KEY"Example response
{ "data": [ { "id": "CF-4821", "title": "Streetlight outage", "category": "lighting", "status": "urgent", "priority": "high", "district": "north", "location": { "address": "Linden St & 4th Ave", "lat": 52.3791, "lng": 4.8994 }, "assigned_team": "elec-4", "created_at": "2026-10-08T09:12:44Z", "sla_due_at": "2026-10-08T21:12:00Z" }, { "id": "CF-4833", "title": "Hydrant leak", "category": "water", "status": "urgent", "priority": "high", "district": "north", "location": { "address": "Birch Ln", "lat": 52.3804, "lng": 4.9051 }, "assigned_team": null, "created_at": "2026-10-08T08:47:10Z", "sla_due_at": "2026-10-08T12:47:10Z" } ], "has_more": true, "next_cursor": "c_8f3a91d2"} Register an HTTPS endpoint and choose the events it receives. Deliveries are JSON POST requests; respond with any 2xx within 10 seconds. Failed deliveries are retried with exponential backoff for up to 24 hours, and every attempt is visible in the delivery log.
| Event | Sent when |
|---|---|
incident.created | A new incident is reported by a resident, staff member or integration. |
incident.assigned | An incident is assigned or reassigned to a team or person. |
incident.status_changed | An incident’s status changes. Includes previous_status. |
incident.resolved | An incident is resolved, with resolution note and proof photos. |
comment.created | An internal note or resident reply is added. |
Verify signatures
Every delivery carries a CivicFlow-Signature header with a timestamp and an HMAC-SHA256 of timestamp.body, signed with the endpoint’s secret. Verify it against the raw request body and reject deliveries older than five minutes.
import crypto from "node:crypto";// header: "t=1791460023,v1=5f2b…"; rawBody: the unparsed request bodyexport function verifySignature(header, rawBody, secret) { const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("="))); const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));} Official, typed clients that handle authentication, pagination and retries for you.
-
Node.js
3.4.0TypeScript types for every resource. Node 18 and later.
npm install @civicflow/node -
Python
1.2.1Sync and async clients with type hints. Python 3.9 and later.
pip install civicflow -
Go
Coming soonAn idiomatic Go module is in private beta with two customers.
-
1 October 2026
v2.8
Incremental sync for incidents
Added the
updated_sincefilter toGET /v2/incidents, andprevious_statustoincident.status_changedevents. -
9 September 2026
v2.7
Larger attachments
Attachments can now be up to 25 MB (previously 10 MB). Uploads over 5 MB use resumable upload sessions.
-
12 August 2026
v2.6
Residents API and Python SDK 1.0
New endpoints to export and anonymize residents for access and erasure requests. The Python SDK reached 1.0.
Building an integration?
Read the guides for setup and concepts, check live API availability, or review how we protect your data.