Skip to content

Customers API

Enterprise feature

The REST API is available on Enterprise plans. Compare plans to find the right fit for your team.

The Customers API lets you create, retrieve, and update customer records and their service locations programmatically. Use it to sync your CRM, import customer data, or build custom dashboards.


Endpoints summary

Method Path Scope Description
GET /api/v1/customers customers:read List customers
GET /api/v1/customers?id={id} customers:read Get a single customer
POST /api/v1/customers customers:write Create a customer
PUT /api/v1/customers?id={id} customers:write Update a customer
GET /api/v1/customers?id={id}&locations=1 customers:read List customer locations
POST /api/v1/customers?id={id}&locations=1 customers:write Create a location
PUT /api/v1/customers?id={id}&locations=1&location_id={loc_id} customers:write Update a location

Customer object

Every customer returned by the API contains these fields:

Field Type Description
customer_id integer Unique customer identifier
customer_name string Customer or company name
customer_email string Primary contact email
customer_phone string Primary contact phone (E.164 format)
default_sla_hours integer Default SLA response time in hours, from 1 to 168 (one week)
tax_exempt boolean Whether the customer is tax exempt
tax_exemption_reason string / null Reason for tax exemption (max 255 characters)
created_at string When the customer was created (ISO 8601)
updated_at string When the customer was last updated (ISO 8601)

List customers

GET /api/v1/customers

Returns a paginated list of customers.

Query parameters

Parameter Type Description
search string Search by customer name, email, or phone
created_after string Filter customers created after this date (ISO 8601 format, e.g., 2026-01-15T00:00:00Z)
created_before string Filter customers created before this date (ISO 8601 format)
has_locations string Filter by location status: 1 to return only customers with locations, 0 for customers without
page integer Page number (default: 1)
per_page integer Results per page (default: 50, max: 100)

Example request

curl -X GET "https://fsmnavigator.com/api/v1/customers?search=Acme" \
  -H "X-API-Key: YOUR_API_KEY"

Example response

{
  "data": [
    {
      "customer_id": 201,
      "customer_name": "Acme Corporation",
      "customer_email": "[email protected]",
      "customer_phone": "+15551234567",
      "default_sla_hours": 24,
      "tax_exempt": false,
      "tax_exemption_reason": null,
      "created_at": "2026-01-15T05:00:00-05:00",
      "updated_at": "2026-02-20T03:30:00-05:00"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total": 1,
    "total_pages": 1
  },
  "request_id": "a1b2c3d4e5f6"
}

Get a single customer

GET /api/v1/customers?id={id}

Retrieves full details for one customer. Add &include=locations to embed service locations in the response.

Example request

curl -X GET "https://fsmnavigator.com/api/v1/customers?id=201&include=locations" \
  -H "X-API-Key: YOUR_API_KEY"

Example response

{
  "data": {
    "customer_id": 201,
    "customer_name": "Acme Corporation",
    "customer_email": "[email protected]",
    "customer_phone": "+15551234567",
    "default_sla_hours": 24,
    "tax_exempt": false,
    "tax_exemption_reason": null,
    "created_at": "2026-01-15T05:00:00-05:00",
    "updated_at": "2026-02-20T03:30:00-05:00"
  },
  "request_id": "a1b2c3d4e5f6"
}

Create a customer

POST /api/v1/customers

Creates a new customer record. Returns 201 Created on success. Locations are managed separately — see Customer locations below.

Request body

Field Type Required Description
customer_name string Yes Customer or company name
customer_email string Yes Primary contact email
customer_phone string Yes Primary contact phone. E.164 with an explicit country code is required — +15551234567. A national-format number with no country code (5551234567) is rejected with 400 validation_failed, because there is no reliable way to infer which country it belongs to.
default_sla_hours integer Yes Whole hours from 1 to 168. See the note on decimal values below
tax_exempt boolean No Whether the customer is tax exempt (default: false)
tax_exemption_reason string No Reason for tax exemption (max 255 characters)

Example request

curl -X POST "https://fsmnavigator.com/api/v1/customers" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Riverside Medical Center",
    "customer_email": "[email protected]",
    "customer_phone": "+15559876543",
    "default_sla_hours": 24,
    "tax_exempt": false,
    "tax_exemption_reason": null
  }'

