# Dónde va cada parámetro: consulta, cuerpo o ruta

Dónde espera cada endpoint de la API externa sus parámetros (en la ruta, en la consulta o en el cuerpo) para evitar errores 400 y 404.

Cada endpoint de la API externa espera sus parámetros en un sitio concreto: en la ruta, en la consulta (lo que va detrás de `?`) o en el cuerpo. Esta tabla te dice cuál es cada uno y te ahorra la mayoría de los errores `400` y `404` de las primeras integraciones.

> **Atención: Un parámetro en el sitio equivocado se pierde sin error**
>
> La causa número uno de errores en integraciones nuevas es enviar `userId` en el cuerpo cuando el endpoint lo espera en la consulta, o al revés. La API descarta los campos que no espera sin devolver error, así que el fallo aparece después, casi siempre como `404 USER_NOT_FOUND`. Lo único que te avisa es una cabecera de la respuesta, que se explica justo debajo.

En las escrituras (`POST`, `PUT`, `PATCH` y `DELETE`) de los endpoints con cuerpo declarado, la respuesta lo avisa: trae la cabecera `X-Utilia-Ignored-Fields` con los nombres de los campos del cuerpo que se han descartado, separados por comas (hasta 20, nunca sus valores), por ejemplo `X-Utilia-Ignored-Fields: userId, lines[0].taxtype`. Si la ves, es casi siempre un nombre mal escrito o un dato en el sitio equivocado: búscalo en la tabla. Para saberlo antes de enviar nada, `POST /api/external/v1/diagnostics/check-request` comprueba la petición sin ejecutarla y devuelve esos campos en `validation.ignoredFields`; lo explica [Comprobar una petición sin ejecutarla](https://os.utilia.ai/docs/developers/getting-started/check-request#comprobar-una-petición-sin-ejecutarla).

## Cómo leer la tabla

- **path** (ruta): parte de la dirección, escrita como `:nombre` o `{nombre}`. Por ejemplo, `/tickets/:id/messages`.
- **query** (consulta): lo que va detrás de `?`. Por ejemplo, `?userId=usr-1&page=2`.
- **body** (cuerpo): el JSON de la petición, solo en `POST`, `PATCH` y `PUT`.

Una celda con «—» significa que el endpoint no usa ese tipo de parámetro.

## Tickets

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/tickets` | POST | `body.user.externalId` (crea o actualiza el usuario) | — | `{ user: { externalId, email?, name?, metadata? }, title, description, category?, priority?, context?, attachmentIds? }` |
| `/tickets` | GET | `query` (obligatorio) | `query.status`, `query.page`, `query.limit` | — |
| `/tickets/unread-count` | GET | `query` (obligatorio) | — | — |
| `/tickets/events` | GET (SSE) | `query` (opcional, filtra eventos del usuario) | — | — |
| `/tickets/{id}` | GET | `query` (obligatorio) | `path.id` | — |
| `/tickets/{id}/messages` | POST | `query` (obligatorio) | `path.id` | `{ content, attachmentIds? }` |
| `/tickets/{id}/close` | PATCH | `query` (obligatorio) | `path.id` | **No lleva** (si lo envías, se ignora) |
| `/tickets/{id}/reopen` | PATCH | `query` (obligatorio) | `path.id` | **No lleva** (si lo envías, se ignora) |

## Sugerencias IA

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/tickets/ai-suggest` | POST | `body.userId` (el usuario tiene que existir) | — | `{ userId, text?, audioBase64?, audioFilename?, images?, source? }` |
| `/tickets/ai-suggest/{jobId}/stream` | GET (SSE) | (el del trabajo; con EventSource, un pase de conexión en `query.streamToken`, nunca la clave) | `path.jobId` | — |
| `/tickets/ai-suggest/{jobId}` | GET | (el del trabajo) | `path.jobId` | — |
| `/tickets/ai-track-action` | POST | (no requiere usuario) | — | `{ eventId, userAction, fieldsAccepted?, fieldsModified?, fieldsRejected?, timeToDecisionMs?, source? }` |
| `/tickets/ai-transcribe` | POST | `body.userId` | — | `{ userId, audioBase64, audioFilename }` |
| `/tickets/ai-transcribe-async` | POST | `body.userId` | — | `{ userId, audioBase64, audioFilename }` |
| `/tickets/ai-transcribe/{jobId}/stream` | GET | (el del trabajo) | `path.jobId` | — |
| `/tickets/ai-transcribe/{jobId}` | GET | (el del trabajo) | `path.jobId` | — |

## Errores de tu aplicación

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/errors/report` | POST | (no requiere usuario; `body.userId` es el de tu sistema y es opcional) | — | `{ message, module, severity?, stack?, file?, line?, function?, endpoint?, method?, userId?, requestId?, context?, release?, environment?, fingerprint?, tags? }` |
| `/errors/batch` | POST | (no requiere usuario) | — | `{ errors: [ ...de 1 a 25, como /errors/report ] }` |
| `/errors` | GET | (no requiere usuario) | `query.severity` (repetido para varias), `query.status`, `query.module`, `query.resolved`, `query.firstRelease`, `query.from`, `query.to`, `query.limit`, `query.offset` | — |
| `/errors/stats` | GET | (no requiere usuario) | — | — |
| `/errors/{hash}` | GET | (no requiere usuario) | `path.hash` | — |
| `/errors/{hash}/resolve` | POST | (no requiere usuario) | `path.hash` | `{ note? }` |
| `/errors/{hash}/reopen` | POST | (no requiere usuario) | `path.hash` | — |
| `/errors/{hash}/silence` | POST y DELETE | (no requiere usuario) | `path.hash` | POST: `{ hours }`, de 1 a 672 |
| `/tickets/client-errors` | POST | (no requiere usuario; se asocia a la aplicación) | — | `{ errors: [{ message, stack?, code?, severity?, endpoint?, method?, requestId?, component?, url?, userAgent?, release?, environment? }] }` |

## Usuarios externos

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/users/identify` | POST | `body.externalId` | — | `{ externalId, email?, name?, avatarUrl?, metadata?, taxId?, billingAddress?, clientId? }` |
| `/users` | GET | (no requiere usuario: lista los de la aplicación) | `query.page`, `query.limit` | — |

## Archivos

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/files/upload` | POST | (no aplica) | — | multipart/form-data con `file` (binario, requerido) y, opcionalmente, `name` y `metadata`. No acepta `userId`: cualquier campo no declarado se descarta |
| `/files/quota` | GET | — | — | — |
| `/files/{id}/url` | GET | — | `path.id` | — |

## Facturación

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/invoices` | POST | `body.user.externalId` (crea o actualiza el usuario) | — | `{ user, recipient, lines, taxSystem?, issueDate?, dueDate?, idempotencyKey?, ... }` |
| `/invoices` | GET | `query.userId` (opcional, filtra) | `query.status`, `query.fromDate`, `query.toDate`, `query.page`, `query.limit` | — |
| `/invoices/stats` | GET | `query.userId` (obligatorio) | — | — |
| `/invoices/{id}` | GET | `query.userId` (obligatorio) | `path.id` | — |
| `/invoices/{id}/pdf` | GET | `query.userId` (obligatorio) | `path.id` | — |
| `/invoices/{id}/cancel` | POST | `query.userId` (obligatorio) | `path.id` | `{ reason? }` (opcional) |
| `/invoices/{id}/pay` | POST | `body.userId` | `path.id` | `{ userId, paymentMethodId? }` |
| `/invoices/{id}/payment-intents` | GET | `query.userId` | `path.id` | — |

## Métodos de pago

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/payment-methods/setup-intent` | POST | `body.user.externalId` (crea o actualiza el usuario) | — | `{ user, ... }` |
| `/payment-methods` | GET | `query.userId` (obligatorio) | — | — |
| `/payment-methods/{pmId}/set-default` | POST | `body.userId` | `path.pmId` | `{ userId }` |
| `/payment-methods/{pmId}` | DELETE | `query.userId` (obligatorio) | `path.pmId` | — |

## Presupuestos

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/budgets/{budgetId}/comments` | GET | (no requiere usuario) | `path.budgetId` y filtros en la consulta | — |
| `/budgets/{budgetId}/comments` | POST | (no requiere usuario) | `path.budgetId` | `{ content, ... }` |
| `/budgets/{budgetId}/signatures` | GET | (no requiere usuario) | `path.budgetId` | — |
| `/budgets/{budgetId}/signatures/{sigId}` | GET | — | `path.budgetId`, `path.sigId` | — |
| `/budgets/{budgetId}/signatures/{sigId}/verify` | GET | — | `path.budgetId`, `path.sigId` | — |

## Analítica de IA

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/ai-analytics/overview` | GET | (no requiere usuario: agrega toda la aplicación) | `query.startDate?`, `query.endDate?` | — |
| `/ai-analytics/trends` | GET | (no requiere usuario) | `query.startDate?`, `query.endDate?` | — |

## Avisos (API del panel)

| Endpoint | Método | userId | Otros parámetros | Body |
| - | - | - | - | - |
| `/integrations/apps/{id}/webhook-deliveries` | GET | (no aplica) | `path.id`, filtros en la consulta | — |

Los endpoints `/integrations/...` no son de la API externa: no aceptan `X-Api-Key` y se llaman con la sesión de una persona del panel. Están aquí solo como referencia; el detalle está en [Entregas y reintentos de los avisos](https://os.utilia.ai/docs/developers/webhooks/delivery-and-retries).

## Antes y después: dos ejemplos prácticos

### Ejemplo 1: añadir un mensaje a un ticket

El error más frecuente es enviar `userId` en el cuerpo cuando el endpoint lo espera en la consulta.

**Mal: `userId` en el cuerpo**

```bash
curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/abc-123/messages' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_xxx' \
  -d '{
    "userId": "usr-4821",
    "content": "Sigue sin funcionar"
  }'
