Empezar
Errores
Todos los errores de la API tienen la misma forma, sea cual sea el endpoint:
{ "error": { "code": "totals_mismatch", "message": "El valor declarado en totales.importe_total (115.00) no coincide con el calculado (115.50).", "details": { "campo": "totales.importe_total", "declarado": 115.00, "calculado": 115.50 }, "request_id": "c0a8f3e2-4b91-4d77-8a1e-9f2c6b5d0e31", "doc_url": "https://olympus.atharix.com/docs/empezar/errores#totals_mismatch" }}| Campo | Qué es |
|---|---|
code | Identificador estable. Programa contra esto, no contra el mensaje |
message | Explicación en español, pensada para que una persona la lea |
details | Datos del error: el campo afectado, los valores en conflicto, lo que falta |
request_id | Identificador de la petición. También viene en la cabecera X-Request-Id |
doc_url | Enlace a la sección de esta página que explica ese código |
Errores de autenticación y permisos
Sección titulada «Errores de autenticación y permisos»invalid_api_key
Sección titulada «invalid_api_key»401. Falta la cabecera X-API-Key o la clave no existe.
Comprueba que envías la cabecera, que la clave está completa (empieza por dlt_ y tiene 44 caracteres)
y que no se coló un espacio o un salto de línea al leerla del entorno.
api_key_revoked
Sección titulada «api_key_revoked»401. La clave fue revocada desde Configuración → Integraciones. La revocación es definitiva: hay que crear una clave nueva.
api_key_expired
Sección titulada «api_key_expired»401. La clave pasó su fecha de caducidad. Si venía de una rotación, terminó el periodo de gracia y tu sistema sigue usando la vieja: despliega la nueva.
api_key_disabled
Sección titulada «api_key_disabled»401. La clave está desactivada (no revocada). Se vuelve a activar desde la web.
ip_not_allowed
Sección titulada «ip_not_allowed»403. La clave tiene lista de IPs y la tuya no está. details.ip dice desde qué dirección llegó la
petición; añádela a la clave o quita la restricción.
insufficient_scope
Sección titulada «insufficient_scope»403. La clave no tiene el permiso necesario. details.required dice cuál falta y details.granted
los que tiene. Añádelo desde la web.
business_not_ready
Sección titulada «business_not_ready»403. El negocio no puede emitir todavía. details.requisitos_faltantes dice qué falta y
details.como_resolverlo, dónde se arregla:
| Requisito | Qué falta |
|---|---|
verifactu_enabled | (España) Activar VERI*FACTU en Configuración → VERI*FACTU |
electronic_signature | Subir el certificado en Configuración → Fiscal |
signature_password | Registrar la contraseña del certificado |
software_producer | (España) Configuración de la plataforma; contacta con soporte |
Esto no se resuelve por la API: el negocio lo configura en la aplicación web.
quota_exceeded
Sección titulada «quota_exceeded»403. El negocio agotó los comprobantes que incluye su plan este mes. El cupo se renueva el día 1, o el negocio puede mejorar el plan.
jurisdiction_not_supported
Sección titulada «jurisdiction_not_supported»403. El país del negocio no tiene facturación electrónica en Olympus. La API emite para Ecuador (SRI) y España (VERI*FACTU).
Errores de agentes
Sección titulada «Errores de agentes»Salvo agent_required, solo los recibe la clave de un agente. Ninguno se
arregla reintentando la misma petición, y más de 20 respuestas 403 en 10 minutos
pausan al agente.
agent_required
Sección titulada «agent_required»403. Usaste una clave de integración en un endpoint solo para agentes: KPIs, listados de datos o reportes contables. Crea un agente en Configuración → Equipo → Agentes y usa su clave. Ver Migrar desde claves.
insufficient_permissions
Sección titulada «insufficient_permissions»403. Al agente le falta el módulo, o el nivel, que pide la operación. details.modulo dice cuál (o
cuáles, si vale cualquiera de varios) y details.nivel, cuál hace falta (view u operate):
{ "error": { "code": "insufficient_permissions", "message": "El agente no tiene permiso para esta operación. Se lo da su responsable en Configuración → Equipo y sucursales → Agentes.", "details": { "modulo": "inventario.stock", "nivel": "view" } }}Si lo que falta es un permiso sensible, details.permiso dice cuál. Pasa con datos.personales, que hace falta
para descargar el PDF o el XML de un comprobante y para buscar por correo, teléfono o identificación en
/contacts, /purchase-invoices y /quotes (un buscar con @ o con 6 cifras o más).
También llega, sin details, si un KPI pide con sucursal una sucursal que no es del agente. Su responsable
le da el permiso en el editor, dentro del techo de los agentes.
branch_not_allowed
Sección titulada «branch_not_allowed»403. El agente está limitado a algunas sucursales y el establecimiento del payload no es de ninguna de
ellas. details.establecimiento dice el código que enviaste. Emite desde uno de sus establecimientos, o no
envíes establecimiento y se usará el principal de los suyos. Ver
Sucursales y ventas de otros.
idempotency_key_required
Sección titulada «idempotency_key_required»400. Un agente emitió sin la cabecera Idempotency-Key, que para él es obligatoria al emitir. No se emitió
nada. Envíala con una clave que identifique la operación en tu sistema (venta-SO-2026-0912), no una al azar en
cada intento. Ver Idempotencia.
agent_without_access
Sección titulada «agent_without_access»403. El agente ya no es miembro del negocio, aunque su clave siga viva. Revisa el agente en Configuración → Equipo → Agentes; si se dio de baja, crea uno nuevo.
agent_paused
Sección titulada «agent_paused»403. El agente está pausado: lo pausó una persona o la pausa automática.
details.motivo dice por qué, si se indicó. Lo reanuda su responsable desde la ficha del agente; antes, revisa
su actividad.
agent_suspended
Sección titulada «agent_suspended»403. Su responsable ya no puede responder por él: salió del negocio o perdió algún permiso que el agente tiene. Se reanuda en la ficha del agente tras asignarle otro responsable.
agent_revoked
Sección titulada «agent_revoked»401. El agente fue revocado. Es definitivo: crea uno nuevo. Al revocar un agente se revocan también sus
claves, así que lo normal es recibir api_key_revoked.
agents_paused
Sección titulada «agents_paused»403. El propietario del negocio pausó todos sus agentes. Solo él los reanuda. Las claves de integración siguen funcionando.
agents_disabled
Sección titulada «agents_disabled»503. Olympus desactivó los agentes en toda la plataforma, normalmente por un incidente. Las claves de integración siguen funcionando. Reintenta más tarde, espaciando los intentos (minutos, no segundos).
agent_action_forbidden
Sección titulada «agent_action_forbidden»403. La autonomía del agente tiene prohibida esta acción. details.accion dice cuál
(invoice.issue, document.issue, document.send_email). La cambia quien administra las integraciones. Ver
Autonomía y límites.
agent_action_requires_approval
Sección titulada «agent_action_requires_approval»403. La acción está en modo con aprobación. Hoy eso significa que el agente no la ejecuta: no se crea
nada. details.accion y details.modo dicen cuál. Que la haga una persona desde Olympus, o que el responsable
la pase a automático con topes. Ver «Con aprobación», hoy.
agent_limit_exceeded
Sección titulada «agent_limit_exceeded»429. El agente llegó a uno de sus topes del día. details.limite dice cuál, details.permitido cuánto es
y details.actual lo que lleva hoy; en el importe diario, además, details.este_documento:
{ "error": { "code": "agent_limit_exceeded", "message": "El agente alcanzó su límite «Documentos al día». Lo cambia su responsable en Configuración → Equipo y sucursales → Agentes.", "details": { "limite": "max_daily_documents", "permitido": 500, "actual": 500 } }}En el tope de correos, details.esta_peticion dice cuántos destinatarios llevaba la petición: cuentan todos.
No trae Retry-After: los contadores vuelven a cero al día siguiente (00:00 de Ecuador). Si el importe de un
comprobante pasa su tope, el comprobante no se emite. Ver
Límites del día.
range_too_large
Sección titulada «range_too_large»422. Pediste a un KPI o a un reporte más de 366 días. Cuenta el periodo efectivo: si solo enviaste
desde, se mide hasta hoy. Los listados no tienen este límite. details.maximo_dias dice el máximo. Divide la consulta en periodos de un
año como mucho.
plan_feature_required
Sección titulada «plan_feature_required»403. El plan del negocio no incluye los datos que pediste. El negocio lo cambia en Configuración → Plan. Crear agentes también exige un plan con API y webhooks; eso lo avisa la propia web.
Errores de validación
Sección titulada «Errores de validación»validation_error
Sección titulada «validation_error»400. El payload tiene campos que faltan, sobran o no valen. details reproduce la estructura del
payload con el problema en el sitio exacto:
{ "error": { "code": "validation_error", "message": "La petición tiene campos inválidos o incompletos.", "details": { "items": [ { "impuestos": [ { "codigo_porcentaje": ["«99» no es un código de IVA del SRI. Consulta los válidos en GET /api/v1/catalogs/tax-rates."] } ] } ] } }}Reintentar sin cambiar el payload da exactamente el mismo error.
invalid_json
Sección titulada «invalid_json»400. El cuerpo no es JSON válido. Revisa que envías Content-Type: application/json y que el JSON
está bien formado (comas de más, comillas sin cerrar).
Conflictos
Sección titulada «Conflictos»external_id_conflict
Sección titulada «external_id_conflict»409. Ya existe una factura con esa referencia_externa en el negocio. details.id y
details.secuencial identifican la que ya se emitió.
Casi siempre significa que tu sistema reintentó una emisión que en realidad salió bien. Trátalo como
éxito: la factura existe, consúltala con el id que te devuelve.
sequence_conflict
Sección titulada «sequence_conflict»409. (Ecuador) Enviaste un secuencial que ya está usado en ese punto de emisión. Si controlas la
numeración tú, avanza el contador; si no te hace falta, deja de enviar secuencial y que Olympus lo
asigne.
idempotency_conflict
Sección titulada «idempotency_conflict»409. Reutilizaste una Idempotency-Key con un cuerpo distinto. Cada operación diferente necesita su
propia clave. Ver Idempotencia.
idempotency_in_progress
Sección titulada «idempotency_in_progress»409. Otra petición con la misma Idempotency-Key se está procesando ahora mismo. Espera un segundo
y consulta el resultado; no la repitas en bucle.
document_not_authorized
Sección titulada «document_not_authorized»409. Pediste el PDF, el XML o un reenvío de correo de un comprobante que aún no está listo.
details.estado dice en qué estado está.
En Ecuador hay que esperar a que el SRI lo autorice. En España solo ocurre si la factura no llegó a registrarse.
document_locked
Sección titulada «document_locked»409. La operación no cabe en el estado actual del comprobante: por ejemplo, reintentar una factura
ya autorizada o que sigue en curso. details.estado dice el estado real.
invoice_not_authorized
Sección titulada «invoice_not_authorized»409. (Ecuador) La factura que referencias en factura_modificada (notas de crédito y débito) o en
destinatarios[].factura (guías de remisión) está anulada o el SRI todavía no la autorizó.
details.estado dice en qué estado está; espera a que sea autorizado y vuelve a intentarlo.
Errores de negocio
Sección titulada «Errores de negocio»totals_mismatch
Sección titulada «totals_mismatch»422. Los totales que declaraste no coinciden con lo que Olympus calcula a partir de las líneas.
details trae declarado, calculado y el campo afectado.
Esta comprobación existe para que nunca se emita algo distinto de lo que tu sistema contabilizó.
Si no cuadra, revisa tus redondeos línea a línea. Si prefieres que Olympus calcule sin comparar, omite
el bloque totales.
insufficient_stock
Sección titulada «insufficient_stock»422. Alguna línea enlaza con un producto del negocio que no tiene existencias suficientes.
details.faltantes lista los productos y las cantidades.
Repón stock, permite stock negativo en la configuración del negocio, o factura sin enlazar el producto
(usa un codigo_principal que no coincida con ningún SKU).
establishment_not_found
Sección titulada «establishment_not_found»422. (Ecuador) El establecimiento o el punto de emisión que indicaste no existe o no está activo.
Consulta GET /establishments para ver los códigos reales.
unsupported_tax
Sección titulada «unsupported_tax»422. Una línea lleva un impuesto que esta versión no admite (ICE, IRBPNR, o varios impuestos en la misma línea). Por ahora cada línea admite un solo IVA.
missing_tax_code
Sección titulada «missing_tax_code»422. (España) Una línea no declara su IVA, o lo declara al 0 % sin decir por qué. Exenta, no
sujeta e inversión del sujeto pasivo se declaran distinto ante la AEAT: hay que elegir.
details.codigos lista los válidos. Ver IVA, exenciones e ISP.
field_not_supported_here
Sección titulada «field_not_supported_here»422. Enviaste un campo que no existe en la facturación del país del negocio: establecimiento o
secuencial a un negocio español, retencion o recargo_equivalencia a uno ecuatoriano.
details.campos_de_este_pais dice cuáles sí puedes usar.
document_rejected_before_issuing
Sección titulada «document_rejected_before_issuing»422. (España) La factura no cumple una regla de VERI*FACTU y no llegó a expedirse: comprador
extranjero sin pais, factura simplificada por encima del límite legal, o total cero.
currency_not_supported
Sección titulada «currency_not_supported»422. El negocio no puede facturar en esa moneda. El SRI solo admite dólares; España acepta las
divisas de referencia del BCE. details.permitidas lista algunas válidas.
xml_not_available
Sección titulada «xml_not_available»422. (España) Pediste el XML de una factura española. VERI*FACTU no genera un XML descargable:
lo que acredita la factura es su registro encadenado. Usa el PDF y urls.verificacion.
access_key_not_available
Sección titulada «access_key_not_available»422. (España) Usaste GET /documents/{clave_acceso} en un negocio español. Allí no hay clave de
acceso: busca la factura en GET /invoices por referencia_externa o secuencial.
retry_not_needed
Sección titulada «retry_not_needed»422. (España) Pediste reintentar el envío. No hace falta: el registro ya existe y Olympus lo reenvía a la AEAT por su cuenta.
invoice_not_found
Sección titulada «invoice_not_found»422. (Ecuador) La factura que referencias por id o secuencial no existe en este negocio.
details.campo dice dónde la enviaste. Comprueba el número completo (001-001-000000123) o usa el
id que devolvió POST /invoices/issue.
document_type_not_supported
Sección titulada «document_type_not_supported»422. Pediste emitir o consultar una nota de crédito, nota de débito, retención, guía de remisión o liquidación de compra en un negocio español. Esos comprobantes existen solo en el SRI; ver Otros comprobantes.
Límite de peticiones
Sección titulada «Límite de peticiones»rate_limited
Sección titulada «rate_limited»429. Superaste el límite de peticiones por minuto de tu clave. details.retry_after_seconds y la
cabecera Retry-After dicen cuánto esperar. Las cabeceras X-RateLimit-Limit y X-RateLimit-Remaining
van en todas las respuestas para que lo veas venir.
Ver Límites y paginación.
No encontrado
Sección titulada «No encontrado»not_found
Sección titulada «not_found»404. El recurso no existe o no pertenece a tu negocio. Por seguridad no distinguimos entre ambos casos: una clave nunca puede averiguar si existe una factura de otro negocio.
Error interno
Sección titulada «Error interno»internal_error
Sección titulada «internal_error»500. Algo falló en Olympus. La respuesta siempre trae request_id; envíalo a soporte.
Es seguro reintentar con la misma Idempotency-Key: si la factura llegó a emitirse recibirás la
respuesta guardada, y si no, se emitirá una sola vez.
Cómo tratarlos en tu código
Sección titulada «Cómo tratarlos en tu código»import timeimport httpx
REINTENTABLES = {"rate_limited", "internal_error", "idempotency_in_progress"}
def emitir(client, payload, idempotency_key, intentos=3): for intento in range(intentos): respuesta = client.post( "/invoices/issue", json=payload, headers={"Idempotency-Key": idempotency_key} ) if respuesta.is_success: return respuesta.json()
error = respuesta.json()["error"]
# Ya estaba emitida: no es un fallo, es el mismo resultado por otro camino. if error["code"] == "external_id_conflict": return client.get(f"/invoices/{error['details']['id']}").json()
if error["code"] not in REINTENTABLES: raise ErrorDeFacturacion(error["code"], error["message"], error.get("request_id"))
time.sleep(float(respuesta.headers.get("Retry-After", 2**intento)))
raise ErrorDeFacturacion("sin_respuesta", "Se agotaron los reintentos", None)La regla: reintenta lo transitorio, no lo que depende de que corrijas el payload. Un
validation_error reintentado da igual resultado.