# Nexcar API Docs

> Integrator reference: cases, documents, webhooks and supported types.

> Auto-generated bundle: a single Markdown file containing every documentation page, intended as context for LLMs.

---

## Introduction

_The Nexcar API lets you digitize and validate vehicle cases — invoices, ID documents, REPUVE, vehicle taxes and more — from your own systems._

### What is the Nexcar API?

Nexcar processes **vehicle cases** end-to-end: you send us documents (invoice, owner ID, vehicle tax payment receipt, REPUVE certificate, proof of address, etc.) and we apply OCR, automatic classification and business rules so your team or your system gets structured, decision-ready data back.

The API is built for integrations with:

- **Insurers** receiving claims that need documentary validation of the affected vehicle.
- **Used-car marketplaces** that need fast document verification before a sale.
- **Auto-loan originators** that must validate the legal owner.
- **Compliance teams** that need an audit trail for every document.

### Key concepts

- **Case (`case`)**: the logical container for a vehicle. Each case has a `case_id` (UUID) and groups every document belonging to that vehicle.
- **Document (`document`)**: an individual file (PDF, image, XML) attached to a case. Each one is identified by a `document_id`.
- **Document type**: the file's category (invoice, ID, REPUVE, tax, etc.). It can be set manually on upload or detected automatically.
- **Webhook**: an HTTP notification we send to your server when a document or case changes state (upload completed, OCR ready, validation finished).

### Typical integration flow

1. **Create a case** with `POST /v1/cases`. You get a `case_id`.
2. **Attach documents** to the case with `POST /v1/documents`, one per file. Each call returns a `document_id`.
3. **Receive webhooks** as processing progresses: classification, OCR, data extraction.
4. **Read results** with `GET /v1/documents/{id}` or `GET /v1/cases/{id}` whenever you need them.

### Environments

| Environment | Base URL | Use |
|---|---|---|
| Production | `https://api.nexcar.mx` | Live traffic |
| Sandbox | `https://api-sandbox.nexcar.mx` | Testing and integration |

### Support

Ready to integrate? Continue with [Authentication](/docs/en/authentication) or email us at [support@nexcar.mx](mailto:support@nexcar.mx).

---

## Authentication

_The API supports two authentication methods. Pick the one that fits your integration best._

### Available methods

| Method | Header | Recommended for |
|---|---|---|
| **API Key** | `x-api-key: <api_key>` | Long-lived server-to-server integrations |
| **Bearer Token (JWT)** | `Authorization: Bearer <token>` | Integrations where the token is rotated per session |

> Every endpoint under `/v1/cases` and `/v1/documents` accepts at least one of the two methods.

### Get credentials

Request your credentials from your Nexcar contact or [support@nexcar.mx](mailto:support@nexcar.mx). You'll get an `api_key` for production and another for the sandbox environment.

> **Never expose your `api_key` in browser-side code or commit it to a public repository.**

### Example: upload a document with API Key

```bash
curl -X POST https://api.nexcar.mx/v1/documents \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
    "url": "https://your-storage.example.com/vehicle-invoice.pdf",
    "mime_type": "application/pdf",
    "type": "factura"
  }'
```

### Example: create a case with Bearer Token

```bash
curl -X POST https://api.nexcar.mx/v1/cases \
  -H "Authorization: Bearer your_jwt_token" \
  -H "Content-Type: application/json" \
  -d '{
    "internal_id": "EXP-2026-0001",
    "vehicle_origin": "Nacional",
    "status": "processing"
  }'
```

### Authentication errors

| HTTP | Code | Cause |
|---|---|---|
| `401` | `UNAUTHORIZED` | Header missing or token expired |
| `401` | `INVALID_CREDENTIALS` | Invalid API key or malformed token |
| `403` | `FORBIDDEN` | Credentials are valid but lack permission on the resource |

---

## Cases

_A case represents a vehicle. Use this resource to obtain the `case_id` you'll attach documents to._

### Endpoints

