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.
Actualizado el . 5 min de lectura
Incluido en todos los planes. Ver precios
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.
Un parámetro en el sitio equivocado se pierde sin aviso
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.
Cómo leer la tabla
- path (ruta): parte de la dirección, escrita como
:nombreo{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,PATCHyPUT.
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, apiKey en la consulta) | 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 del cliente
| Endpoint | Método | userId | Otros parámetros | Body |
|---|---|---|---|---|
/tickets/client-errors | POST | (no requiere usuario; se asocia a la aplicación) | — | { errors: [{ message, stack?, code?, severity?, endpoint?, method?, requestId?, component?, url?, userAgent? }] } |
/errors/report | POST | (no requiere usuario) | — | { message, module, severity?, stack?, file?, line?, function?, endpoint?, method?, userId?, requestId?, context? } |
/errors | GET | (no requiere usuario) | query.severity[], query.module, query.resolved, query.from, query.to, query.limit, query.offset | — |
/errors/stats | GET | (no requiere usuario) | — | — |
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.
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
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 sin aviso (la API elimina
# los campos que no espera). Como no llega en la consulta, la respuesta es
# 404 USER_NOT_FOUND.Bien: userId en la consulta y content en el cuerpo
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 creadoEjemplo 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
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
# 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
- Los endpoints que aceptan
usercomo 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/identifyantes. - Los que aceptan
userIdsuelto en la consulta o en el cuerpo dan por hecho que el usuario existe; si no, responden404 USER_NOT_FOUND. Llama antes aPOST /users/identify. - Los
GETcon datos de un usuario lo reciben siempre en la consulta, nunca en el cuerpo (HTTP no garantiza que se lea el cuerpo de unGET). - Los
PATCHde cambio de estado (/close,/reopen) llevan el recurso en la ruta yuserIden la consulta. No esperan cuerpo. - Los
POSTde 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 y el significado de cada código en Errores, identificador de petición y reintentos.