Skip to content

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.

Your CRM  ──push──▶  FSM Navigator

Pattern:

  1. Fetch new/updated records from your source system.
  2. For each record, search FSM Navigator to check if it already exists.
  3. If it exists, update it with PUT. If not, create it with POST.

One-way pull (FSM Navigator → data warehouse)

Pull data from FSM Navigator into your analytics platform or data warehouse.

FSM Navigator  ──pull──▶  Your warehouse

Pattern:

  1. Store the last sync timestamp.
  2. Query FSM Navigator for records updated since that timestamp.
  3. Upsert the records into your warehouse.
  4. 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