| Method | Path | Description |
|---|---|---|
| `POST` | `/v1/cases` | Create a case, optionally with initial documents |
| `GET` | `/v1/cases/{case_id}` | Read the case and its documents (with OCR) |
| `DELETE` | `/v1/cases/{case_id}` | Soft-delete the case |
| `GET` | `/v1/cases/{case_id}/names` | Names consolidated from all documents |
| `GET` | `/v1/cases/{case_id}/vins` | Primary VIN and all VINs detected per document |
| `GET` | `/v1/cases/{case_id}/plates` | License plates detected in the case with vigencia data |
| `GET` | `/v1/cases/{case_id}/repuve` | REPUVE registration data for the vehicle |
| `GET` | `/v1/cases/{case_id}/pedimento` | Most recent customs entry (pedimento) result |
| `POST` | `/v1/cases/{case_id}/pedimento` | Trigger a new customs entry lookup from OCR data |
| `GET` | `/v1/cases/{case_id}/rapi` | Most recent RAPI (FGJCDMX) criminal activity lookup result |
| `POST` | `/v1/cases/{case_id}/rapi` | Trigger a new RAPI lookup for the case's primary VIN |
| `POST` | `/v1/cases/{case_id}/taxes` | Trigger a bulk tenencia lookup for all plates in the case |
| `GET` | `/v1/cases/{case_id}/invoices` | Invoices with their processing status |
| `GET` | `/v1/cases/{case_id}/metadata` | Business metadata attached to the case |
| `POST` | `/v1/cases/{case_id}/metadata` | Append an entry to the metadata history |
| `POST` | `/v1/cases/{case_id}/process` | Bulk-process the case's documents |
| `GET` | `/v1/cases/{case_id}/status` | Status of the latest processing job |

### POST `/v1/cases` — Create a case

Creates a case for a vehicle. You may attach documents in the same call or upload them later.

#### Body

| Field | Type | Required | Description |
|---|---|---|---|
| `internal_id` | string | Yes | Case identifier on your side. Cannot be empty. |
| `status` | string | Yes | Initial state of the case (e.g. `processing`, `nuevo`). |
| `vehicle_origin` | string | No | `Nacional` (default) or `Importado`. |
| `initial_status` | string | No | Alternative initial state when a status catalog is configured. |
| `use_case` | UUID | No | Use case for the file when a catalog applies. |
| `metadata` | object | No | Additional business data (see below). |
| `files` | array | No | Documents to upload immediately. |

#### curl example

```bash
curl -X POST https://api.nexcar.mx/v1/cases \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "internal_id": "EXP-2026-0001",
    "status": "processing",
    "vehicle_origin": "Nacional",
    "metadata": {
      "numero_siniestro": "128890890",
      "tipo_afectado": "TERCERO",
      "nombre_afectado": "JUAN PÉREZ GARCÍA"
    },
    "files": [
      {
        "url": "https://your-storage.example.com/invoice.pdf",
        "mime_type": "application/pdf",
        "document_type": "factura"
      },
      {
        "url": "https://your-storage.example.com/owner-id.jpg",
        "mime_type": "image/jpeg",
        "document_type": "ine"
      }
    ]
  }'
```

#### Request body

```json
{
  "internal_id": "EXP-2026-0001",
  "status": "processing",
  "vehicle_origin": "Nacional",
  "metadata": {
    "numero_siniestro": "128890890",
    "tipo_afectado": "TERCERO",
    "nombre_afectado": "JUAN PÉREZ GARCÍA"
  },
  "files": [
    {
      "url": "https://your-storage.example.com/invoice.pdf",
      "mime_type": "application/pdf",
      "document_type": "factura"
    },
    {
      "url": "https://your-storage.example.com/owner-id.jpg",
      "mime_type": "image/jpeg",
      "document_type": "ine"
    }
  ]
}
```

#### Responses

**`201 Created`** — case created without files:

```json
{ "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c", "status": "completed" }
```

