API best practices¶
Enterprise feature
The REST API is available on Enterprise plans. Compare plans to find the right fit for your team.
This guide covers proven patterns for building robust, production-quality integrations with the FSM Navigator API. Whether you are syncing data from a CRM, dispatching jobs from an ERP, or feeding a data warehouse, these practices will help you build integrations that are reliable, efficient, and easy to maintain.
Request chaining¶
Many real-world workflows require multiple API calls in sequence, where each step uses data returned by the previous one. Here are the most common chaining patterns.
Create a customer → location → job¶
The most common workflow: onboard a new customer and schedule their first job.
# Step 1 — Create the customer
curl -X POST "https://fsmnavigator.com/api/v1/customers" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_name": "Acme Corp",
"contact_email": "[email protected]",
"contact_phone": "+15551234567"
}'
# Response → { "success": true, "customer": { "id": 123, ... } }
# Step 2 — Create a location for the customer (use customer ID from step 1)
curl -X POST "https://fsmnavigator.com/api/v1/customers/123/locations" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"location_name": "Acme HQ",
"street": "100 Main Street",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}'
# Response → { "success": true, "location": { "id": 456, ... } }
# Step 3 — Create a job at that location (use location ID from step 2)
curl -X POST "https://fsmnavigator.com/api/v1/jobs" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_title": "Initial HVAC inspection",
"job_description": "Annual system check for new client",
"job_priority": "Medium",
"customer_location_id": 456
}'
# Response → { "success": true, "job": { "id": 789, ... } }
Save each ID
Always capture the id from every response before proceeding to the next step. If any step fails, you can retry from that point without duplicating earlier work.
Look up a customer → get locations → create a job¶
When the customer already exists, look them up before creating a job.
# Step 1 — Find the customer
curl "https://fsmnavigator.com/api/v1/customers?search=acme" \
-H "X-API-Key: YOUR_API_KEY"
# Response → { "success": true, "customers": [{ "id": 123, ... }] }
# Step 2 — List their locations
curl "https://fsmnavigator.com/api/v1/customers/123/locations" \
-H "X-API-Key: YOUR_API_KEY"
# Response → { "success": true, "locations": [{ "id": 456, ... }] }
# Step 3 — Create a job at the first location
curl -X POST "https://fsmnavigator.com/api/v1/jobs" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_title": "Emergency plumbing repair",
"job_priority": "High",
"customer_location_id": 456
}'
Create an asset → log service → transfer¶
Track a new asset from procurement through deployment.
# Step 1 — Create the asset
curl -X POST "https://fsmnavigator.com/api/v1/assets" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"asset_name": "Carrier AC Unit 24ACC636",
"asset_type_id": 5,
"serial_number": "SN-2026-00142",
"status": "Pending Install"
}'
# Response → { "success": true, "asset": { "id": 1001, ... } }
# Step 2 — Log an initial service record
curl -X POST "https://fsmnavigator.com/api/v1/assets/1001/service-records" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"service_type": "Installation",
"notes": "Pre-deployment inspection passed",
"service_date": "2026-02-24"
}'
# Step 3 — Transfer the asset to a field location
curl -X POST "https://fsmnavigator.com/api/v1/assets/1001/transfer" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to_location_id": 456,
"notes": "Deployed to Acme HQ"
}'
Error handling¶
Always check the response¶
Every response includes a success field. Never assume a 200 status alone means success — always check the body too.
import requests
response = requests.post(
"https://fsmnavigator.com/api/v1/jobs",
headers={"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"},
json={"job_title": "Repair furnace", "customer_location_id": 456}
)
data = response.json()
if not data.get("success"):
print(f"Error: {data.get('error')} — {data.get('message')}")
# Log the failure and handle accordingly
else:
job_id = data["job"]["id"]
print(f"Job created: {job_id}")
Retry strategy¶
Not all errors should be retried. Follow these rules:
| Status code | Should you retry? | Strategy |
|---|---|---|
| 400 | No | Fix the request — the input is invalid |
| 401 | No | Check your API key |
| 403 | No | Check your key's scopes or IP whitelist |
| 404 | No | Verify the resource ID |
| 409 | No | Resolve the conflict (e.g., use existing record) |
| 429 | Yes | Wait for Retry-After seconds, then retry |
| 500 | Yes | Retry with exponential backoff |
Exponential backoff¶
For retryable errors, increase the wait time between attempts:
import time
import requests
def api_request(method, url, headers, json_body=None, max_retries=5):
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, json=json_body)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
if response.status_code >= 500:
time.sleep(2 ** attempt)
continue
return response.json()
raise Exception(f"Request failed after {max_retries} retries")
Never retry automatically on 400-level errors
Retrying a malformed request will always produce the same error. Fix the payload first.
Idempotency¶
Understanding idempotency helps you safely retry requests without creating duplicate data.
| Method | Idempotent? | Safe to retry? | Notes |
|---|---|---|---|
GET | Yes | Yes | Read-only — always safe |
POST | No | Conditional | May create duplicates — check first |
PUT | Yes | Yes | Replaces the entire resource |
PATCH | Yes | Yes | Updates specific fields only |
Preventing duplicate creates¶
Before creating a resource with POST, check whether it already exists:
# Check if customer already exists
curl "https://fsmnavigator.com/api/v1/customers?search=contact%40acme.com" \
-H "X-API-Key: YOUR_API_KEY"
# If no results, create the customer
curl -X POST "https://fsmnavigator.com/api/v1/customers" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_name": "Acme Corp", "contact_email": "[email protected]"}'
Pagination¶
List endpoints return paginated results. Always iterate through all pages to get complete data.
cURL example¶
# Fetch page 1
curl "https://fsmnavigator.com/api/v1/jobs?page=1&per_page=50" \
-H "X-API-Key: YOUR_API_KEY"
# Response includes pagination metadata:
# { "success": true, "jobs": [...], "page": 1, "per_page": 50, "total": 237, "total_pages": 5 }
# Fetch page 2
curl "https://fsmnavigator.com/api/v1/jobs?page=2&per_page=50" \
-H "X-API-Key: YOUR_API_KEY"
Python — iterate all pages¶
import requests
def get_all_jobs(api_key):
all_jobs = []
page = 1
while True:
response = requests.get(
"https://fsmnavigator.com/api/v1/jobs",
headers={"X-API-Key": api_key},
params={"page": page, "per_page": 50}
)
data = response.json()
if not data.get("success"):
raise Exception(f"API error: {data.get('message')}")
all_jobs.extend(data["jobs"])
if page >= data["total_pages"]:
break
page += 1
return all_jobs
Use per_page=50
50 records per page is a good balance between fewer API calls and manageable response sizes. The maximum allowed is 100.
Data synchronization patterns¶
One-way push (external system → FSM Navigator)¶
Push data from your CRM or ERP into FSM Navigator. Best for systems that own the customer master data.
Pattern:
- Fetch new/updated records from your source system.
- For each record, search FSM Navigator to check if it already exists.
- If it exists, update it with
PUT. If not, create it withPOST.
One-way pull (FSM Navigator → data warehouse)¶
Pull data from FSM Navigator into your analytics platform or data warehouse.
Pattern:
- Store the last sync timestamp.
- Query FSM Navigator for records updated since that timestamp.
- Upsert the records into your warehouse.
- Update the stored timestamp.
Incremental sync with timestamps¶
Use updated_at filters to fetch only changed records since your last sync:
# Fetch jobs updated since last sync
curl "https://fsmnavigator.com/api/v1/jobs?updated_since=2026-02-23T00:00:00Z&per_page=50" \
-H "X-API-Key: YOUR_API_KEY"
import requests
from datetime import datetime
def sync_jobs_since(api_key, last_sync_iso):
"""Fetch all jobs updated since the given timestamp."""
updated_jobs = []
page = 1
while True:
response = requests.get(
"https://fsmnavigator.com/api/v1/jobs",
headers={"X-API-Key": api_key},
params={
"updated_since": last_sync_iso,
"page": page,
"per_page": 50
}
)
data = response.json()
updated_jobs.extend(data["jobs"])
if page >= data["total_pages"]:
break
page += 1
return updated_jobs
# Usage
changes = sync_jobs_since("YOUR_API_KEY", "2026-02-23T00:00:00Z")
print(f"Found {len(changes)} updated jobs since last sync")
Security best practices¶
Follow these guidelines to keep your API integration secure.
Key management¶
| Practice | Why it matters |
|---|---|
| One key per integration | Revoke a single integration without affecting others |
| Minimum required scopes | Reduce blast radius if a key is compromised |
| IP whitelisting | Allow requests only from your known server IPs |
| Regular rotation | Rotate keys periodically (e.g., every 90 days) |
| Never log keys | Mask or redact keys in application logs |
Scope selection guide¶
Grant only the scopes your integration actually needs:
| Integration type | Recommended scopes |
|---|---|
| Read-only dashboard | jobs:read, customers:read |
| CRM sync | customers:read, customers:write |
| Dispatching system | jobs:read, jobs:write, customers:read |
| IoT monitoring | assets:read, assets:write |
| Full ERP integration | jobs:read, jobs:write, customers:read, customers:write |
Never use full-scope keys for single-purpose integrations
If a reporting dashboard only needs read access, don't grant write scopes. This limits the damage if the key is ever exposed.
Common integration scenarios¶
Quick-reference recipes for the most frequent integration patterns.
| Scenario | API calls needed | Key scopes |
|---|---|---|
| CRM customer sync | List customers → compare → create/update | customers:read, customers:write |
| Dispatch from ERP | Search customer → get locations → create job | customers:read, jobs:write |
| Asset IoT monitoring | Get asset → submit meter readings | assets:read, assets:write |
| Reporting / analytics | List jobs + list customers (paginated) | jobs:read, customers:read |
| Invoice reconciliation | List jobs by status → match with your records | jobs:read |
| Technician scheduling | List jobs by date range and technician | jobs:read, technicians:read |
Related guides¶
- Authentication — API key setup and scopes
- Endpoints overview — all available endpoints at a glance
- Rate limits — understand throttling and backoff
- Error codes — full error reference
- Jobs API — job lifecycle and statuses
- Customers API — customer and location management
- Assets API — asset tracking and maintenance