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
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, andnotes. Fields arenullwhen they have no value.Initial-create mirror (2026-05-27): When a customer is created with an inline
locationobject (POST /v1/customerswithlocationin the request body), the first location now automatically copies the customer-levelcontact_emailandcontact_phoneinto 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-upPATCH. Locations created via the separatePOST /v1/customers/{customer_id}/locationsendpoint (additional locations) do NOT auto-mirror — passcontact_email/contact_phoneexplicitly 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).
Related guides
- Endpoints overview — all available endpoints
- Authentication — API key setup
- Jobs API — create jobs linked to customer locations
- Managing customers — web dashboard guide
- Bulk import — import customers via CSV