**`202 Accepted`** — case created with files queued for processing:

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "job_id": "31b4ecf2-9a0b-4f98-8a91-2ab6e93f4f10",
  "total_files": 2,
  "status": "processing"
}
```

#### Common errors

| HTTP | Code | Cause |
|---|---|---|
| `400` | `VALIDATION_ERROR` | Missing `status` or empty `internal_id` |
| `400` | `DUPLICATE_INTERNAL_ID` | A case with that `internal_id` already exists with files |
| `422` | `INVALID_STATUS` | `status` is not part of the configured catalog |

### GET `/v1/cases/{case_id}`

Returns the case and all of its documents along with their OCR (when available).

#### curl example

```bash
curl https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c \
  -H "x-api-key: your_api_key"
```

#### Response

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "internal_id": "EXP-2026-0001",
  "status": "nuevo",
  "vehicle_origin": "Nacional",
  "documents": [
    {
      "document_id": "abc123",
      "type": "factura",
      "mime_type": "application/pdf",
      "url": "https://...nexcar.mx/storage/.../invoice.pdf",
      "parsed_data": { "vin": "3VWFE21C04M000001", "monto_total": 285000 }
    }
  ]
}
```

### DELETE `/v1/cases/{case_id}`

Marks the case as inactive. Files are not physically deleted; the case stops appearing in lookups and reports.

```bash
curl -X DELETE https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c \
  -H "x-api-key: your_api_key"
```

### POST `/v1/cases/{case_id}/process`

Queues the bulk processing (OCR + extraction) of the documents in the case. Useful when you uploaded files without `type` and want to run the flow as a batch.

```bash
curl -X POST https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/process \
  -H "x-api-key: your_api_key"
```

```json
{ "job_id": "31b4ecf2-9a0b-4f98-8a91-2ab6e93f4f10", "status": "processing" }
```

Track progress with `GET /v1/cases/{case_id}/status`.

### GET `/v1/cases/{case_id}/plates`

Returns the license plates detected across the case's documents, enriched with VIN, motor number, owner name and — for plates registered in Estado de México — the current vigencia data (status, validity dates and previous plate, when applicable) from the official portal.

The list is built and persisted automatically as part of the case processing pipeline. This endpoint is read-only.

#### curl example

```bash
curl https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/plates \
  -H "x-api-key: your_api_key"
```

#### Response

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "internal_id": "EXP-2026-0001",
  "plates": [
    {
      "plate": "MJM626C",
      "entity": "MEX",
      "document_type": "vehicle_certificate",
      "document_date": "01/09/2025",
      "file_id": "0126537c-8dad-402a-a559-0bbbcafd53a3",
      "ocr_id": "f2dd2062-d11d-480b-9c0b-1b6d03b6f1df",
      "niv": "JN1BE6DSXS9121418",
      "motor_number": "QR258585700Q",
      "owner_name": null,
      "plate_status": "Vigente",
      "plate_valid_from": "01/09/2025",
      "plate_valid_until": "01/09/2030",
      "previous_plate": null,
      "previous_plate_lookup_status": null
    }
  ]
}
```

#### Field reference

| Field | Type | Description |
|---|---|---|
| `plate` | string | Normalized plate number (uppercase, no separators) |
| `entity` | string | Three-letter state code (`MEX`, `NLE`, `JAL`, …) |
| `document_type` | string | Source document: `certificate_title`, `tax_payment`, `alta_vehicular`, `vehicle_certificate`, `vehicle_plate`, `lumo_checklist` or `repuve` |
| `document_date` | string | Date associated with the source document (`DD/MM/YYYY` when available) |
| `niv` | string \| null | Vehicle VIN (17 chars when available, partial otherwise) |
| `motor_number` | string \| null | Engine number from the source document |
| `owner_name` | string \| null | Owner as declared in the source document |
| `plate_status` | string \| null | Current plate status returned by the portal: `Vigente`, `Vencida` or `Inactiva` |
| `plate_valid_from` | string \| null | Start of current plate validity (`DD/MM/YYYY`) |
| `plate_valid_until` | string \| null | End of current plate validity (`DD/MM/YYYY`) |
| `previous_plate` | string \| null | Previous plate number, when one was emitted before the current one |
| `previous_plate_lookup_status` | string \| null | Lookup state — see below |

#### `previous_plate_lookup_status` values

| Value | Meaning |
|---|---|
| `null` | The lookup does not apply (plate not registered in MEX) or the portal confirmed there is no previous plate |
| `"pending"` | Lookup in progress in background — re-query in a few seconds |
| `"found"` | Portal returned a previous plate (populated in `previous_plate`) |
| `"error"` | Lookup failed after retries (portal down, parsing error, etc.) |

The same plate may appear in multiple rows when it was detected in more than one document. All rows sharing the same plate value carry the same `plate_status`, vigencia dates and `previous_plate_*` fields once the background lookup finishes.

#### Common errors

| HTTP | Cause |
|---|---|
| `404` | Case not found, inactive, or plates have not been computed yet for this case |

### GET `/v1/cases/{case_id}/repuve`

Returns the most recent REPUVE job for the case. If the job ended in error but a valid response from placas.info is available, the endpoint auto-recovers and returns the correct data.

#### curl example

```bash
curl https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/repuve \
  -H "x-api-key: your_api_key"
