API Docs
Watchtower Public API Documentation
Overview
The Watchtower Public API allows you to programmatically access your incident and form data. This REST API provides secure access to submission and dispatch information, form definitions and form responses with flexible filtering and pagination options, and lets approved integrations submit form responses.
Base URL
https://api.yourwatchtower.com/apiAPI Versions
Endpoint | Status | Response Version | Timestamp Behavior |
|---|---|---|---|
| Recommended |
| ISO 8601 with the agency's DST-aware UTC offset |
| Legacy compatibility |
| Original beta timestamp and filter behavior |
All /v2 endpoints share one response version, returned in the version field of every response.
The legacy endpoint remains unchanged for existing integrations. It continues to return the historical timestamp representation and lowercase modifieddatetime field used by ISO responses. New integrations should use v2.
Authentication
The API uses client credentials authentication. You'll need to generate API credentials from your Watchtower account.
Generating API Credentials
- Log into your Watchtower account
- Navigate to Account Settings
- Go to the Security section
- Click Generate New API Credentials
- Provide a descriptive name (e.g., "Mobile App Integration")
- To let the integration submit form responses, check Allow form submissions. This can't be changed after the credentials are generated.
- Important: Copy and save the Client Secret immediately - it won't be shown again!
API credentials can only be used with the endpoints in this documentation.
Authentication Headers
Include these headers in all API requests:
X-Client-Id: your_client_id_here
X-Client-Secret: your_client_secret_hereEndpoints
GET /v2/incidents
Retrieves incident data with optional filtering and pagination.
Request Headers
Header | Required | Description |
|---|---|---|
| Yes | Your API client ID |
| Yes | Your API client secret |
Query Parameters
Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| string | No | Filter incidents from this instant (ISO 8601; include | |
| string | No | Filter incidents to this instant (ISO 8601; include | |
| string | No | Filter by incident category (partial match) | |
| boolean | No | false | Include incident notes in response |
| boolean | No | false | Include involved party information |
| string | No | Filter records modified since this instant (ISO 8601; include | |
| string | No | Exclusive upper bound for | |
| integer | No | 1000 | Number of records per page (max 1000) |
| integer | No | 0 | Number of records to skip |
| integer | No | Page number (alternative to offset) |
Example Requests
Basic Request
curl -X GET "https://api.yourwatchtower.com/api/v2/incidents?startDate=2024-01-01T00:00:00Z&endDate=2024-12-31T23:59:59Z&limit=100&includeNotes=true" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Delta Loading Request
curl -X GET "https://api.yourwatchtower.com/api/v2/incidents?modifiedDateTime=2024-01-15T10:30:00Z&modifiedBefore=2024-01-29T10:30:00Z&limit=100" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Unbounded modifiedDateTime polling is limited to the most recent 14 days. For
historical synchronization, provide a modifiedBefore timestamp no more than 14
days after modifiedDateTime. The upper bound is exclusive, so adjacent windows
can be processed without overlap:
curl -X GET "https://api.yourwatchtower.com/api/v2/incidents?modifiedDateTime=2024-01-01T00:00:00Z&modifiedBefore=2024-01-15T00:00:00Z&limit=1000" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Response Format
{
"data": [
{
"incidentDateTime": "2024-06-15T14:30:00.000-07:00",
"involved": 2,
"creationDateTime": "2024-06-15T14:32:00.000-07:00",
"modifiedDateTime": "2024-06-15T14:35:00.000-07:00",
"category": "Water Rescue",
"submittedBy": "John Smith",
"callNumber": "SD24-001234",
"lat": 32.7157,
"lng": -117.1611,
"location": "Mission Beach",
"resource": ["Lifeguard Unit 1", "Rescue Boat 2"],
"responders": ["John Smith", "Jane Doe"],
"airTemp": 72,
"tideHeight": 4.2,
"waveHeight": 3,
"waveInterval": 12,
"waterTemp": 68,
"agencyId": "agency_123",
"agency": "San Diego Lifeguards",
"notes": ["Initial response notes", "Follow-up information"],
"involvedParties": ["Individual - 25 - Male - Local"]
}
],
"version": "2.0.0-beta.3",
"pagination": {
"totalCount": 1250,
"hasNextPage": true,
"hasPreviousPage": false,
"currentPage": 1,
"totalPages": 13,
"limit": 100,
"offset": 0
}
}Response Fields
Field | Type | Description |
|---|---|---|
| string | When the incident occurred (ISO 8601 in the agency timezone) |
| number | Number of people involved |
| string | When the record was created (ISO 8601 in the agency timezone) |
| string | When the record was last modified (ISO 8601 in the agency timezone) |
| string | Incident category/type |
| string | Name of person who submitted the report |
| string | Unique call/incident number |
| number | Latitude coordinate (5 decimal places) |
| number | Longitude coordinate (5 decimal places) |
| string | Location name/description |
| array | Equipment/units used in response |
| array | Personnel who responded |
| number | Air temperature (°F or °C based on agency settings) |
| number | Tide height (feet or meters based on agency settings) |
| number | Wave height (feet or meters based on agency settings) |
| number | Wave interval (seconds) |
| number | Water temperature (°F or °C based on agency settings) |
| string | Your agency ID |
| string | Your agency name |
| array | Incident notes (if |
| array | Involved party details (if |
Note: Temperature and distance measurements are automatically returned in your agency's preferred units (Imperial or Metric) as configured in your account settings.
Timestamp Handling
Response timestamps include the agency's UTC offset and automatically reflect daylight saving time. For example, an America/Los_Angeles timestamp uses -08:00 during standard time and -07:00 during daylight time. A trailing Z is only used for UTC.
Timestamp filters are normalized to UTC before comparison. Include either Z or an explicit numeric offset in filter values. Offset-less values are interpreted as UTC for backward compatibility.
The forms endpoints follow the same timestamp handling.
Forms Endpoints
The forms endpoints let you read form definitions and form responses, and submit new form responses. They use the same authentication headers as the incidents endpoint.
- Access: Requests act as the Watchtower user who created the API credentials. You can only reach forms that user can open in Watchtower. If that user is archived, their credentials stop working.
- Excluded forms: Medical forms and supervisor review forms are not available through the API. They are not listed, and requests for them return
404. - Submitting: Requires API credentials generated with Allow form submissions checked. Other credentials receive
403. - Attribution: Submitted responses show the credential's creator as the submitter and record which credentials sent them. In Watchtower, these responses show an API badge with the credential name.
- Retries: Submissions are not idempotent. Retrying a
POSTafter a timeout can create a duplicate response, so checkGET /v2/forms/{formId}/responsesbefore retrying. - Conventions: Timestamps, delta loading (
modifiedDateTime/modifiedBefore), pagination and the response format work the same way as/v2/incidents.
Method | Endpoint | Description |
|---|---|---|
GET |
| List the forms you can access |
GET |
| Retrieve a form's fields and questions |
GET |
| List valid records for a Personnel, Resource or Asset question |
GET |
| List responses for a form |
GET |
| Retrieve a single form response |
POST |
| Submit a form response |
All forms endpoints use the same request headers as /v2/incidents:
Header | Required | Description |
|---|---|---|
| Yes | Your API client ID |
| Yes | Your API client secret |
GET /v2/forms
Retrieves the forms you can access, sorted by name.
Query Parameters
Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| string | No | Filter forms modified since this instant (ISO 8601; include | |
| string | No | Exclusive upper bound for | |
| integer | No | 100 | Number of records per page (max 100) |
| integer | No | 0 | Number of records to skip |
| integer | No | Page number (alternative to offset) |
Example Request
curl -X GET "https://api.yourwatchtower.com/api/v2/forms?limit=100" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Response Format
{
"data": [
{
"id": "form_1",
"name": "Rescue Report",
"modifiedDateTime": "2024-06-15T10:00:00.000-07:00",
"requiresIncident": false,
"submittable": true
}
],
"version": "2.0.0-beta.3",
"pagination": {
"totalCount": 24,
"hasNextPage": false,
"hasPreviousPage": false,
"currentPage": 1,
"totalPages": 1,
"limit": 100,
"offset": 0
}
}Response Fields
Field | Type | Description |
|---|---|---|
| string | Form ID |
| string | Form name |
| string | When the form was last modified (ISO 8601 in the agency timezone) |
| boolean | Whether responses must be attached to an incident ( |
| boolean | Whether the form can be submitted through the API (workflow forms cannot be) |
GET /v2/forms/{formId}
Retrieves a form's fields and questions. Use the field and question IDs when submitting responses.
Example Request
curl -X GET "https://api.yourwatchtower.com/api/v2/forms/form_1" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Response Format
{
"data": {
"id": "form_1",
"name": "Rescue Report",
"modifiedDateTime": "2024-06-15T10:00:00.000-07:00",
"requiresIncident": false,
"submittable": true,
"fields": [
{
"id": "field_1",
"title": "Details",
"repeatable": false,
"items": [
{
"id": "item_1",
"label": "Cause",
"type": "text",
"format": null,
"required": true,
"multiple": false,
"sensitive": false,
"submittable": true,
"options": ["Rip current", "Other"],
"allowOther": false
}
]
}
]
},
"version": "2.0.0-beta.3"
}Response Fields
The form-level fields are the same as GET /v2/forms, plus:
Field | Type | Description |
|---|---|---|
| array | Form fields (sections) in display order |
| string | Field ID |
| string | Field title |
| boolean | Whether the field can appear more than once in a response |
| array | Questions in the field |
| string | Question ID |
| string | Question label |
| string |
|
| string | Display format, such as |
| boolean | Whether an answer is required |
| boolean | Whether the question accepts multiple answers |
| boolean | Whether answers are sensitive (always redacted in responses) |
| boolean | Whether the API can answer the question (files, addresses, signatures and diagrams cannot be answered yet) |
| array | Allowed answers for |
| boolean | Whether a |
GET /v2/forms/{formId}/items/{itemId}/options
Retrieves the records a Personnel (users), Resource/Location (positions) or Asset (assets) question accepts. Use these IDs when submitting answers. Other question types return 400.
Example Request
curl -X GET "https://api.yourwatchtower.com/api/v2/forms/form_1/items/item_2/options" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Response Format
{
"data": [
{ "id": "user_2", "name": "Jane Doe" },
{ "id": "user_3", "name": "John Smith" }
],
"version": "2.0.0-beta.3"
}GET /v2/forms/{formId}/responses
Retrieves a form's responses with optional filtering and pagination, newest first.
Query Parameters
Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| string | No | Filter responses created from this instant (ISO 8601; include | |
| string | No | Filter responses created to this instant (ISO 8601; include | |
| string | No | Filter responses modified since this instant (ISO 8601; include | |
| string | No | Exclusive upper bound for | |
| string | No | Only include responses attached to this incident | |
| boolean | No | false | Include draft responses |
| integer | No | 50 | Number of records per page (max 100) |
| integer | No | 0 | Number of records to skip |
| integer | No | Page number (alternative to offset) |
The modifiedDateTime / modifiedBefore rules match /v2/incidents: unbounded polling is limited to the most recent 14 days, and historical windows may not exceed 14 days.
Example Requests
Basic Request
curl -X GET "https://api.yourwatchtower.com/api/v2/forms/form_1/responses?startDate=2024-01-01T00:00:00Z&endDate=2024-12-31T23:59:59Z&limit=50" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Delta Loading Request
curl -X GET "https://api.yourwatchtower.com/api/v2/forms/form_1/responses?modifiedDateTime=2024-01-15T10:30:00Z&modifiedBefore=2024-01-29T10:30:00Z&limit=100" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"Response Format
{
"data": [
{
"id": "response_1",
"formId": "form_1",
"formName": "Rescue Report",
"submissionId": null,
"submittedBy": { "id": "user_1", "name": "Jane Smith" },
"draft": false,
"creationDateTime": "2024-06-15T14:32:00.000-07:00",
"modifiedDateTime": "2024-06-15T14:35:00.000-07:00",
"answers": [
{
"fieldId": "field_1",
"fieldTitle": "Details",
"items": [
{
"itemId": "item_1",
"label": "Cause",
"type": "text",
"value": "Rip current"
},
{
"itemId": "item_2",
"label": "Crew",
"type": "users",
"value": [{ "id": "user_2", "name": "Jane Doe" }]
},
{
"itemId": "item_3",
"label": "SSN",
"type": "text",
"value": { "redacted": true }
}
]
}
]
}
],
"version": "2.0.0-beta.3",
"pagination": {
"totalCount": 120,
"hasNextPage": true,
"hasPreviousPage": false,
"currentPage": 1,
"totalPages": 3,
"limit": 50,
"offset": 0
}
}Response Fields
Field | Type | Description |
|---|---|---|
| string | Form response ID |
| string | Form ID |
| string | Form name |
| string | Incident the response is attached to, or |
| object | Person who submitted the response ( |
| boolean | Whether the response is a draft |
| string | When the response was created (ISO 8601 in the agency timezone) |
| string | When the response was last modified (ISO 8601 in the agency timezone) |
| array | One entry per field in the response (repeatable fields can appear more than once) |
| array | Answers to the field's questions ( |
Answer value formats:
Question type | Value |
|---|---|
| String, or an array of strings for multiple answers |
| Number, or an array of numbers for multiple answers |
| ISO 8601 in the agency timezone |
|
|
| Array of records ( |
| Array of addresses ( |
| Array of files ( |
Any sensitive question |
|
GET /v2/form-responses/{responseId}
Retrieves a single form response. The response fields are the same as GET /v2/forms/{formId}/responses.
curl -X GET "https://api.yourwatchtower.com/api/v2/form-responses/response_1" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret"{
"data": {
"id": "response_1",
"formId": "form_1",
"...": "same fields as above"
},
"version": "2.0.0-beta.3"
}POST /v2/forms/{formId}/responses
Submits a new form response. Requires API credentials generated with Allow form submissions checked. Include the Content-Type: application/json header.
Request Body
Field | Type | Required | Description |
|---|---|---|---|
| array | Yes | One entry per field. Repeat a field only if it is |
| string | Yes | Field ID from |
| array | Yes | Answers to the field's questions |
| string | Yes | Question ID |
| varies | Yes | Answer value (see below) |
| string | No | Incident to attach the response to; required when the form has |
| string | No | Backdates the response (ISO 8601; include |
Answer value formats:
Question type | Value |
|---|---|
| String, or an array of strings for multiple answers. Must match |
| Number, or an array of numbers for multiple answers |
| ISO 8601 string |
|
|
| Array of records, for example |
Example Request
curl -X POST "https://api.yourwatchtower.com/api/v2/forms/form_1/responses" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret" \
-H "Content-Type: application/json" \
-d '{
"submissionId": "optional_incident_id",
"creationDateTime": "2024-06-15T14:30:00-07:00",
"answers": [
{
"fieldId": "field_1",
"items": [
{ "itemId": "item_1", "value": "Rip current" },
{ "itemId": "item_2", "value": [{ "id": "user_2" }] }
]
}
]
}'Response
A successful submission returns 201 with the created response in data, in the same format as GET /v2/form-responses/{responseId}. Invalid answers return 422 with a message naming the field and question:
{
"error": "answers[0] question item_1: value is not one of the question's options"
}Pagination
The API supports two pagination methods:
Method 1: Offset-based
GET /api/v2/incidents?limit=100&offset=200Method 2: Page-based
GET /api/v2/incidents?limit=100&page=3The page parameter automatically calculates the offset: offset = (page - 1) * limit
The forms list endpoints (/v2/forms and /v2/forms/{formId}/responses) support the same pagination methods and return the same pagination object.
Error Responses
Status Code | Description |
|---|---|
400 | Bad Request - Invalid parameters |
401 | Unauthorized - Invalid credentials |
403 | Forbidden - Access denied, credentials not permitted for the endpoint, or rate limit exceeded |
404 | Not Found - Form or response not found (forms endpoints) |
413 | Payload Too Large - Body over 1 MB (form submissions) |
422 | Unprocessable - Invalid answers in a form submission |
429 | Too Many Requests - Form submission rate limit exceeded |
500 | Internal Server Error |
Error Response Format
{
"error": "Invalid client credentials"
}Rate Limits
- Maximum 1000 records per request for incidents, and 100 for the forms endpoints
- Form submissions are limited to 10 requests per second (bursts of 20) across all API clients; requests over the limit return
429 - API credentials can be disabled if misused
Security Best Practices
- Store credentials securely - Never commit API credentials to version control
- Use environment variables - Store credentials in environment variables or secure configuration
- Rotate credentials regularly - Generate new credentials periodically
- Monitor usage - Check your API credential status in Account Settings
- Disable unused credentials - Disable credentials that are no longer needed
Code Examples
JavaScript/Node.js
const axios = require('axios')
const apiClient = axios.create({
baseURL: 'https://api.yourwatchtower.com/api',
headers: {
'X-Client-Id': process.env.WATCHTOWER_CLIENT_ID,
'X-Client-Secret': process.env.WATCHTOWER_CLIENT_SECRET
}
})
async function getIncidents(params = {}) {
try {
const response = await apiClient.get('/v2/incidents', { params })
return response.data
} catch (error) {
console.error('API Error:', error.response?.data || error.message)
throw error
}
}
// Initial load - get all data
async function initialLoad() {
const data = await getIncidents({
startDate: '2024-01-01T00:00:00Z',
endDate: '2024-12-31T23:59:59Z',
limit: 1000,
includeNotes: true
})
console.log(`Initial load: ${data.data.length} incidents`)
// Store the timestamp for delta loading
const lastSyncTime = new Date().toISOString()
return { data, lastSyncTime }
}
// Delta load - get only updated records
async function deltaLoad(lastSyncTime) {
const data = await getIncidents({
modifiedDateTime: lastSyncTime,
limit: 1000,
includeNotes: true
})
console.log(`Delta load: ${data.data.length} new/updated incidents`)
return data
}
Python
import requests
import os
from datetime import datetime
class WatchtowerAPI:
def __init__(self):
self.base_url = 'https://api.yourwatchtower.com/api'
self.headers = {
'X-Client-Id': os.getenv('WATCHTOWER_CLIENT_ID'),
'X-Client-Secret': os.getenv('WATCHTOWER_CLIENT_SECRET')
}
def get_incidents(self, **params):
response = requests.get(
f'{self.base_url}/v2/incidents',
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json()
def delta_sync(self, last_sync_time):
"""Get incidents modified since last sync"""
return self.get_incidents(
modifiedDateTime=last_sync_time,
limit=1000
)
# Usage
api = WatchtowerAPI()
# Initial load
initial_data = api.get_incidents(
startDate='2024-01-01T00:00:00Z',
endDate='2024-12-31T23:59:59Z',
limit=1000,
includeNotes=True
)
print(f"Initial load: {len(initial_data['data'])} incidents")
# Store sync timestamp
last_sync = datetime.utcnow().isoformat() + 'Z'
# Later - delta load
delta_data = api.delta_sync(last_sync)
print(f"Delta load: {len(delta_data['data'])} new/updated incidents")
cURL
#!/bin/bash
CLIENT_ID="your_client_id"
CLIENT_SECRET="your_client_secret"
# Initial load
echo "Performing initial load..."
curl -X GET "https://api.yourwatchtower.com/api/v2/incidents?startDate=2024-01-01T00:00:00Z&limit=1000" \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-Client-Secret: $CLIENT_SECRET" \
-H "Accept: application/json"
# Delta load (replace timestamp with your last sync time)
echo "Performing delta load..."
curl -X GET "https://api.yourwatchtower.com/api/v2/incidents?modifiedDateTime=2024-01-15T10:30:00Z&modifiedBefore=2024-01-29T10:30:00Z&limit=1000" \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-Client-Secret: $CLIENT_SECRET" \
-H "Accept: application/json"
# Form responses modified in a window (replace FORM_ID)
echo "Loading modified form responses..."
curl -X GET "https://api.yourwatchtower.com/api/v2/forms/FORM_ID/responses?modifiedDateTime=2024-01-15T10:30:00Z&modifiedBefore=2024-01-29T10:30:00Z&limit=100" \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-Client-Secret: $CLIENT_SECRET" \
-H "Accept: application/json"Rate Limiting
IP-based rate limiting is enforced through AWS WAF:
- Public Incidents API: 100 requests per 5-minute window per IP address
- Aggregation: Custom keys (IP address + HTTP method)
- Action: Block requests exceeding limit
- Response: HTTP 403 (Forbidden)
Forms endpoints:
- Reads (
GET): 300 requests per 5-minute window per IP address - Submissions (
POST): 60 requests per 5-minute window per IP address - Aggregation: IP address
- Action: Block requests exceeding limit
- Response: HTTP 403 (Forbidden)
Support
For API support, contact:
- Email: support@yourwatchtower.com
- Phone: 714-316-9734
- Documentation: https://knowledge.yourwatchtower.com/en/
Changelog
Version 2.0.0-beta.3
- Added forms endpoints: list forms, retrieve a form's fields and questions, list valid options for Personnel, Resource and Asset questions, list and retrieve form responses, and submit form responses
- Added the Allow form submissions option when generating API credentials
- Medical and supervisor review forms are not available through the API
- All
/v2endpoints now share one response version
Version 2.0.0-beta.2
- Added the optional
modifiedBeforeexclusive upper bound for historical delta synchronization - Limited unbounded
modifiedDateTimelookbacks and bounded modification windows to 14 days
Version 2.0.0-beta.1
- Added the versioned
/api/v2/incidentsendpoint while preserving/api/incidentsbehavior - Fixed response timestamps that previously represented agency-local time with an incorrect trailing
Z - Response timestamps now include the agency's DST-aware UTC offset
- Fixed ISO 8601 request filters to honor
Zand explicit numeric offsets - Standardized the
modifiedDateTimeresponse field casing (previouslymodifieddatetimein ISO responses)
Version 1.0.0-beta.4
- Added
modifiedDateTimeparameter for delta loading and incremental data synchronization - Supports both standalone delta loading and combined filtering with date ranges
Version 1.0.0-beta.3
- Improved property naming for consistency and clarity (incidentTime -> incidentDateTime)
Version 1.0.0-beta.2
- Time values now returned in ISO 8601 format
- Includes modifiedDateTime field for accurate change tracking
- Improved property naming for consistency and clarity
- Consistent camelCasing on response object
Version 1.0.0-beta.1
- Initial release with incident data access
- Support for filtering by date range and category
- Pagination support
- Optional notes and involved parties data
- Automatic unit conversion based on agency preferences
Updated on: 07/10/2026
Thank you!