API error codes
Enterprise feature
The REST API is available on Enterprise plans. Compare plans to find the right fit for your team.
When an API request fails, the response includes an HTTP status code and a structured JSON body with details about the error. This page covers every error you may encounter and how to resolve it.
Error response format
All error responses follow this structure:
{
"success": false,
"error": "error_code",
"message": "A human-readable description of what went wrong."
}
| Field | Type | Description |
|---|---|---|
success | boolean | Always false for errors |
error | string | Machine-readable error code (snake_case) |
message | string | Human-readable description you can display to users |
HTTP status codes
400 — Bad request
The request is malformed or contains invalid data.
| Error code | Description | How to fix |
|---|---|---|
validation_failed | One or more required fields are missing or invalid | Check the request body against the endpoint documentation |
missing_required_field | A required field was not included | Add the missing field to your request |
assignee_required | You set a job to Assigned without a technician on it | Assign a technician to the job first, then set the status |
sla_required | You tried to clear a job's SLA deadline. Every job must have one | Send a new due_at, or leave the field out of the request to keep the existing deadline. See SLA deadlines |
meter_not_found | The specified meter does not exist for this asset | Configure meters for the asset in the dashboard first |
invalid_status | On a GET list endpoint, the status query parameter is not a value the resource can hold (assets, technicians) | The message lists every accepted value — send one of those. Note this code also appears under 409 with a different meaning; see the note below |
invalid_availability_status | On the technicians GET list endpoint, the availability_status query parameter is not a value a technician can hold | The message lists every accepted value — send one of those. This code is 400 only; it has no 409 sense |
invalid_sub | On the jobs GET endpoint, the sub query parameter is not hold-reasons, or it was sent together with id | Send sub=hold-reasons with no id to list your company's hold reasons |
custom_field_invalid | A job's custom_fields object was refused: a key that is not one of your fields, a value of the wrong type, or a field required at creation left out | The errors object names each field key and the problem. Nothing was written — fix those values and resend. See Custom fields |
final_comment_required | You set a job to Completed without a final_comment | Add a short note on the work done as final_comment. See Completing a job |
comment_too_long | The job's final_comment is longer than 500 characters | Shorten the comment and resend |
401 — Unauthorized
Authentication failed or was not provided.
| Error code | Description | How to fix |
|---|---|---|
unauthorized | API key is missing, invalid, or revoked | Verify your X-API-Key header contains a valid key |
Check your key
If you recently rotated your key, make sure all integrations use the new one. Revoked keys return 401 immediately.
403 — Forbidden
The API key does not have permission for this action.
| Error code | Description | How to fix |
|---|---|---|
forbidden | The key's scopes do not include the required permission | Update the key's scopes in Settings → API Keys |
ip_not_allowed | Request came from an IP not in the key's whitelist | Add the source IP to the key's whitelist or remove the restriction |
scope_not_authorized | The API key lacks the required scope for this operation | Add the required scope to your API key in Settings → API Keys |
Suspended or inactive company account
When your company account is suspended or set to inactive, every API request returns 403 forbidden with the message "This company account is suspended". Your keys start working again on the next request once the account is active. Contact support to restore the account.
404 — Not found
The requested resource does not exist.
| Error code | Description | How to fix |
|---|---|---|
not_found | The resource ID does not exist or is not accessible | Verify the ID is correct and belongs to your company |
405 — Method not allowed
The HTTP method is not supported for this endpoint.
| Error code | Description | How to fix |
|---|---|---|
method_not_allowed | You used the wrong HTTP method (e.g., POST to a read-only endpoint) | Check the endpoint documentation for the correct method |
409 — Conflict
The request conflicts with the current state of the resource.
| Error code | Description | How to fix |
|---|---|---|
customer_duplicate_contact | Another active customer in your account already uses this email address or phone number | Check matched_field to see which one collided. The response also returns the customer_id and customer_name of the existing record — update that customer, add the site as another location on it, or use a different contact detail |
customer_soft_deleted_conflict | A removed customer still holds this email address or phone number | Restore that customer from the dashboard, or use a different contact detail. The response returns its customer_id and customer_name |
duplicate_customer | A row in a CSV bulk import matched an existing customer | Bulk import only — remove the duplicate row or let the import skip it |
conflict | The resource was modified by another request since you read it (optimistic concurrency / concurrent status change) | Re-read the resource and retry — for job updates, resend with the current version as expected_version |
invalid_status | On a write endpoint, the status you sent is not one this resource uses | The message lists every status the resource accepts — send one of those |
invalid_transition | The status change is not allowed from the status the resource is in right now | The message lists the statuses you can move to from here. Move through an allowed step first, or re-read the resource if someone else already changed it |
invalid_state | The resource is in a terminal state — a completed, declined or cancelled job, or a decommissioned asset — and can no longer be edited | Move it out of the terminal state first (reopen the job), then resend. A decommissioned asset cannot be updated through the write endpoints at all |
job_forms_incomplete | You set a job to Completed while forms it requires are not submitted | The missing list names each form and its unanswered items. Submit the forms in the app, then resend. The API cannot skip this check |
invalid_status carries two senses, and the HTTP code tells them apart
On a write endpoint the status names a resource state, so the refusal is 409 Conflict. On a GET list endpoint it is only a malformed query-string filter — nothing conflicts with any resource state — so the refusal is 400 Bad Request. Branch on the status code, not on the error string alone.
Never retry a 409
A contact conflict is permanent: each account allows one customer per email address and one per phone number, and a removed customer keeps its contact details until it is restored. The identical request will fail the same way every time, so resolve the conflict rather than retrying.
422 — Unprocessable
The request is well formed, but what it asks for cannot be done with the record as it is.
| Error | Description | Resolution |
|---|---|---|
custom_fields_required | You set a job to Completed while custom fields required for completion are empty | The missing list names each field (field_key, field_name). Fill them in with a PUT carrying custom_fields, then complete the job again. See Completing a job |
hold_reason_required | You set a job to On-Hold without a hold_reason_key | Send one of your company's hold reasons. See Putting a job on hold |
invalid_hold_reason | The hold_reason_key is not on your company's list, or the reason is hidden | Use a key from the list in Putting a job on hold or one your company added |
hold_note_required | The reason is other and no hold_reason_note was sent | Add a short note and send the request again |
hold_note_too_long | The hold_reason_note is over 500 characters | Shorten the note to 500 characters or fewer |
429 — Too many requests
You have exceeded the rate limit.
| Error code | Description | How to fix |
|---|---|---|
rate_limit_exceeded | Too many requests in the current time window | Wait for the duration specified in the Retry-After header |
The response includes a retry_after field (seconds) and a Retry-After HTTP header.
See Rate limits for best practices.
500 — Server error
An unexpected error occurred on the server.
| Error code | Description | How to fix |
|---|---|---|
server_error | An internal error occurred | Retry the request after a short delay. If the issue persists, contact support. |
Troubleshooting checklist
Use this checklist when you encounter an API error:
- Check the HTTP status code — it tells you the category of the problem.
- Read the
errorandmessagefields — they pinpoint the specific issue. - Verify your API key — is it valid, not revoked, and included in the
X-API-Keyheader? - Check your scopes — does the key have permission for this operation?
- Validate your request body — are all required fields present and correctly typed?
- Review the
Retry-Afterheader — if rate-limited, wait before retrying. - Contact support — if you see
500errors consistently, reach out with the request details.
Frequently asked questions
Why do I get 401 even though my key looks correct?
Make sure the key is in the X-API-Key header — not in the URL or request body. Also verify the key has not been revoked.
Can I get more details about a 400 error?
Yes. The message field describes which validation rule failed, and in most cases names the problematic field.
What should I do on a 500 error?
Retry after a short delay (5–10 seconds). If the error persists, contact support with the request method, endpoint, and approximate timestamp.
Related guides
- Authentication — fix
401and403errors - Rate limits — understand
429responses - Endpoints overview — verify you are using the correct method and path
- Jobs API — job-specific validation rules