```

#### Response

```json
{
  "codigo": "ok",
  "info": {
    "niv": "3N6AD35A9LK875826",
    "placa": "Y83BGS",
    "marca": "NISSAN",
    "modelo": "NP300/NP300 FRONTIER/FRONTIER",
    "tipo": "CAB. Y CHASIS ESTACAS",
    "clase": "CAMIONETA",
    "anio_modelo": 2020,
    "entidad_emplacado": "CIUDAD DE MEXICO",
    "fecha_inscripcion": "22/09/20",
    "fecha_actualizacion": "12/10/24",
    "reporte_robo": [
      { "tipo_reporte": "FGJ", "estatus": "SIN REPORTE DE ROBO" },
      { "tipo_reporte": "OCRA", "estatus": "SIN REPORTE DE ROBO" }
    ],
    "robo_usa_can": { "tiene_robo": false },
    "aviso_judicial": { "tiene_aviso_judicial": false }
  },
  "job_id": "4a201139-7945-4ae7-8a17-dc8e048c7437",
  "job_status": "completed"
}
```

#### Field reference

| Field | Type | Description |
|---|---|---|
| `codigo` | string | `ok` — REPUVE data available. `error` — all sources failed. `processing` — job still in progress. |
| `info` | object \| null | Vehicle registration data from REPUVE. `null` when the vehicle is not registered or the job has not completed yet. |
| `info.niv` | string | Vehicle identification number (VIN) |
| `info.placa` | string \| null | License plate on record in REPUVE |
| `info.marca` | string | Make |
| `info.modelo` | string | Model |
| `info.anio_modelo` | integer | Model year |
| `info.entidad_emplacado` | string \| null | State where the plate was registered |
| `info.fecha_inscripcion` | string \| null | REPUVE registration date |
| `info.fecha_actualizacion` | string \| null | Last update date in REPUVE |
| `info.reporte_robo` | array | Theft reports from FGJ and OCRA sources |
| `info.robo_usa_can` | object | USA/Canada theft report status |
| `info.aviso_judicial` | object | Judicial notice status |
| `job_id` | UUID | Internal job identifier |
| `job_status` | string | `completed`, `error`, `processing` or `pending` |

#### Common errors

| HTTP | Cause |
|---|---|
| `404` | Case not found, inactive, or no REPUVE query has been run for this case |

### GET `/v1/cases/{case_id}/pedimento`

Returns the most recent customs entry (pedimento) result for the case. Data is read from `pedimento_responses` (legacy pipeline) and `processing_jobs` (v1 API), deduplicated by content, and sorted by recency.

#### curl example

```bash
curl https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/pedimento \
  -H "x-api-key: your_api_key"
