[← API v2 Overview](https://motordesk.com/api-docs/v2/)

# Errors

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.

## Error Shape

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`.

```json
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "The email field is required.",
    "details": { "field": "email" }
  }
}
```

## HTTP Status Families

| 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. |

## Error Codes

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.

### Authentication

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. |

### Authorization

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. |

### Request Validation

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. |

### Not Found

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. |

### Conflict

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. |

### Operation Failed

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. |

### Rate Limiting & Server

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). |
