Errors return a consistent envelope with a stable, machine-readable code and an appropriate HTTP status. Branch on the status family and the code; fall back to the message for codes you don't recognise.
Every error uses the same envelope. error.code is a stable machine-readable string (it will not change within a major version), error.message is human-readable and may change, and error.details is optional and code-specific; for validation_failed it carries the offending field.
{
"success": false,
"error": {
"code": "validation_failed",
"message": "The email field is required.",
"details": { "field": "email" }
}
}| Status | Meaning | Retry? |
|---|---|---|
400 | Malformed request (invalid JSON). | No, fix the request. |
401 | Authentication failed (missing/expired/revoked key). | No, re-authenticate. |
403 | Authenticated but not authorized (missing scope / business). | No, grant the scope. |
404 | Route or addressed resource not found. | No. |
409 | Conflict with current state (duplicate, limit, wrong state). | Only after resolving the conflict. |
422 | Validation failed, or an operation was rejected. | No, fix the input. |
429 | Rate limit exceeded. | Yes, after Retry-After. |
5xx | Server or upstream-provider failure. | Yes, with exponential backoff. |
Treat unknown codes defensively: branch on the HTTP status family and fall back to error.message for any code your client does not yet recognise.
Returned as 401. The bearer token is missing, malformed, expired or revoked. Re-issue or refresh the key.
| Code | HTTP | Meaning |
|---|---|---|
authentication_required | 401 | No bearer token was supplied on the request. |
authentication_invalid | 401 | The bearer token is malformed or does not match an active key. |
authentication_expired | 401 | The API key has passed its expiry date. |
authentication_revoked | 401 | The API key has been revoked. |
authentication_blocked | 401 | The request was blocked (for example by an IP or abuse block) before authentication. |
Returned as 403. The key authenticated but is not permitted to perform the request.
| Code | HTTP | Meaning |
|---|---|---|
scope_required | 403 | The endpoint requires a scope and the key presented none for it. |
scope_denied | 403 | The key does not hold the scope this endpoint requires. |
business_required | 403 | The key is not associated with a business. |
business_not_found | 403 | The business the key belongs to no longer exists. |
business_unverified | 403 | The business has not completed the verification required for API access. |
account_frozen | 403 | The business account is frozen; API access is locked. Contact support. |
account_suspended | 403 | The business account is suspended; resolve billing to restore access. |
account_demo | 403 | Demo accounts cannot access the API. |
account_read_only | 403 | The business account is read-only; the API is not available. |
plan_upgrade_required | 403 | API access requires the Growth plan or above. |
api_access_required | 403 | API 2.0 access has not been granted for this business. Request access from the dashboard. |
The request reached a handler but the body or fields were unacceptable. For validation_failed, details.field names the offending field.
| Code | HTTP | Meaning |
|---|---|---|
json_invalid | 400 | The request body is not valid JSON. |
validation_failed | 422 | One or more fields failed validation. details.field identifies the field. |
idempotency_key_invalid | 400 | The Idempotency-Key header is malformed (1-255 characters: letters, numbers, hyphens and underscores). |
idempotency_key_reused | 422 | The Idempotency-Key was already used with a different request body. |
Returned as 404 (the route or addressed resource does not exist for this business), or 405 when the path exists but not for the request method - see the Allow header.
| Code | HTTP | Meaning |
|---|---|---|
route_not_found | 404 | No endpoint matches the request method and path. |
method_not_allowed | 405 | The path exists but does not support this HTTP method; the Allow response header lists the methods that are accepted. |
listing_not_found | 404 | The listing could not be found (not published, or outside the for-sale/recently-sold window). |
contact_not_found | 404 | The contact could not be found. |
contact_note_not_found | 404 | The contact note could not be found. |
lead_not_found | 404 | The lead could not be found. |
lead_note_not_found | 404 | The lead note could not be found. |
lead_message_not_found | 404 | The lead message could not be found. |
lead_vehicle_not_found | 404 | The lead-vehicle association could not be found. |
lead_appointment_not_found | 404 | The lead appointment could not be found. |
appointment_not_found | 404 | The appointment could not be found. |
call_not_found | 404 | The call record could not be found. |
blog_not_found | 404 | The blog article could not be found. |
review_not_found | 404 | The review could not be found. |
document_not_found | 404 | The document could not be found. |
document_signature_not_found | 404 | The document signature could not be found. |
vehicle_not_found | 404 | The vehicle could not be found. |
vehicle_document_not_found | 404 | The vehicle document could not be found. |
vehicle_media_not_found | 404 | The vehicle media item could not be found. |
vehicle_job_not_found | 404 | The vehicle job could not be found. |
vehicle_job_stage_not_found | 404 | The vehicle job stage could not be found. |
vehicle_job_document_not_found | 404 | The vehicle job document could not be found. |
vehicle_job_purchase_not_found | 404 | The vehicle job purchase could not be found. |
Returned as 409. The request is valid but conflicts with the current state; resolve the conflict and retry.
| Code | HTTP | Meaning |
|---|---|---|
contact_email_duplicate | 409 | A contact with this email address already exists (see POST /contacts/duplicate-check). |
lead_vehicle_duplicate | 409 | The vehicle or appointment is already associated with the lead. |
vehicle_drive_not_active | 409 | There is no active test drive on the vehicle to act on. |
contact_login_disabled | 409 | The contact does not have customer login enabled. |
vehicle_publish_limit_reached | 409 | Publishing would exceed the business's for-sale vehicle limit. |
vehicle_publish_incomplete | 409 | The vehicle is missing required fields and cannot be published. |
vehicle_status_process_required | 409 | This status is set by a dedicated process (reserve or sale), not by changing status directly. |
vehicle_status_transition_invalid | 409 | The requested status transition is not allowed from the vehicle's current status. |
vehicle_not_reserved | 409 | The vehicle is not currently reserved. |
vehicle_already_reserved | 409 | The vehicle already has an active reservation. |
vehicle_reserve_invalid_status | 409 | Only a for-sale vehicle can be reserved. |
vehicle_not_sold | 409 | The vehicle is not sold, so its handover cannot be completed. |
vehicle_not_appraisal | 409 | The endpoint is only available for appraisal vehicles. |
vehicle_appraisal_invalid_status | 409 | The appraisal is not in the right state for this action. |
vehicle_appraisal_incomplete | 409 | The appraisal needs vehicle details before it can be accepted into stock. |
credit_cap_exceeded | 409 | An item-linked credit line exceeds what remains creditable on the invoice line it references. |
purchase_acquisition_linked | 409 | The purchase is linked to a vehicle acquisition (the dashboard's vehicle-purchase flow) and cannot be edited via the API - manage it from the dashboard. |
invoice_not_issued | 409 | The action needs an issued or paid invoice (e.g. generating the finance provider invoice for a draft - issuing the draft does that automatically). |
finance_invoice_exists | 409 | The invoice already has a finance provider invoice; the message names its number. |
invoice_fully_credited | 409 | The invoice has been fully credited, so the action is no longer available. |
The request was accepted but the underlying action did not complete. 422 means the input was usable but the operation was rejected; 5xx means a server or upstream-provider failure, safe to retry with backoff.
| Code | HTTP | Meaning |
|---|---|---|
appointment_contact_not_found | 422 | The contact referenced when booking the appointment could not be found. |
lead_send_failed | 422 | The lead reply could not be sent. |
vehicle_lookup_failed | 422 | The third-party vehicle lookup (DVLA/DVSA/AutoTrader) failed. |
vehicle_recognition_failed | 422 | Vehicle/number-plate recognition failed. |
vehicle_drive_photo_failed | 422 | The test-drive photo could not be stored. |
vehicle_media_edit_failed | 422 | The media edit could not be applied. |
vehicle_media_upload_failed | 422 | The media upload could not be processed. |
vehicle_costs_full | 422 | The vehicle already holds the maximum number of purchase cost rows (50), so the additional-cost link cannot be attached. |
finance_invoice_ineligible | 422 | The invoice is missing what the finance provider invoice needs: a finance provider contact, a finance amount above zero and a sold vehicle line. |
description_failed | 500 | AI advert description generation failed. |
document_send_failed | 500 | The document could not be sent. |
appointment_notify_failed | 500 | The appointment notification could not be sent. |
contact_login_email_failed | 500 | The login-details email could not be sent. |
contact_login_password_unavailable | 500 | A new login password could not be generated. |
vehicle_drive_email_failed | 502 | The test-drive terms email failed at the mail provider. |
Throttling and unexpected server conditions. Honour Retry-After on 429; retry 5xx with exponential backoff.
| Code | HTTP | Meaning |
|---|---|---|
rate_limited | 429 | The per-business request rate limit was exceeded. Retry after the window (see Retry-After and X-RateLimit-Reset). |
internal | 500 | An unexpected server error occurred (503 is returned during maintenance or overload). |
internal_error | 500 | An unexpected internal error occurred while completing the request (safe to retry with backoff). |