```

#### Response

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "job_id": "4a201139-7945-4ae7-8a17-dc8e048c7437",
  "status": "completed",
  "source": "processing_jobs",
  "result": {
    "success": true,
    "data": { "pedimento": "39317003384", "aduana": "MANZANILLO", "vin": "LUCGM6669J3104807" }
  }
}
```

#### Common errors

| HTTP | Cause |
|---|---|
| `404` | Case not found, inactive, or no pedimento query has been run for this case. Call `POST /v1/cases/{id}/pedimento` first. |

### GET `/v1/cases/{case_id}/taxes` (without `?plate`)

Returns all tenencia results for every plate in the case, grouped by plate. Results are read from `processing_jobs` across all sources (background pipeline and on-demand v1).

When `?plate` is supplied the original single-plate behavior is used unchanged — see the existing documentation.

#### curl example

```bash
curl https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/taxes \
  -H "x-api-key: your_api_key"
```

#### Response

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "plates_count": 2,
  "taxes": [
    {
      "plate": "MES452A",
      "job_id": "abc123",
      "status": "completed",
      "source": "api_v1_cases_taxes_bulk",
      "result": { "codigo": "ok", "info": [...] },
      "created_at": "2026-06-05T10:00:00+00:00"
    }
  ]
}
```

### POST `/v1/cases/{case_id}/taxes`

Triggers a bulk tenencia lookup for all unique plates in the case (main plates + previous plates), using `GET /v1/cases/:id/plates` data internally (no HTTP). One `processing_jobs` child row is created per plate, each with its own `job_id`, `external_payload` and `external_response`. The workflow runs in Temporal (`invoice-background-queue`).

#### curl example

```bash
curl -X POST https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/taxes \
  -H "x-api-key: your_api_key"
```

#### Response `202 Accepted`

```json
{
  "job_id": "31b4ecf2-9a0b-4f98-8a91-2ab6e93f4f10",
  "plates": ["MES452A", "NDJ3622"],
  "status": "pending"
}
```

Poll `GET /v1/cases/{case_id}/taxes` (without `?plate`) to see the results per plate as they complete.

#### Common errors

| HTTP | Cause |
|---|---|
| `400` | No plates found for this case. Call `POST /v1/cases/{id}/plates` first. |
| `404` | Case not found or inactive |

### POST `/v1/cases/{case_id}/pedimento`

Triggers a new customs entry lookup using data extracted from the OCR of the case's primary document (the invoice linked to the main VIN). Runs asynchronously in Temporal. Returns a `job_id` immediately; poll `GET /v1/cases/{id}/pedimento` for the result.

The required parameters (`aduana`, `ano_vehiculo`) are read from the OCR automatically. If they are not present in the document, the endpoint returns a `400` listing the missing fields.

#### curl example

```bash
curl -X POST https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/pedimento \
  -H "x-api-key: your_api_key"
```

#### Response `202 Accepted`

```json
{ "job_id": "4a201139-7945-4ae7-8a17-dc8e048c7437", "status": "pending" }
```

#### Common errors

| HTTP | Cause |
|---|---|
| `400` | No primary VIN found, or required OCR fields (`aduana`, `ano_vehiculo`) are missing |
| `404` | Case not found or inactive |

### GET `/v1/cases/{case_id}/rapi`

Returns the most recent RAPI (FGJCDMX criminal activity registry) lookup result for the case. Data is read from `rapi_responses` (legacy pipeline) and `processing_jobs` (v1 API), deduplicated by content, and sorted by recency.

#### curl example

```bash
curl https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/rapi \
  -H "x-api-key: your_api_key"
```

#### Response

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "job_id": "4a201139-7945-4ae7-8a17-dc8e048c7437",
  "status": "completed",
  "source": "processing_jobs",
  "result": { ... }
}
```

#### Common errors

| HTTP | Cause |
|---|---|
| `404` | Case not found, inactive, or no RAPI query has been run. Call `POST /v1/cases/{id}/rapi` first. |

