Ir al contenido

Errores

Cuando una petición falla, la API responde con un documento application/problem+json (RFC 9457), siempre con la misma forma y siempre en español:

{
"type": "https://docs.emitay.com/errors/invalid_request",
"title": "La petición no es válida",
"status": 422,
"detail": "Revisa 3 campos: customer.document_type, items.0.unit_price, items.0.unitPrice. El motivo de cada uno está en errors",
"errors": [
{
"field": "customer.document_type",
"message": "Indica el tipo de documento (catálogo 06 de SUNAT: \"1\" DNI, \"4\" carné de extranjería, \"7\" pasaporte, \"0\" sin documento…): solo se deduce cuando el número es un RUC válido"
},
{ "field": "items.0.unit_price", "message": "Es obligatorio" },
{
"field": "items.0.unitPrice",
"message": "Campo no reconocido. Los campos de la API se escriben en snake_case: prueba con unit_price"
}
]
}
Campo Qué es
type La dirección de la página que explica el error. Su último tramo es el código (invalid_request): compara contra él, no contra los textos
title El error en una frase. Es el mismo para todas las respuestas con ese código
status El estado HTTP, repetido en el cuerpo
detail Qué pasó en esta petición en concreto
errors Cuando el problema es de campos: una entrada por campo, con su ruta (items.0.unit_price) y el motivo

Un 422 no dice «petición inválida» y te deja buscando. Dice qué campo, por qué, y a veces cómo arreglarlo: un nombre en camelCase recibe su forma en snake_case; un código fuera de catálogo, la lista de los válidos. Esos mensajes se pueden mostrar tal cual a quien llenó el formulario.

Y un 422 nunca consume número: lo que SUNAT rechazaría se niega antes de numerar. La serie no queda con huecos por un error de validación.

Todo lo que el cliente lanza extiende EmitayError:

import { EmitayApiError, EmitayConnectionError } from "emitay";
try {
await emitay.invoices.create(body);
} catch (error) {
if (error instanceof EmitayApiError) {
error.status; // 422
error.code; // "invalid_request"
error.detail; // el motivo, en español
error.fields; // { "customer.document_type": "Indica el tipo de documento…" }
error.requestId; // "req_…": cítalo al pedir soporte
} else if (error instanceof EmitayConnectionError) {
// No hubo respuesta tras los reintentos: red caída o tiempo agotado.
}
}

La lista es cerrada: la API no responde con ningún otro. Cada código enlaza a su página, que es la dirección que trae type.

EstadoCódigoQué pasó
400bad_requestNo se pudo leer la petición
401unauthorizedFalta la llave de API o no es válida
403forbiddenNo tienes permiso para hacer esto
404not_foundNo existe el recurso
409conflictLa petición choca con el estado actual del recurso
409number_already_usedEse número de comprobante ya existe
409company_not_configuredLa empresa no está lista para producción
413payload_too_largeEl cuerpo de la petición es demasiado grande
415unsupported_media_typeEl cuerpo de la petición debe ser JSON
422invalid_requestLa petición no es válida
422invalid_documentEl comprobante no es válido
429rate_limitedDemasiadas peticiones
500internal_errorError inesperado
501not_implementedAún no está implementado
  • Request-Id: toda respuesta, también las de error, lleva esta cabecera (req_…).

  • Registro de peticiones: Emitay guarda 30 días el método, la ruta, el estado, la duración y el código de error de cada petición autenticada (nunca el cuerpo).

    Ventana de terminal
    npx emitay logs --errors
    2026-10-08 17:40:12 POST /v1/invoices 422 invalid_request 4 ms req_01m4…
  • Los rechazos de SUNAT no son errores HTTP. Un comprobante que SUNAT rechaza se creó (201) y queda rejected, con el código y el mensaje de SUNAT en sunat. Ver El veredicto de SUNAT.