Example response

{
  "success": true,
  "message": "Customer created successfully",
  "data": {
    "customer_id": 202,
    "customer_name": "Riverside Medical Center"
  },
  "request_id": "a1b2c3d4e5f6"
}

Update a customer

PUT /api/v1/customers?id={id}

Updates one or more fields on an existing customer record.

Example request

curl -X PUT "https://fsmnavigator.com/api/v1/customers?id=201" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_email": "[email protected]"
  }'

Example response

{
  "success": true,
  "message": "Customer updated successfully",
  "data": {
    "customer_id": 201,
    "customer_name": "Acme Corporation"
  },
  "request_id": "a1b2c3d4e5f6"
}

Customer locations

Locations are managed as a sub-resource of a customer. Each customer can have multiple service locations, and each location is automatically geocoded when created or updated.

v1 API change (2026-05-27): Location response now includes contact_email, contact_phone, and notes. Fields are null when they have no value.

Initial-create mirror (2026-05-27): When a customer is created with an inline location object (POST /v1/customers with location in the request body), the first location now automatically copies the customer-level contact_email and contact_phone into the location's per-location contact fields. This ensures default-location billing and service notifications route through the customer's primary contact without a follow-up PATCH. Locations created via the separate POST /v1/customers/{customer_id}/locations endpoint (additional locations) do NOT auto-mirror — pass contact_email / contact_phone explicitly if you want a per-location override.

Location object

Field Type Description
location_id integer Unique location identifier
location_name string Name for this location
address_street string Street address
address_city string City
address_state string Two-letter US state or territory code — the 50 states plus DC, PR, VI, GU, AS and MP.
address_zip string ZIP or postal code. 3-12 characters of letters, digits, spaces or hyphens — 78701 and 78701-1234 are both accepted, and since 2026-08-30 so are non-US postal codes such as SW1A 1AA.
latitude number Latitude (auto-geocoded)
longitude number Longitude (auto-geocoded)
is_primary boolean Whether this is the primary location
created_at string When the location was created (ISO 8601)
updated_at string When the location was last updated (ISO 8601)
contact_email string Email for location-specific contact (optional, nullable)
contact_phone string Phone for location-specific contact (optional, E.164 format recommended)
notes string Internal notes about this location (optional, max ~2KB)

List locations

GET /api/v1/customers?id={id}&locations=1

Returns a paginated list of locations for the specified customer.

Example request

curl -X GET "https://fsmnavigator.com/api/v1/customers?id=201&locations=1" \
  -H "X-API-Key: YOUR_API_KEY"

Example response

{
  "data": [
    {
      "location_id": 87,
      "location_name": "Main Office",
      "address_street": "123 Commerce Blvd",
      "address_city": "Austin",
      "address_state": "TX",
      "address_zip": "78701",
      "latitude": 30.2672,
      "longitude": -97.7431,
      "is_primary": true,
      "created_at": "2026-01-15T05:00:00-05:00",
      "updated_at": "2026-01-15T05:00:00-05:00",
      "contact_email": "[email protected]",
      "contact_phone": "+15558901234",
      "notes": "Main office with 2 conference rooms"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total": 1,
    "total_pages": 1
  },
  "request_id": "a1b2c3d4e5f6"
}

Create a location

POST /api/v1/customers?id={id}&locations=1

Creates a new service location for the specified customer. The address is automatically geocoded. Returns 201 Created on success.

Request body

Field Type Required Description
location_name string Yes Name for this location
address_street string Yes Street address
address_city string Yes City
address_state string Yes Two-letter US state or territory code (e.g. TX) — the 50 states plus DC, PR, VI, GU, AS and MP.
address_zip string Yes ZIP or postal code. 3-12 characters of letters, digits, spaces or hyphens — 78701 and 78701-1234 are both accepted, and since 2026-08-30 so are non-US postal codes such as SW1A 1AA. The accept set only WIDENED, so nothing previously accepted is now refused.
contact_email string No Email for location-specific contact
contact_phone string No Phone for location-specific contact (E.164 format recommended)
notes string No Internal notes about this location (max ~2KB)

Example request

curl -X POST "https://fsmnavigator.com/api/v1/customers?id=201&locations=1" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "location_name": "East Wing",
    "address_street": "456 Health Park Dr",
    "address_city": "Dallas",
    "address_state": "TX",
    "address_zip": "75201"
  }'