### POST `/v1/cases/{case_id}/rapi`

Triggers a RAPI lookup for the case's primary VIN (from `GET /v1/cases/:id/vins`). Runs asynchronously in Temporal (`invoice-background-queue`). Returns a `job_id` immediately.

#### curl example

```bash
curl -X POST https://api.nexcar.mx/v1/cases/7040fd87-5f49-4187-b2a3-b4a19670825c/rapi \
  -H "x-api-key: your_api_key"
```

#### Response `202 Accepted`

```json
{ "job_id": "4a201139-7945-4ae7-8a17-dc8e048c7437", "vin": "3N6AD35A9LK875826", "status": "pending" }
```

#### Common errors

| HTTP | Cause |
|---|---|
| `400` | No primary VIN found. Process documents first with `POST /v1/cases/{id}/process`. |
| `404` | Case not found or inactive |

---

## Documents

_Upload individual files to a case, fetch their OCR and reclassify them when needed._

### Endpoints

| Method | Path | Description |
|---|---|---|
| `POST` | `/v1/documents` | Upload a document to a case |
| `GET` | `/v1/documents/{document_id}` | Read metadata and OCR for a document |
| `GET` | `/v1/documents/{document_id}/extraction-data` | Advanced extraction (validity, QR/barcodes, owner info) |
| `DELETE` | `/v1/documents/{document_id}` | Soft-delete a document |
| `POST` | `/v1/documents/{document_id}/restore` | Restore a soft-deleted document |
| `PATCH` | `/v1/documents/{document_id}/ocr` | Inject OCR manually |
| `POST` | `/v1/documents/{document_id}/classify` | Re-classify the document (manual or automatic) |
| `POST` | `/v1/documents/{document_id}/process` | Trigger automatic OCR |
| `POST` | `/v1/documents/{document_id}/reclassify` | Change the type and re-process |

### POST `/v1/documents` — Upload a document

Uploads a file and attaches it to a case.

#### Body

| Field | Type | Required | Description |
|---|---|---|---|
| `case_id` | UUID | Yes | Case the document belongs to |
| `mime_type` | string | Yes | File MIME (see supported types) |
| `url` | string | Conditional | Public URL of the file. **XOR** with `base64`. |
| `base64` | string | Conditional | File content in base64. **XOR** with `url`. |
| `type` | string | No | Document type (see [Document types](/docs/en/document-types)). If omitted, classification runs automatically. |
| `parent_file_id` | UUID | No | Parent document inside the same case (e.g. an invoice attachment). |

> **Size limit**: 20 MB per file. You must send **exactly one** of `url` or `base64`.

#### Supported MIME types

`application/pdf` · `application/xml` · `image/jpeg` · `image/jpg` · `image/png` · `image/tiff` · `image/tif` · `image/x-tiff`

#### curl example (URL)

```bash
curl -X POST https://api.nexcar.mx/v1/documents \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
    "url": "https://your-storage.example.com/repuve-certificate.pdf",
    "mime_type": "application/pdf",
    "type": "repuve"
  }'
```

#### curl example (base64)

```bash
BASE64=$(base64 -i ./invoice.pdf)
curl -X POST https://api.nexcar.mx/v1/documents \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d "{
    \"case_id\": \"7040fd87-5f49-4187-b2a3-b4a19670825c\",
    \"base64\": \"$BASE64\",
    \"mime_type\": \"application/pdf\",
    \"type\": \"factura\"
  }"
```

#### Response `201`

```json
{
  "document_id": "abc123",
  "url": "https://...nexcar.mx/storage/.../repuve-certificate.pdf"
}
```

#### Errors

| HTTP | Code | Cause |
|---|---|---|
| `400` | `MISSING_PARAMETER` | Missing `case_id`, `mime_type` or the file source |
| `400` | `VALIDATION_ERROR` | Invalid UUID, unsupported MIME, file > 20 MB, both `url` and `base64` |
| `404` | `RESOURCE_NOT_FOUND` | The `case_id` or `parent_file_id` does not exist |