# Resultado: el "userId" del cuerpo se descarta (la API elimina los campos
# que no espera y lo dice en la cabecera X-Utilia-Ignored-Fields: userId).
# Como no llega en la consulta, la respuesta es 404 USER_NOT_FOUND.
```

**Bien: `userId` en la consulta y `content` en el cuerpo**

```bash
curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/abc-123/messages?userId=usr-4821' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_xxx' \
  -d '{
    "content": "Sigue sin funcionar"
  }'
# Resultado: 201 con el mensaje creado
```

### Ejemplo 2: cerrar un ticket

`PATCH /:id/close` no espera cuerpo. Si tu cliente HTTP envía uno, se ignora sin aviso: los datos que pases no se guardan.

**Mal: pasar `reason` en el cuerpo esperando que se guarde**

```bash
curl -X PATCH 'https://os.utilia.ai/api/external/v1/tickets/abc-123/close?userId=usr-4821' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_xxx' \
  -d '{ "reason": "El cliente lo resolvió por su cuenta" }'
# Resultado: 200 (ticket cerrado), pero "reason" se descarta sin avisar.
```

**Bien: dejar el motivo como mensaje antes de cerrar**

```bash
# Paso 1: añadir el comentario
curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/abc-123/messages?userId=usr-4821' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_xxx' \
  -d '{ "content": "Cerrado: el cliente lo resolvió por su cuenta" }'