Example response

{
  "success": true,
  "message": "Location created successfully",
  "data": {
    "location_id": 88,
    "location_name": "East Wing",
    "address_street": "456 Health Park Dr",
    "address_city": "Dallas",
    "address_state": "TX",
    "address_zip": "75201",
    "latitude": 32.7767,
    "longitude": -96.7970,
    "contact_email": "[email protected]",
    "contact_phone": "+15559876500",
    "notes": "Dedicated HVAC maintenance area"
  },
  "request_id": "a1b2c3d4e5f6"
}

Update a location

PUT /api/v1/customers?id={id}&locations=1&location_id={loc_id}

Updates one or more fields on an existing service location. If the address changes, it is automatically re-geocoded.

Example request

curl -X PUT "https://fsmnavigator.com/api/v1/customers?id=201&locations=1&location_id=87" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "location_name": "Main Office - Renovated",
    "address_street": "125 Commerce Blvd"
  }'

Example response

{
  "success": true,
  "message": "Location updated successfully",
  "data": {
    "location_id": 87,
    "location_name": "Main Office - Renovated",
    "address_street": "125 Commerce Blvd",
    "address_city": "Austin",
    "address_state": "TX",
    "address_zip": "78701",
    "latitude": 30.2675,
    "longitude": -97.7428,
    "contact_email": "[email protected]",
    "contact_phone": "+15558901234",
    "notes": "Recently renovated reception area"
  },
  "request_id": "a1b2c3d4e5f6"
}

A note on default_sla_hours

default_sla_hours is, and has always been documented as, an integer — a whole number of hours between 1 and 168 (one week). Send 24, or the string "24"; both are accepted.

Values that are not whole numbers are now rejected

Until August 2026 this field also accepted a handful of shapes that are not whole numbers, and quietly rounded them down before saving:

You sent We used to save We now return
48.9 48 400 validation_failed
"24.0" 24 400 validation_failed
"1e2" 100 400 validation_failed
"024" 24 400 validation_failed
"24." 24 400 validation_failed

Rounding a value down without telling you is the part we wanted to stop: if you asked for a 48.9-hour response window, the record said 48 hours and the response said success. There was no way for you to tell from the reply that anything had changed.

What this means for you. If your integration already sends whole numbers, nothing changes. If it can produce a decimal — a computed value, a spreadsheet column, a division — round it yourself before sending, so you decide whether 48.9 hours means 48 or 49.

Nothing else about this field moved. The HTTP status is still 400, the error is still validation_failed, and the message is unchanged. Values outside 1–168, and values that were never numbers at all, behave exactly as they did before.


Error responses

See Error codes for the full reference. Common customer-specific errors:

HTTP code Error Description
400 validation_failed Missing required fields or invalid data
404 not_found Customer or location ID does not exist
409 customer_duplicate_contact Another active customer already uses this email address or phone number
409 customer_soft_deleted_conflict A removed customer still holds this email address or phone number

Each account allows one customer per email address and one per phone number. Both 409 responses name the collision so you can resolve it without guessing:

{
  "success": false,
  "error": "customer_duplicate_contact",
  "matched_field": "email",
  "customer_id": 4821,
  "customer_name": "Riverside Property Group",
  "message": "A customer with this email address already exists — Riverside Property Group (#4821). Use a different email address, or add this site as another location on the existing customer."
}

matched_field is email or phone. A conflict is permanent — do not retry it. Either reuse the customer named in the response (adding the new site as one of its locations), restore it if the code is customer_soft_deleted_conflict, or submit a different contact detail. The same two codes apply to POST (create) and to PUT (update).