### GET `/v1/documents/{document_id}`

Returns metadata, OCR and extracted data for the document.

#### curl example

```bash
curl https://api.nexcar.mx/v1/documents/abc123 \
  -H "x-api-key: your_api_key"
```

#### Response `200`

```json
{
  "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
  "url": "https://...nexcar.mx/storage/.../invoice.pdf",
  "mime_type": "application/pdf",
  "type": "factura",
  "json_ocr": { "...": "..." },
  "parsed_data": {
    "vin": "3VWFE21C04M000001",
    "rfc_emisor": "ABC010101AAA",
    "monto_total": 285000.00,
    "fecha_emision": "2024-08-15"
  },
  "validity": {
    "document_validity": "vigente",
    "codes": ["QR detectado"],
    "userInfo": { "nombre": "JUAN PÉREZ GARCÍA" }
  }
}
```

`json_ocr` and `parsed_data` are `null` while OCR has not finished. Instead of polling, configure a [webhook](/docs/en/webhooks).

### POST `/v1/documents/{document_id}/classify`

Sets or changes the document type. Useful when you uploaded the file without a `type` and want to classify it manually, or when you want to force a fresh automatic detection.

```bash
curl -X POST https://api.nexcar.mx/v1/documents/abc123/classify \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "type": "tenencia" }'
```

If you omit the body, automatic classification runs against the file's content.

### POST `/v1/documents/{document_id}/reclassify`

Changes the document type **and** re-runs OCR with the new classification. The response is `202 Accepted` with the `job_id` of the new processing job.

```bash
curl -X POST https://api.nexcar.mx/v1/documents/abc123/reclassify \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "type": "factura_xml" }'
```

---

## Webhooks

_Get HTTP notifications when a document or case changes state._

### How they work

When a document or case advances through processing (classification, OCR, data extraction, validation), Nexcar sends an HTTP `POST` to the URL you configured, with a JSON payload describing the event.

Configure your receiving URL with your Nexcar contact.

### Available events

| Event | When it fires |
|---|---|
| `document.classified` | A document type was set (or changed) |
| `document.ready` | The document's OCR finished and the data is available |
| `document.validity` | Validity / code extraction finished |
| `case.upload.completed` | The initial upload of files for a case finished |
| `case.status.changed` | The case state changed |

### Payload structure

Every event shares the same envelope:

```json
{
  "event": "document.ready",
  "occurred_at": "2026-04-09T23:04:18.515738Z",
  "data": {
    "case_id": "7040fd87-5f49-4187-b2a3-b4a19670825c",
    "document_id": "abc123",
    "type": "factura",
    "parsed_data": {
      "vin": "3VWFE21C04M000001",
      "monto_total": 285000
    }
  }
}
```

### Verification

Each request includes the `x-nexcar-signature` header with an HMAC SHA-256 computed over the raw body using the webhook secret we share with you. Verify it like this (Node.js):

```js
const crypto = require("crypto");

function verify(req, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(req.rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(req.headers["x-nexcar-signature"])
  );
}
```

### Retries

If your endpoint replies with `>= 400` or doesn't respond within **10 seconds**, we retry with exponential backoff up to 5 times across 24 hours. After that the event is dropped and recorded in your audit log.

> Reply with `2xx` as fast as possible and process the event asynchronously on your side.

### Idempotency

Every event includes a unique identifier in `data.event_id`. Your system must ignore repeat events in case of retries.

---

## Document types

_Catalog of types you can assign when uploading a document. If you omit the type, Nexcar tries to detect it automatically._

### Catalog