# Paso 2: cerrar
curl -X PATCH 'https://os.utilia.ai/api/external/v1/tickets/abc-123/close?userId=usr-4821' \
  -H 'X-Api-Key: utl_xxx'
# Resultado: 200, con el contexto registrado en el hilo del ticket.
```

## Patrones generales

1. **Los endpoints que aceptan `user` como objeto en el cuerpo** (`POST /tickets`, `POST /invoices`, `POST /payment-methods/setup-intent`) crean o actualizan el usuario externo por su cuenta: no necesitan `/users/identify` antes.
2. **Los que aceptan `userId` suelto en la consulta o en el cuerpo** dan por hecho que el usuario existe; si no, responden `404 USER_NOT_FOUND`. Llama antes a `POST /users/identify`.
3. **Los `GET` con datos de un usuario** lo reciben siempre en la consulta, nunca en el cuerpo (HTTP no garantiza que se lea el cuerpo de un `GET`).
4. **Los `PATCH` de cambio de estado** (`/close`, `/reopen`) llevan el recurso en la ruta y `userId` en la consulta. No esperan cuerpo.
5. **Los `POST` de acciones sobre un recurso** (`/messages`, `/cancel`, `/pay`, `/set-default`) suelen identificar a quien actúa en la ruta o en la consulta y llevar los datos de la acción en el cuerpo.

## Siguiente paso

Repasa los otros fallos habituales en [Errores frecuentes al integrar](https://os.utilia.ai/docs/developers/getting-started/common-mistakes) y el significado de cada código en [Errores, identificador de petición y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries).