| Type (`type`) | Description | Typical extracted data |
|---|---|---|
| `factura` | Invoice (PDF) or CFDI XML for the vehicle | VIN, issuer/recipient RFC, total amount, date |
| `factura_xml` | CFDI 4.0 XML | UUID, RFC, totals, line items |
| `ine` | Mexican INE/IFE official ID of the owner | Name, CURP, validity, voter key |
| `pasaporte` | Mexican or foreign passport | Name, date of birth, validity |
| `repuve` | REPUVE certificate | VIN, make, model, year, vehicle status |
| `tenencia` | State vehicle-tax payment receipt | Year, amount, payment date, plate |
| `placas` | Circulation card or plates certificate | Plate, validity, owner |
| `comprobante_domicilio` | Utility bill (electricity, water, phone, property) | Address, holder, issue date |
| `comprobante_pago` | Bank receipt / SPEI | Amount, bank, date, beneficiary |
| `pedimento` | Customs declaration (imported vehicles) | Pedimento number, customs office, date |
| `verificacion` | Vehicle emissions inspection certificate | Hologram, validity, plate |
| `multas` | Traffic violations clearance | Outstanding fines, issue date |
| `acta_nacimiento` | Owner's birth certificate | Name, CURP, date and place |
| `comprobante_ingresos` | Bank statement or pay stub | Holder, period, amount |
| `responsiva` | Signed transfer-of-responsibility letter | Buyer, seller, date, VIN |

### Rules

- If you upload a document **with** `type`, Nexcar respects your classification but may flag inconsistencies.
- If you upload **without** `type`, automatic content-based classification runs. The result arrives via the `document.classified` webhook.
- You can change the type at any time with `POST /v1/documents/{id}/classify` or `/reclassify` (the latter re-runs OCR).

### Linked documents

Some types accept a **parent document** through `parent_file_id`. For instance:

- A `factura_xml` can be linked to its matching `factura` (PDF).
- A `comprobante_pago` can be linked to the `tenencia` it pays for.

This way, lookups return coherent groups of related documents.

---

## Errors

_Standard error format and a quick guide to the most common codes._

### Format

Every error uses the same JSON envelope:

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "case_id is not a valid UUID",
    "details": { "field": "case_id" }
  }
}
```

- `code` is stable: your system **must** branch on it.
- `message` is human-readable and may change (do not parse it).
- `details` is optional and specific to the error.

### Common codes

#### 4xx — client errors

| HTTP | `code` | Meaning | Suggested action |
|---|---|---|---|
| `400` | `MISSING_PARAMETER` | A required field is missing | Review the request body |
| `400` | `VALIDATION_ERROR` | A field has an invalid format | Check `details.field` |
| `400` | `DUPLICATE_INTERNAL_ID` | A case already exists for that `internal_id` | Reuse the `case_id` returned in `details` |
| `401` | `UNAUTHORIZED` | Missing or expired credentials | Refresh your token or check your API key |
| `401` | `INVALID_CREDENTIALS` | Invalid credentials | Make sure you are using the correct key for the environment |
| `403` | `FORBIDDEN` | No permission on the resource | Contact support to review your access |
| `404` | `RESOURCE_NOT_FOUND` | Case or document does not exist | Verify the ID or whether it was soft-deleted |
| `409` | `CONFLICT` | The resource is in a state incompatible with the operation | Read the current state and retry |
| `413` | `PAYLOAD_TOO_LARGE` | File > 20 MB | Reduce the file size and retry |
| `415` | `UNSUPPORTED_MEDIA_TYPE` | MIME not supported | Use a MIME from the catalog |
| `422` | `INVALID_STATUS` | The `status` value is not part of your catalog | Use a value from the configured catalog |
| `429` | `RATE_LIMITED` | Rate limit exceeded | Apply backoff and retry |

#### 5xx — server errors

| HTTP | `code` | Meaning |
|---|---|---|
| `500` | `INTERNAL_ERROR` | Unexpected error on Nexcar's side |
| `502` | `UPSTREAM_ERROR` | An external service failed (OCR, storage) |
| `503` | `SERVICE_UNAVAILABLE` | Maintenance in progress |

> For `5xx`, retry with exponential backoff. If it persists for more than 5 minutes, email [support@nexcar.mx](mailto:support@nexcar.mx) with the `request_id` from the response headers.
