# Ecuafact API - guia completa > Documentacion completa para asistentes. Generada desde el contenido versionado de https://docsapi.ecuafact.com. # Introduccion La API de Ecuafact permite **emitir y consultar comprobantes electronicos** desde cualquier sistema: un ERP, un punto de venta, un SaaS o una tienda en linea. Tu sistema envia el comprobante en JSON y la plataforma se encarga de validarlo, firmarlo, transmitirlo al SRI y generar el RIDE (el PDF del comprobante). ## Base URL La API tiene una URL por ambiente. Empieza con la de pruebas y, cuando tu integracion este lista, usa la de produccion. | Ambiente | Base URL | |---|---| | Pruebas (sandbox) | `https://staging-api.mynexusapi.com` | | Produccion | `https://api.mynexusapi.com` | Todos los ejemplos de esta documentacion usan la URL de pruebas. ## Autenticacion Cada peticion incluye tu **API Key** en la cabecera `X-Api-Key`. Si aun no la tienes, ve a [Autenticacion](/v1/guias/autenticacion) para saber como obtenerla. ## Que puedes hacer | Metodo | Ruta | Para que sirve | |---|---|---| | `GET` | `/v1/contexto` | Ver tu cliente y los contribuyentes habilitados | | `POST` | `/v1/contribuyentes/{identificacion}/comprobantes` | Emitir un comprobante | | `GET` | `/v1/contribuyentes/{identificacion}/comprobantes/emitidos` | Listar comprobantes emitidos | | `GET` | `/v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/ride` | Descargar PDF | | `GET` | `/v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/xml` | Descargar XML | | `POST` | `/v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/correo` | Reenviar el correo | | `GET` | `/v1/catalogos/{nombre}` | Leer un catalogo SRI | | `GET` | `/v1/operaciones/{id}` | Consultar el estado de una emision | | `GET` | `/v1/consumo` | Ver tu cupo disponible | | `GET` | `/v1/contribuyentes/{identificacion}/perfil` | Ver el perfil del emisor | | `PUT` | `/v1/contribuyentes/{identificacion}/perfil` | Actualizar el perfil del emisor | | `POST` | `/v1/contribuyentes/{identificacion}/perfil/logo` | Actualizar el logo del RIDE | | `GET` | `/v1/consultas/*` | Consultar datos del SRI (contribuyentes, establecimientos, claves) | Los cambios de estado de una emision tambien se pueden recibir por [webhook](/v1/guias/webhooks). ## Tipos de comprobante Puedes emitir facturas, liquidaciones de compra, notas de credito y debito, guias de remision, comprobantes de retencion y liquidaciones. Cada tipo tiene su propia pagina con los campos que recibe. ## Siguientes pasos - [Primeros pasos](/v1/guias/primeros-pasos): emite tu primer comprobante paso a paso. - [Ciclo de vida del comprobante](/v1/guias/ciclo-de-vida): entiende que ocurre desde que envias hasta que el SRI autoriza. - [Referencia API](/v1/referencia): la especificacion OpenAPI interactiva. --- # Primeros pasos Camino de cero a tu primer comprobante autorizado en el ambiente de pruebas. ## Antes de empezar - [ ] RUC del emisor habilitado para tu integracion. - [ ] API Key de la integracion. - [ ] IP de salida registrada. - [ ] Un cliente HTTP (cURL, .NET, JavaScript o PHP). ## 1. API Key Tu integracion parte del ambiente de pruebas. Envia tu API Key en la cabecera `X-Api-Key`. Solo las direcciones IP registradas pueden consumir la API. Si no tienes credencial, crea un **sandbox de pruebas** en [Crear sandbox](https://staging-portal.mynexusapi.com/sandbox): cargas tu emisor y tu certificado, y recibes por correo la **API Key** y tu acceso al Portal del Cliente. El sandbox usa el plan **Sandbox MyNexusApi** (5.000 documentos, 2 meses), no tiene validez fiscal y vence a los 2 meses. Tambien puedes solicitarla con el [formulario de solicitud](https://www.ecuafact.com/) indicando tu RUC y las IP de salida; el detalle esta en [Autenticacion](/v1/guias/autenticacion). ## 2. Verifica tu contexto `GET /v1/contexto` devuelve tu cliente y los contribuyentes habilitados, con la identificacion que usas en las rutas. Empieza por aqui para saber para quien puedes emitir. ```bash curl -s "https://staging-api.mynexusapi.com/v1/contexto" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```json { "datos": { "idCliente": "1f2e3d4c-5b6a-4789-9012-abcdef012345", "nombre": "Integracion de ejemplo", "contribuyentes": [ { "identificacion": "0123456789", "identificacionCompleta": "0123456789001", "puedeEmitir": true, "puedeRecibir": true, "tiposComprobante": ["01", "03", "04", "05", "06", "07"] } ] } } ``` Usa el valor `identificacion` que aparece aqui en las rutas. El detalle de los campos esta en [Contexto](/v1/guias/consulta-contexto). ## 3. Emite un comprobante `POST /v1/contribuyentes/0123456789/comprobantes` con cuerpo JSON y la cabecera `Idempotency-Key` obligatoria. ```bash curl -s -X POST \ "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: factura-2026-0001" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "factura-2026-0001"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "factura-2026-0001"}, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'factura-2026-0001', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: factura-2026-0001', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . curl_exec($ch); ``` ```java HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "factura-2026-0001") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)).build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` Cuerpo de la factura: ```json { "origenReferencia": "MiERP", "referenciaExterna": "FACTURA-2026-0001", "infoTributaria": { "ruc": "0123456789001", "codDoc": "01", "estab": "002", "ptoEmi": "001", "secuencial": "000000123" }, "info": { "fechaEmision": "07/09/2026", "tipoIdentificacionComprador": "05", "identificacionComprador": "0123456789", "razonSocialComprador": "CONTRIBUYENTE DE EJEMPLO", "totalSinImpuestos": 100, "totalDescuento": 0, "totalConImpuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "baseImponible": 100, "valor": 15, "tarifa": 15 } ], "importeTotal": 115, "moneda": "DOLAR", "pagos": [ { "formaPago": "01", "total": 115 } ] }, "detalles": [ { "codigoPrincipal": "SERV-001", "descripcion": "Servicio de ejemplo", "cantidad": 1, "precioUnitario": 100, "descuento": 0, "precioTotalSinImpuesto": 100, "impuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "tarifa": 15, "baseImponible": 100, "valor": 15 } ] } ] } ``` La API responde `202 Accepted` con la operacion creada. Describe cada campo: | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision (guardalo) | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Sigue con el paso 4. ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` ## 4. Consulta el estado Mientras la plataforma procesa el comprobante, consulta su estado con el `idOperacion` (o espera el [webhook](/v1/guias/webhooks)). La respuesta trae el estado del envio (`estado`) y el resultado fiscal (`estadoAutorizacion`). ```bash curl -s "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp string json = await http.GetStringAsync( "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f"); Console.WriteLine(json); ``` ```python r = requests.get( "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.status_code, r.text) ``` ```javascript const res = await fetch( 'https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f', { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . curl_exec($ch); ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET().build(); HttpClient cliente = HttpClient.newHttpClient(); System.out.println(cliente.send(peticion, HttpResponse.BodyHandlers.ofString()).body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET Operation operacion = await client.GetOperacionAsync(idOperacion); Console.WriteLine(operacion.Estado); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python operacion = client.get_operacion(id_operacion) print(operacion.estado) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const operacion = await client.getOperacion(idOperacion); console.log(operacion.estado); ``` ```php-sdk getOperacion($idOperacion); echo $operacion->estado; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java Operation operacion = client.getOperacion(idOperacion); System.out.println(operacion.estado()); ``` El resultado fiscal final tambien llega por [webhook](/v1/guias/webhooks). | `estado` | Significado | |---|---| | `en_cola` | Registrado, aun no enviado | | `enviando` | En despacho | | `enviado` | Enviado para procesamiento fiscal | | `resultado_desconocido` | El resultado aun no se confirma | | `cancelado` | Operacion cancelada | | `estadoAutorizacion` | Significado | |---|---| | `no_disponible` | Aun sin resultado fiscal | | `pendiente` | En procesamiento por el SRI | | `autorizado` | Autorizacion confirmada | | `error` | Rechazo fiscal | El detalle completo esta en [Seguimiento de una operacion](/v1/guias/consulta-operacion). ## Siguientes pasos - [Datos de prueba](/v1/guias/datos-de-prueba): ambiente, datos y casos. - [Emision](/v1/guias/emision): bloques por tipo de comprobante. - [Webhooks](/v1/guias/webhooks): recibe el resultado fiscal. - [Errores](/v1/guias/errores): codigos y significados. --- # Datos y credenciales de prueba Todo lo necesario para tu primera emision en el ambiente de pruebas, de punta a punta. > Ambiente de pruebas. Los comprobantes **no tienen validez fiscal** y no se > transmiten al SRI en produccion. ## Ambiente | Ambiente | Base URL | |---|---| | Pruebas (sandbox) | `https://staging-api.mynexusapi.com` | | Produccion | `https://api.mynexusapi.com` | ## Datos de ejemplo Usa estos valores para probar; el contribuyente de ejemplo existe en pruebas. | Dato | Valor | Donde | |---|---|---| | Identificacion del emisor | `0123456789` | ruta `contribuyentes/{identificacion}` | | RUC completo | `0123456789001` | `infoTributaria.ruc` | | Tipo de identificacion (RUC) | `04` | `tipoIdentificacionComprador` | | Tipo de identificacion (cedula) | `05` | `tipoIdentificacionComprador` | En pruebas puedes enviar `secuencial` de 9 ceros (`000000000`) y una clave de acceso de 49 ceros para ensayar rapido. ## Como obtener una credencial - **Probar sin credencial:** abre la [Consola de prueba](/v1/consola). La API Key de demostracion se aplica del lado del servidor. - **Credencial propia:** crea una API Key en el portal del cliente (seccion **Acceso API**) o solicita el alta indicando tu RUC y las IP de salida ([Autenticacion](/v1/guias/autenticacion)). ## Empezar El paso a paso (contexto, emision, seguimiento y descarga del RIDE/XML) esta en [Primeros pasos](/v1/guias/primeros-pasos). Aqui tienes los datos y los casos. ## Casos de prueba | Escenario | Como | Respuesta esperada | |---|---|---| | Emision exitosa | `POST` con `Idempotency-Key` nueva | `202` (`codigo` `200`), con `idOperacion` | | Reintento idempotente | Repite la misma `Idempotency-Key` y cuerpo | `202` (`codigo` `201`), misma operacion | | Conflicto de idempotencia | Misma `Idempotency-Key`, otro cuerpo | `409` (`codigo` `403`) | | Duplicado | Numero o clave ya registrados | `409` (`codigo` `401`) | | Validacion de campos | Falta `Idempotency-Key` o campos mal formados | `400` (`codigo` `301`), con `errores` | | Regla fiscal | Totales o impuestos inconsistentes | `422` (`codigo` `302`) | | Cupo agotado | Plan sin documentos | `429` (`codigo` `501`) | | Limite de solicitudes | Supera 200/min | `429` (`codigo` `104`) + `Retry-After` | | Rechazo del SRI | Comprobante rechazado | `estadoAutorizacion=error` y webhook `document.rejected` | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Primeros pasos](/v1/guias/primeros-pasos) - [Comprobantes electronicos](/v1/guias/emision) - [Errores](/v1/guias/errores) --- # Ciclo de vida de un comprobante Emitir un comprobante no es una sola llamada: es un proceso. Entenderlo te evita confusiones como creer que un comprobante ya esta autorizado cuando aun no lo esta. ## 1. Envias el comprobante Envia la solicitud con `POST /v1/contribuyentes/{identificacion}/comprobantes`. Si el formato, la estructura y las validaciones son correctas, la API responde con un **`idOperacion`**: el identificador de seguimiento de tu emision. A partir de este momento tu parte termino: la plataforma toma el comprobante y lo procesa. ## 2. La plataforma lo procesa La plataforma valida las reglas fiscales, firma el comprobante, lo transmite al SRI y guarda el resultado. Mientras tanto, la operacion pasa por distintos estados de envio (`en_cola`, `enviando`, `enviado`). ## 3. Sigues la operacion Tienes dos formas de enterarte del resultado: - **Consulta**: `GET /v1/operaciones/{idOperacion}`. Te devuelve el estado actual. - **Webhook**: si lo tienes configurado, la plataforma te notifica automaticamente cuando la operacion cambia de estado. Usa el webhook como mecanismo principal y la consulta como respaldo. Ve [Seguimiento de una operacion](/v1/guias/consulta-operacion) y [Webhooks](/v1/guias/webhooks). ## 4. El SRI autoriza El campo `estadoAutorizacion` indica el resultado fiscal: | `estadoAutorizacion` | Significado | |---|---| | `no_disponible` | Aun no hay resultado fiscal | | `pendiente` | El SRI esta procesando el comprobante | | `autorizado` | Autorizacion confirmada | | `error` | El SRI rechazo el comprobante | Para uso fiscal solo cuenta `autorizado`. ## Idempotencia Si tu proceso reintenta una emision (por ejemplo, por un timeout), envia la misma cabecera `Idempotency-Key` que en el primer intento. Asi la plataforma no crea un comprobante duplicado y te devuelve la misma operacion. Guarda la `Idempotency-Key` junto con la referencia del comprobante para poder reintentar con seguridad. Ve [Buenas practicas](/v1/guias/buenas-practicas). ## Siguientes pasos - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) --- # Autenticacion Todas las peticiones a la API se autentican con tu **API Key**. La API Key identifica a tu integracion y representa al cliente y a sus contribuyentes habilitados. ## Envio de la API Key La API Key viaja en la cabecera `X-Api-Key` de cada peticion: ``` GET /v1/contexto X-Api-Key: ``` ```bash curl -s "https://staging-api.mynexusapi.com/v1/contexto" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync("https://staging-api.mynexusapi.com/v1/contexto"); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/contexto", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) ``` ```javascript const res = await fetch('https://staging-api.mynexusapi.com/v1/contexto', { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . curl_exec($ch); ``` ```java HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contexto")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET().build(); HttpClient cliente = HttpClient.newHttpClient(); System.out.println(cliente.send(peticion, HttpResponse.BodyHandlers.ofString()).body()); ``` Guarda la API Key como un secreto: no la compartas ni la expongas en el navegador. Si se filtra, genera una nueva desde el portal. ## Como obtener tu API Key - **Si ya tienes acceso al portal del cliente**, entra a la seccion **Acceso API** y crea una API Key. Ahi mismo puedes verla, rotarla y revocarla. - **Si aun no tienes acceso**, envia el [formulario de solicitud](https://www.ecuafact.com/) indicando el RUC de la integracion y las direcciones IP de salida que consumiran la API. Con eso se crea la integracion, se habilitan sus contribuyentes y se te entrega la API Key. Por defecto empiezas en el ambiente de pruebas. La habilitacion de produccion se coordina por el mismo canal. ## Restriccion por IP Ademas de la API Key, la API valida que la peticion provenga de una direccion registrada. Registra en el portal las IP de salida de tu servidor. Si tus IP cambian, actualizalas en el portal antes de que se corten las peticiones. ## Errores de autenticacion | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `101` | 401 | La cabecera `X-Api-Key` falta, esta mal escrita o la API Key no existe | Revisa la cabecera y la API Key | | `101` | 401 | La API Key existe pero no esta autorizada para la IP de origen | Registra la IP de salida en el portal | | `102` | 403 | Origen de la solicitud no autorizado | Revisa la IP registrada para tu API Key | | `103` | 403 | El contribuyente, el tipo de comprobante o el plan no estan habilitados | Habilita el contribuyente o el servicio | | `104` | 429 | Demasiados intentos de autenticacion fallidos desde la misma IP | Espera el `Retry-After` (ve [Limite de request](/v1/guias/limite-de-request)) | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Ambientes y contribuyentes](/v1/guias/ambientes): elige la URL y el contribuyente. - [Primeros pasos](/v1/guias/primeros-pasos): emite tu primer comprobante. --- # Limite de request La API limita el numero de solicitudes para mantener el servicio estable. Si superas el limite, la API responde `429 Too Many Requests` e indica cuanto esperar antes de reintentar. ## Que se limita | Limite | Valor | Se cuenta por | |---|---|---| | Solicitudes por API Key | 200 por minuto (valor por defecto) | API Key | | Intentos de autenticacion fallidos | 20 por minuto | Direccion IP | - **Solicitudes por API Key.** Se cuentan en una ventana de un minuto por API Key. El valor por defecto es **200 solicitudes por minuto** y puede variar segun el plan o la configuracion de tu integracion. - **Intentos de autenticacion fallidos.** Se cuentan por direccion IP; sirven para frenar intentos de fuerza bruta. Una autenticacion correcta reinicia el contador. ## Que pasa al superar el limite La API rechaza la solicitud que supera el limite con: | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `104` | 429 | Superaste el limite de solicitudes o de intentos de autenticacion | Espera el tiempo de `Retry-After` y reintenta | ```json { "codigo": "104", "mensaje": "Limite de solicitudes excedido." } ``` El cupo de documentos es distinto del limite de solicitudes: si te quedas sin documentos, la emision responde `429` (`codigo` `501`). Ve [Consumo y cuotas](/v1/guias/consumo). ## Cabeceras de la respuesta Las respuestas a `/v1` incluyen cabeceras con el estado de tu limite: | Cabecera | Cuando aparece | Significado | |---|---|---| | `X-RateLimit-Limit` | En todas las respuestas | Maximo de solicitudes permitidas en la ventana | | `X-RateLimit-Remaining` | En todas las respuestas | Solicitudes restantes en la ventana actual | | `X-RateLimit-Reset` | Solo al superar el limite (`429`) | Momento (timestamp Unix, en segundos) en que se reinicia la ventana | | `Retry-After` | En `429` y `503` | Segundos que debes esperar antes de reintentar | Ejemplo de una respuesta normal: ``` HTTP/1.1 200 OK X-RateLimit-Limit: 200 X-RateLimit-Remaining: 42 ``` Ejemplo de respuesta `429`: ``` HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 200 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1757341500 Retry-After: 43 Content-Type: application/json { "codigo": "104", "mensaje": "Limite de solicitudes excedido." } ``` ## Recomendaciones - Reintenta `429`, `502`, `503` y `504` con espera exponencial y un poco de aleatoriedad (jitter). - Respeta `Retry-After`: no reintentes antes de que termine. - Para el estado de una emision, usa el [webhook](/v1/guias/webhooks) como fuente principal; evita el sondeo constante. - Si necesitas mas capacidad, solicita un limite mayor para tu integracion. ## Siguientes pasos - [Limites](/v1/guias/limites): tamano de solicitud, paginacion y cupo. - [Buenas practicas](/v1/guias/buenas-practicas) - [Errores](/v1/guias/errores) --- # Ambientes y contribuyentes La API funciona en dos ambientes independientes. Elige la URL segun el momento de tu integracion. ## Ambientes | Ambiente | Base URL | Uso | |---|---|---| | Pruebas (sandbox) | `https://staging-api.mynexusapi.com` | Desarrollar y probar. No tiene validez fiscal. | | Produccion | `https://api.mynexusapi.com` | Emitir comprobantes reales ante el SRI. | Empieza siempre en pruebas. Cuando tu integracion funcione, solicita el acceso a produccion. En pruebas existe una excepcion para ensayar rapido: puedes enviar `secuencial` de 9 ceros y una clave de acceso de 49 ceros. No aplica a produccion. ## Contribuyentes Una integracion puede emitir y consultar para **varios contribuyentes** habilitados. Para saber cuales tienes disponibles, y si pueden emitir, recibir o ambos, consulta el contexto de tu API Key: [Mas informacion sobre el contexto](/v1/guias/consulta-contexto) El contexto devuelve, por cada contribuyente: - `identificacion`: el valor que usas en la ruta de emision y consulta. - `identificacionCompleta`: el RUC completo de 13 digitos. - `puedeEmitir`, `puedeRecibir`: si puede emitir o recibir. - `tiposComprobante`: los tipos de comprobante habilitados. ### Identificacion en la ruta En las rutas que terminan en `contribuyentes/{identificacion}` usa el valor `identificacion` que devuelve el contexto. El RUC completo de 13 digitos se informa aparte, dentro de la solicitud, en `infoTributaria.ruc`. Si un contribuyente no aparece en el contexto, no esta habilitado para tu integracion: solicita su alta. ## Clave de acceso Cada comprobante tiene una **clave de acceso**: un identificador unico de 49 digitos que asigna la plataforma. No la envias en la solicitud; la recibes en el seguimiento de la operacion o en el webhook. ## Errores de este apartado | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `103` | 403 | El contribuyente no esta habilitado para tu integracion | Solicita su alta | | `102` | 403 | Origen de la solicitud no autorizado | Revisa la IP registrada para tu API Key | ## Siguientes pasos - [Primeros pasos](/v1/guias/primeros-pasos) - [Datos de prueba](/v1/guias/datos-de-prueba) - [Comprobantes electronicos](/v1/guias/emision) --- # Buenas practicas Recomendaciones para integrar de forma estable: reintentos seguros, control de duplicados y buen manejo del estado fiscal. ## Idempotencia Envia siempre `Idempotency-Key` en la emision: una cadena ASCII estable por intencion (por ejemplo `FACTURA-2026-0001`). ```bash curl -s -X POST \ "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: FACTURA-2026-0001" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "FACTURA-2026-0001"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "FACTURA-2026-0001", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'FACTURA-2026-0001', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: FACTURA-2026-0001', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "FACTURA-2026-0001") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); // Si el proceso reintenta, reenvia la MISMA clave. string clave = resultado.IdempotencyKey; ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) # Si el proceso reintenta, reenvia la MISMA clave. clave = resultado.idempotency_key ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); // Si el proceso reintenta, reenvia la MISMA clave. const clave = resultado.idempotencyKey; ``` ```php-sdk emitir($comprobante); // Si el proceso reintenta, reenvia la MISMA clave. $clave = $resultado->idempotencyKey; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); // Si el proceso reintenta, reenvia la MISMA clave. String clave = resultado.idempotencyKey(); ``` - Misma clave y mismo contenido: `202` con `codigo` `201` y la misma operacion; no crea un comprobante nuevo. - Misma clave con otro contenido: `409` (`codigo` `403`). - Guarda la `Idempotency-Key` y la `referenciaExterna` que enviaste. Si tu proceso reintenta por un timeout, reenvia las mismas para no crear un comprobante duplicado. ## Errores y reintentos | Situacion | Accion | |---|---| | `429` (limite o cupo), `502`, `503`, `504` | Reintentar con espera exponencial (con jitter) y respetar `Retry-After` | | `400`, `401`, `403`, `404`, `409`, `422` | **No** reintentar sin corregir la causa | | Timeout de red | Reintentar con la misma `Idempotency-Key` | Los limites de solicitudes y el manejo del `429` se explican en [Limite de request](/v1/guias/limite-de-request). - Registra el `X-Correlation-Id` de cada respuesta: identifica la peticion ante soporte. - No reintentes la emision con una referencia nueva: es lo que duplica documentos. ## Emision en lote y concurrencia - Un comprobante por intencion: no reutilices el mismo numero (`estab`/`ptoEmi`/`secuencial`) para otra emision. - Conserva fechas e importes tal como los originaste; no los reescribas en reintentos. - No envies campos fuera de la Estructura del comprobante: los campos desconocidos se rechazan. - Puedes emitir en paralelo con la misma API Key; manten un numero de solicitudes moderado y respeta `429`. ## Seguimiento del estado - La emision responde `202`: la solicitud quedo **recibida**, no autorizada. No esperes el resultado fiscal en la misma respuesta. - Consulta `GET /v1/operaciones/{id}` o espera el [webhook](/v1/guias/webhooks). - Evita el sondeo agresivo: usa el webhook como fuente principal y la consulta como respaldo. ## Webhooks - Verifica siempre la firma antes de interpretar el cuerpo ([Verificacion de firma](/v1/guias/webhooks-firma)). - Responde `2xx` rapido y procesa en segundo plano. - Trata los eventos como idempotentes: un mismo `resourceId` puede reentregarse. ## Cupo - El cupo se descuenta al resolverse la operacion, sea autorizada o rechazada. - Revisa el saldo en [`/v1/consumo`](/v1/guias/consumo) antes de lotes grandes. - Sin cupo, la emision responde `429` (`codigo` `501`); no insistas en bucle. ## Formatos - Importes: numeros JSON decimales con punto, sin comillas. - Fechas fiscales: `dd/MM/yyyy`. Periodo: `MM/yyyy`. - Codigos e identificaciones: cadenas. ## Seguridad - Envia `X-Api-Key` **solo** desde tu backend; nunca la expongas en el navegador ni en aplicaciones cliente. - Manten autorizadas solo las IP de origen que usan la API. - No compartas tu API Key entre integraciones distintas. ## Checklist antes de produccion - [ ] `Idempotency-Key` estable y reutilizada en los reintentos. - [ ] Reintentos con espera exponencial para `429/502/503/504`. - [ ] Numero de comprobante unico por emision. - [ ] Webhook con verificacion de firma e idempotente. - [ ] Guarda el `X-Correlation-Id` para reportar a soporte. - [ ] Monitoreo del saldo en `/v1/consumo`. ## Siguientes pasos - [Errores](/v1/guias/errores) - [Webhooks](/v1/guias/webhooks) - [Limites](/v1/guias/limites) --- # Comprobantes electronicos Todos los tipos de comprobante se emiten con el **mismo endpoint y la misma estructura**. Lo unico que cambia es el `codDoc`, los campos de `info` y las colecciones que uses. Al enviar la solicitud recibes un `idOperacion` para hacer seguimiento; el resultado del SRI llega despues y se consulta o se recibe por webhook. ## Endpoint ``` POST /v1/contribuyentes/{identificacion}/comprobantes Content-Type: application/json X-Api-Key: Idempotency-Key: ``` El cuerpo no puede superar 2 MiB ni una profundidad JSON de 32 niveles. ## Ejemplo de emision ```bash curl -s -X POST \ "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: FACTURA-2026-0001" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "FACTURA-2026-0001"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "FACTURA-2026-0001", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'FACTURA-2026-0001', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: FACTURA-2026-0001', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "FACTURA-2026-0001") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` Cada tipo tiene su propia pagina con el detalle de `info` y la coleccion que usa. ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta [Seguimiento de una operacion](/v1/guias/consulta-operacion) o espera el [webhook](/v1/guias/webhooks). ## Tipos de comprobante Todos los tipos comparten la misma estructura: solo cambian `info`, la coleccion y el `codDoc`. **Factura (`01`).** Venta de bienes o servicios a un comprador. Cabecera con los datos del comprador, totales y `pagos`, y las lineas en `detalles`. Ve [Factura](/v1/guias/emision-01). **Liquidacion de compra (`03`).** Adquisicion de bienes o servicios a un proveedor. Como la factura, pero centrada en el proveedor. Ve [Liquidacion de compra](/v1/guias/emision-03). **Nota de credito (`04`).** Ajusta o anula un comprobante ya emitido (devoluciones, descuentos, anulaciones) y referencia el documento modificado. Ve [Nota de credito](/v1/guias/emision-04). **Nota de debito (`05`).** Incrementa el valor de un comprobante ya emitido (intereses, recargos); usa `motivos`. Ve [Nota de debito](/v1/guias/emision-05). **Guia de remision (`06`).** Acompana el traslado de bienes; sin importes, con `destinatarios`. Ve [Guia de remision](/v1/guias/emision-06). **Comprobante de retencion (`07`).** Acredita impuestos retenidos a un proveedor en `docsSustento`. Ve [Comprobante de retencion](/v1/guias/emision-07). Los codigos de tipo y las tablas de referencia estan en [Catalogos](/v1/guias/catalogos). ## Estructura del comprobante | Clave | Contenido | |---|---| | `origenReferencia`, `referenciaExterna` | Identificadores de tu sistema. Los usas para rastrear y reintentar sin duplicar | | `infoTributaria` | `ruc`, `codDoc`, `estab`, `ptoEmi`, `secuencial` | | `info` | Cabecera del tipo; solo recibe los campos de ese `codDoc` | | `detalles`, `motivos`, `destinatarios`, `docsSustento` | Coleccion segun el tipo | | `infoAdicional` | Hasta 20 objetos `{nombre, valor}` | Reglas transversales: importes como numeros JSON decimales (punto decimal, sin comillas); codigos e identificaciones como cadena; fechas `dd/MM/yyyy`; periodo `MM/yyyy`. ## Clave de acceso Cada comprobante recibe una **clave de acceso** de 49 digitos que asigna la plataforma; no la envias en la solicitud. La ves en el seguimiento de la operacion o en el webhook. En pruebas puedes usar la excepcion de `secuencial` de 9 ceros y una clave de 49 ceros. ## Validacion Antes de procesar el comprobante, la API valida dos cosas: que la solicitud tenga los campos y formatos correctos, y que se cumplan las reglas fiscales (totales, impuestos, referencias y retenciones). Si algo no concuerda, responde con un error de validacion y no descuenta cupo. ## Reintentos y conflictos - Reintento con la misma `Idempotency-Key` y el mismo contenido: `202` con `codigo` `201`. Es la misma operacion. - Misma `Idempotency-Key` con otro contenido, o la misma referencia con otro contenido: `409` (`codigo` `403`). - Numero o clave ya registrados: `409` (`codigo` `401`). Si el comprobante anterior esta rechazado o con error, el reenvio reutiliza la operacion sin descontar cupo. - Enviar campos de otro tipo en `info`: `422` (`codigo` `302`). ## Siguientes pasos - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Factura La factura usa la misma estructura que los demas comprobantes; solo cambian los campos de `info` y las colecciones. > Ejemplo en el **ambiente de pruebas**. Los comprobantes no tienen validez fiscal. ``` POST /v1/contribuyentes/{identificacion}/comprobantes infoTributaria.codDoc = "01" ``` ## Estructura del comprobante | Clave | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `origenReferencia` | string | Si | Sistema de origen (max. 64) | | `referenciaExterna` | string | Si | Referencia estable de tu sistema (max. 128) | | `infoTributaria` | object | Si | Identidad del documento (ver tabla) | | `info` | object | Si | Cabecera de la factura (ver tabla) | | `detalles` | array | Si | Lineas de la factura | | `infoAdicional` | array | No | Hasta 20 objetos `{nombre, valor}` | ### `infoTributaria` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `ruc` | string | Si | 13 digitos del emisor | | `codDoc` | string | Si | `01` | | `estab` | string | Si | 3 digitos | | `ptoEmi` | string | Si | 3 digitos | | `secuencial` | string | Si | 9 digitos | La clave de acceso la asigna la plataforma; no la envies. ## Campos de `info` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `fechaEmision` | string | Si | `dd/MM/yyyy` | | `tipoIdentificacionComprador` | string | Si | [Catalogos: identificacion](/v1/guias/catalogos#tipos-de-identificacion) | | `identificacionComprador` | string | Si | Documento del comprador | | `razonSocialComprador` | string | Si | Razon social o nombres | | `dirEstablecimiento` | string | No | Direccion del establecimiento | | `direccionComprador` | string | No | Direccion del comprador | | `totalSinImpuestos` | number | Si | Suma de bases | | `totalDescuento` | number | Si | Suma de descuentos de linea | | `totalConImpuestos` | array | Si | `codigo`, `codigoPorcentaje`, `baseImponible`, `valor` (`tarifa` opcional) | | `propina` | number | No | Importe de propina | | `importeTotal` | number | Si | Total a pagar | | `moneda` | string | Si | Codigo de moneda (por ejemplo `DOLAR`) | | `pagos` | array | Si | `formaPago` y `total` obligatorios | ## `detalles` Lineas de la factura. | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigoPrincipal` | string | Si | Codigo del bien o servicio | | `codigoAuxiliar` | string | No | Codigo auxiliar del emisor | | `descripcion` | string | Si | Descripcion de la linea | | `unidadMedida` | string | No | Unidad de medida | | `cantidad` | number | Si | Cantidad (hasta 6 decimales) | | `precioUnitario` | number | Si | Precio unitario (hasta 6 decimales) | | `descuento` | number | Si | Descuento de la linea (importe, no porcentaje) | | `precioTotalSinImpuesto` | number | Si | `cantidad x precioUnitario - descuento` | | `impuestos` | array | Si | Impuestos de la linea (ver abajo) | | `detallesAdicionales` | array | No | Hasta 3 objetos `{nombre, valor}` | ### `detalles[].impuestos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigo` | string | Si | Impuesto: `2` IVA, `3` ICE, `5` IRBPNR | | `codigoPorcentaje` | string | Si | Codigo de tarifa (ve [Catalogos](/v1/guias/catalogos)) | | `tarifa` | number | Si | Porcentaje de la tarifa | | `baseImponible` | number | Si | Base sobre la que se calcula | | `valor` | number | Si | Valor del impuesto | ### `pagos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `formaPago` | string | Si | Codigo de forma de pago | | `total` | number | Si | Monto con esta forma de pago | | `plazo` | number | No | Plazo | | `unidadTiempo` | string | Condicional | Obligatorio si `plazo > 0` | ### `infoAdicional` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `nombre` | string | Si | Nombre del campo adicional | | `valor` | string | Si | Valor del campo adicional | Un IVA explicito y, opcionalmente, un ICE porcentual. La `tarifa` de `totalConImpuestos`, si llega, debe coincidir con la del detalle. ## Ejemplo de solicitud ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: FACTURA-2026-0001" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "FACTURA-2026-0001"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "FACTURA-2026-0001", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'FACTURA-2026-0001', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: FACTURA-2026-0001', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "FACTURA-2026-0001") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` ## Cuerpo de la Factura ```json { "origenReferencia": "MiERP", "referenciaExterna": "FACTURA-2026-0001", "infoTributaria": { "ruc": "0123456789001", "codDoc": "01", "estab": "002", "ptoEmi": "001", "secuencial": "000000123" }, "info": { "fechaEmision": "07/09/2026", "tipoIdentificacionComprador": "05", "identificacionComprador": "0123456789", "razonSocialComprador": "CONTRIBUYENTE DE EJEMPLO", "dirEstablecimiento": "AV. PRINCIPAL 123", "direccionComprador": "CALLE 1 NORTE", "totalSinImpuestos": 100, "totalDescuento": 0, "totalConImpuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "baseImponible": 100, "valor": 15, "tarifa": 15 } ], "propina": 0, "importeTotal": 115, "moneda": "DOLAR", "pagos": [ { "formaPago": "01", "total": 115 } ] }, "detalles": [ { "codigoPrincipal": "SERV-001", "descripcion": "Servicio de ejemplo", "cantidad": 1, "precioUnitario": 100, "descuento": 0, "precioTotalSinImpuesto": 100, "impuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "tarifa": 15, "baseImponible": 100, "valor": 15 } ] } ], "infoAdicional": [ { "nombre": "Orden", "valor": "OC-1001" } ] } ``` ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta `urlEstado` o espera el [webhook](/v1/guias/webhooks). ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 202 | Emision aceptada | Sigue el estado de la operacion | | `201` | 202 | Reintento con la misma `Idempotency-Key` y el mismo contenido | Sigue la misma operacion | | `301` | 400 | Falta `Idempotency-Key` o hay campos mal formados | Corrige la solicitud | | `302` | 422 | El comprobante no cumple una regla fiscal o de estructura | Revisa `errores` | | `401` | 409 | Numero o clave ya registrados (duplicado) | Consulta la operacion existente | | `403` | 409 | Misma `Idempotency-Key` con otro contenido | Usa la misma intencion | | `501` | 429 | Cupo agotado | Amplia tu cupo | | `101` | 401 | API Key invalida | Revisa la API Key | | `103` | 403 | Contribuyente o tipo no habilitado | Habilita el contribuyente | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descarga el ejemplo de solicitud](/downloads/v1/ejemplos/example-01.json) - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Liquidacion de compra La liquidacion de compra se emite cuando el emisor adquiere bienes o servicios a un proveedor. La estructura del comprobante es la misma que en [Factura](/v1/guias/emision-01); cambia el contenido de `info` y el cuerpo de ejemplo (`comprobante.json`). > Ejemplo en el **ambiente de pruebas**. Los comprobantes no tienen validez fiscal. ``` POST /v1/contribuyentes/{identificacion}/comprobantes infoTributaria.codDoc = "03" ``` ## Estructura del comprobante | Clave | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `origenReferencia` | string | Si | Sistema de origen (max. 64) | | `referenciaExterna` | string | Si | Referencia estable de tu sistema (max. 128) | | `infoTributaria` | object | Si | Identidad del documento | | `info` | object | Si | Cabecera de la liquidacion | | `detalles` | array | Si | Lineas | | `infoAdicional` | array | No | Hasta 20 objetos `{nombre, valor}` | `infoTributaria`: `ruc` (13), `codDoc` (`03`), `estab` (3), `ptoEmi` (3) y `secuencial` (9) obligatorios. La clave de acceso la asigna la plataforma; no la envies. ## Campos de `info` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `fechaEmision` | string | Si | `dd/MM/yyyy` | | `tipoIdentificacionProveedor` | string | Si | [Catalogos: identificacion](/v1/guias/catalogos#tipos-de-identificacion) | | `identificacionProveedor` | string | Si | Documento del proveedor | | `razonSocialProveedor` | string | Si | Razon social o nombres | | `dirEstablecimiento` | string | No | Direccion del establecimiento | | `direccionProveedor` | string | No | Direccion del proveedor | | `totalSinImpuestos` | number | Si | Suma de bases | | `totalDescuento` | number | Si | Suma de descuentos de linea | | `totalConImpuestos` | array | Si | `codigo`, `codigoPorcentaje`, `baseImponible`, `valor` | | `importeTotal` | number | Si | Total a pagar | | `moneda` | string | Si | Codigo de moneda | | `pagos` | array | Si | `formaPago` y `total` obligatorios | ## `detalles` Lineas de la liquidacion. | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigoPrincipal` | string | Si | Codigo del bien o servicio | | `codigoAuxiliar` | string | No | Codigo auxiliar del emisor | | `descripcion` | string | Si | Descripcion de la linea | | `unidadMedida` | string | No | Unidad de medida | | `cantidad` | number | Si | Cantidad (hasta 6 decimales) | | `precioUnitario` | number | Si | Precio unitario (hasta 6 decimales) | | `descuento` | number | Si | Descuento de la linea (importe, no porcentaje) | | `precioTotalSinImpuesto` | number | Si | `cantidad x precioUnitario - descuento` | | `impuestos` | array | Si | Impuestos de la linea (ver abajo) | | `detallesAdicionales` | array | No | Hasta 3 objetos `{nombre, valor}` | ### `detalles[].impuestos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigo` | string | Si | Impuesto: `2` IVA, `3` ICE, `5` IRBPNR | | `codigoPorcentaje` | string | Si | Codigo de tarifa (ve [Catalogos](/v1/guias/catalogos)) | | `tarifa` | number | Si | Porcentaje de la tarifa | | `baseImponible` | number | Si | Base sobre la que se calcula | | `valor` | number | Si | Valor del impuesto | ### `pagos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `formaPago` | string | Si | Codigo de forma de pago | | `total` | number | Si | Monto con esta forma de pago | | `plazo` | number | No | Plazo | | `unidadTiempo` | string | Condicional | Obligatorio si `plazo > 0` | ### `infoAdicional` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `nombre` | string | Si | Nombre del campo adicional | | `valor` | string | Si | Valor del campo adicional | ## Ejemplo de solicitud ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: LIQ-2026-0007" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "LIQ-2026-0007"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "LIQ-2026-0007", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'LIQ-2026-0007', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: LIQ-2026-0007', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "LIQ-2026-0007") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` ## Cuerpo de la Liquidacion de compra ```json { "origenReferencia": "MiERP", "referenciaExterna": "LIQ-2026-0007", "infoTributaria": { "ruc": "0123456789001", "codDoc": "03", "estab": "002", "ptoEmi": "001", "secuencial": "000000045" }, "info": { "fechaEmision": "07/09/2026", "tipoIdentificacionProveedor": "05", "identificacionProveedor": "0987654321", "razonSocialProveedor": "PROVEEDOR DE EJEMPLO", "totalSinImpuestos": 200, "totalDescuento": 0, "totalConImpuestos": [ { "codigo": "2", "codigoPorcentaje": "0", "baseImponible": 200, "valor": 0 } ], "importeTotal": 200, "moneda": "DOLAR", "pagos": [ { "formaPago": "01", "total": 200 } ] }, "detalles": [ { "codigoPrincipal": "MP-001", "descripcion": "Materia prima", "cantidad": 2, "precioUnitario": 100, "descuento": 0, "precioTotalSinImpuesto": 200, "impuestos": [ { "codigo": "2", "codigoPorcentaje": "0", "tarifa": 0, "baseImponible": 200, "valor": 0 } ] } ] } ``` ## Notas - El emisor es quien liquida; el `proveedor` es la contraparte. - No recibe consumidor final como proveedor. - No referencia un documento modificado. ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta `urlEstado` o espera el [webhook](/v1/guias/webhooks). ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 202 | Emision aceptada | Sigue el estado de la operacion | | `201` | 202 | Reintento con la misma `Idempotency-Key` y el mismo contenido | Sigue la misma operacion | | `301` | 400 | Falta `Idempotency-Key` o hay campos mal formados | Corrige la solicitud | | `302` | 422 | El comprobante no cumple una regla fiscal o de estructura | Revisa `errores` | | `401` | 409 | Numero o clave ya registrados (duplicado) | Consulta la operacion existente | | `403` | 409 | Misma `Idempotency-Key` con otro contenido | Usa la misma intencion | | `501` | 429 | Cupo agotado | Amplia tu cupo | | `101` | 401 | API Key invalida | Revisa la API Key | | `103` | 403 | Contribuyente o tipo no habilitado | Habilita el contribuyente | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descarga el ejemplo de solicitud](/downloads/v1/ejemplos/example-03.json) - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Nota de credito La nota de credito ajusta o anula un comprobante ya emitido (devoluciones, descuentos, anulaciones). La estructura del comprobante es la misma que en [Factura](/v1/guias/emision-01); cambia el contenido de `info` y el cuerpo de ejemplo (`comprobante.json`). > Ejemplo en el **ambiente de pruebas**. Los comprobantes no tienen validez fiscal. ``` POST /v1/contribuyentes/{identificacion}/comprobantes infoTributaria.codDoc = "04" ``` ## Estructura del comprobante | Clave | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `origenReferencia` | string | Si | Sistema de origen (max. 64) | | `referenciaExterna` | string | Si | Referencia estable de tu sistema (max. 128) | | `infoTributaria` | object | Si | Identidad del documento | | `info` | object | Si | Cabecera de la nota | | `detalles` | array | Si | Lineas | | `infoAdicional` | array | No | Hasta 20 objetos `{nombre, valor}` | `infoTributaria`: `ruc` (13), `codDoc` (`04`), `estab` (3), `ptoEmi` (3) y `secuencial` (9) obligatorios. La clave de acceso la asigna la plataforma; no la envies. ## Campos de `info` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `fechaEmision` | string | Si | `dd/MM/yyyy` | | `tipoIdentificacionComprador` | string | Si | Catalogos de identificacion | | `identificacionComprador` | string | Si | Documento del comprador | | `razonSocialComprador` | string | Si | Razon social | | `codDocModificado` | string | Si | Tipo del documento de referencia | | `numDocModificado` | string | Si | Formato `ddd-ddd-ddddddddd` | | `fechaEmisionDocSustento` | string | Si | `dd/MM/yyyy` | | `totalDocumentoSustento` | number | No | Total del documento de referencia | | `totalSinImpuestos` | number | Si | Base de la nota | | `totalConImpuestos` | array | Si | `codigo`, `codigoPorcentaje`, `baseImponible`, `valor` | | `valorModificacion` | number | Si | Valor de la modificacion | | `moneda` | string | Si | Codigo de moneda | | `motivo` | string | Si | Motivo de la nota | ## `detalles` Lineas de la nota de credito. Usa `codigoInterno` (no `codigoPrincipal`). | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigoInterno` | string | Si | Codigo del bien o servicio | | `codigoAdicional` | string | No | Codigo adicional | | `descripcion` | string | Si | Descripcion de la linea | | `unidadMedida` | string | No | Unidad de medida | | `cantidad` | number | Si | Cantidad (hasta 6 decimales) | | `precioUnitario` | number | Si | Precio unitario (hasta 6 decimales) | | `descuento` | number | Si | Descuento de la linea (importe, no porcentaje) | | `precioTotalSinImpuesto` | number | Si | `cantidad x precioUnitario - descuento` | | `impuestos` | array | Si | Impuestos de la linea (ver abajo) | | `detallesAdicionales` | array | No | Hasta 3 objetos `{nombre, valor}` | ### `detalles[].impuestos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigo` | string | Si | Impuesto: `2` IVA, `3` ICE, `5` IRBPNR | | `codigoPorcentaje` | string | Si | Codigo de tarifa (ve [Catalogos](/v1/guias/catalogos)) | | `tarifa` | number | Si | Porcentaje de la tarifa | | `baseImponible` | number | Si | Base sobre la que se calcula | | `valor` | number | Si | Valor del impuesto | ### `infoAdicional` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `nombre` | string | Si | Nombre del campo adicional | | `valor` | string | Si | Valor del campo adicional | ## Ejemplo de solicitud ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: NC-2026-0003" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "NC-2026-0003"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "NC-2026-0003", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'NC-2026-0003', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: NC-2026-0003', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "NC-2026-0003") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` ## Cuerpo de la Nota de credito ```json { "origenReferencia": "MiERP", "referenciaExterna": "NC-2026-0003", "infoTributaria": { "ruc": "0123456789001", "codDoc": "04", "estab": "002", "ptoEmi": "001", "secuencial": "000000032" }, "info": { "fechaEmision": "07/09/2026", "tipoIdentificacionComprador": "05", "identificacionComprador": "0123456789", "razonSocialComprador": "CONTRIBUYENTE DE EJEMPLO", "codDocModificado": "01", "numDocModificado": "002-001-000000123", "fechaEmisionDocSustento": "01/09/2026", "totalDocumentoSustento": 115, "totalSinImpuestos": 20, "totalConImpuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "baseImponible": 20, "valor": 3 } ], "valorModificacion": 23, "moneda": "DOLAR", "motivo": "DEVOLUCION PARCIAL" }, "detalles": [ { "codigoInterno": "SERV-001", "descripcion": "Devolucion de servicio", "cantidad": 1, "precioUnitario": 20, "descuento": 0, "precioTotalSinImpuesto": 20, "impuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "tarifa": 15, "baseImponible": 20, "valor": 3 } ] } ] } ``` ## Reglas especificas - Si informas `totalDocumentoSustento`, `valorModificacion` no puede superarlo. - Informa tu los datos del documento de referencia; la API no los completa por ti. - Debe identificar al receptor (no consumidor final). ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta `urlEstado` o espera el [webhook](/v1/guias/webhooks). ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 202 | Emision aceptada | Sigue el estado de la operacion | | `201` | 202 | Reintento con la misma `Idempotency-Key` y el mismo contenido | Sigue la misma operacion | | `301` | 400 | Falta `Idempotency-Key` o hay campos mal formados | Corrige la solicitud | | `302` | 422 | El comprobante no cumple una regla fiscal o de estructura | Revisa `errores` | | `401` | 409 | Numero o clave ya registrados (duplicado) | Consulta la operacion existente | | `403` | 409 | Misma `Idempotency-Key` con otro contenido | Usa la misma intencion | | `501` | 429 | Cupo agotado | Amplia tu cupo | | `101` | 401 | API Key invalida | Revisa la API Key | | `103` | 403 | Contribuyente o tipo no habilitado | Habilita el contribuyente | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descarga el ejemplo de solicitud](/downloads/v1/ejemplos/example-04.json) - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Nota de debito La nota de debito incrementa el valor de un comprobante ya emitido (intereses, recargos, ajustes). La estructura del comprobante es la misma que en [Factura](/v1/guias/emision-01); cambia el contenido de `info`, la coleccion `motivos` y el ejemplo. > Ejemplo en el **ambiente de pruebas**. Los comprobantes no tienen validez fiscal. ``` POST /v1/contribuyentes/{identificacion}/comprobantes infoTributaria.codDoc = "05" ``` ## Estructura del comprobante | Clave | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `origenReferencia` | string | Si | Sistema de origen (max. 64) | | `referenciaExterna` | string | Si | Referencia estable de tu sistema (max. 128) | | `infoTributaria` | object | Si | Identidad del documento | | `info` | object | Si | Cabecera de la nota | | `motivos` | array | Si | Objetos `{razon, valor}` | | `infoAdicional` | array | No | Hasta 20 objetos `{nombre, valor}` | `infoTributaria`: `ruc` (13), `codDoc` (`05`), `estab` (3), `ptoEmi` (3) y `secuencial` (9) obligatorios. La clave de acceso la asigna la plataforma; no la envies. ## Campos de `info` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `fechaEmision` | string | Si | `dd/MM/yyyy` | | `tipoIdentificacionComprador` | string | Si | Catalogos de identificacion | | `identificacionComprador` | string | Si | Documento del comprador | | `razonSocialComprador` | string | Si | Razon social | | `codDocModificado` | string | Si | Tipo del documento de referencia | | `numDocModificado` | string | Si | Formato `ddd-ddd-ddddddddd` | | `fechaEmisionDocSustento` | string | Si | `dd/MM/yyyy` | | `totalSinImpuestos` | number | Si | Base | | `impuestos` | array | Si | `codigo`, `codigoPorcentaje`, `tarifa`, `baseImponible`, `valor` | | `valorTotal` | number | Si | Valor total de la nota | | `pagos` | array | Si | `formaPago` y `total` obligatorios | ## `motivos` Razones que sustentan la nota de debito. | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `razon` | string | Si | Descripcion del motivo | | `valor` | number | Si | Importe asociado al motivo | ## `info.impuestos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigo` | string | Si | Impuesto: `2` IVA, `3` ICE, `5` IRBPNR | | `codigoPorcentaje` | string | Si | Codigo de tarifa (ve [Catalogos](/v1/guias/catalogos)) | | `tarifa` | number | Si | Porcentaje de la tarifa | | `baseImponible` | number | Si | Base sobre la que se calcula | | `valor` | number | Si | Valor del impuesto | ## `pagos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `formaPago` | string | Si | Codigo de forma de pago | | `total` | number | Si | Monto con esta forma de pago | | `plazo` | number | No | Plazo | | `unidadTiempo` | string | Condicional | Obligatorio si `plazo > 0` | ## `infoAdicional` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `nombre` | string | Si | Nombre del campo adicional | | `valor` | string | Si | Valor del campo adicional | ## Ejemplo de solicitud ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: ND-2026-0002" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "ND-2026-0002"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "ND-2026-0002", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'ND-2026-0002', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: ND-2026-0002', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "ND-2026-0002") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` ## Cuerpo de la Nota de debito ```json { "origenReferencia": "MiERP", "referenciaExterna": "ND-2026-0002", "infoTributaria": { "ruc": "0123456789001", "codDoc": "05", "estab": "002", "ptoEmi": "001", "secuencial": "000000011" }, "info": { "fechaEmision": "07/09/2026", "tipoIdentificacionComprador": "05", "identificacionComprador": "0123456789", "razonSocialComprador": "CONTRIBUYENTE DE EJEMPLO", "codDocModificado": "01", "numDocModificado": "002-001-000000123", "fechaEmisionDocSustento": "01/09/2026", "totalSinImpuestos": 30, "valorTotal": 34.5, "impuestos": [ { "codigo": "2", "codigoPorcentaje": "4", "tarifa": 15, "baseImponible": 30, "valor": 4.5 } ], "pagos": [ { "formaPago": "01", "total": 34.5 } ] }, "motivos": [ { "razon": "INTERESES POR MORA", "valor": 34.5 } ] } ``` ## Notas - Usa `impuestos` de cabecera (no `totalConImpuestos`) y `valorTotal` (no `valorModificacion`). - La nota recibe una tarifa de IVA. - Debe identificar al receptor (no consumidor final). ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta `urlEstado` o espera el [webhook](/v1/guias/webhooks). ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 202 | Emision aceptada | Sigue el estado de la operacion | | `201` | 202 | Reintento con la misma `Idempotency-Key` y el mismo contenido | Sigue la misma operacion | | `301` | 400 | Falta `Idempotency-Key` o hay campos mal formados | Corrige la solicitud | | `302` | 422 | El comprobante no cumple una regla fiscal o de estructura | Revisa `errores` | | `401` | 409 | Numero o clave ya registrados (duplicado) | Consulta la operacion existente | | `403` | 409 | Misma `Idempotency-Key` con otro contenido | Usa la misma intencion | | `501` | 429 | Cupo agotado | Amplia tu cupo | | `101` | 401 | API Key invalida | Revisa la API Key | | `103` | 403 | Contribuyente o tipo no habilitado | Habilita el contribuyente | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descarga el ejemplo de solicitud](/downloads/v1/ejemplos/example-05.json) - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Guia de remision La guia de remision acompana el traslado de bienes. > Ejemplo en el **ambiente de pruebas**. Los comprobantes no tienen validez fiscal. ``` POST /v1/contribuyentes/{identificacion}/comprobantes infoTributaria.codDoc = "06" ``` No lleva importes y usa `destinatarios` en lugar de `detalles`. La estructura del comprobante es la misma que en [Factura](/v1/guias/emision-01); cambia el contenido de `info` y el cuerpo de ejemplo (`comprobante.json`). ## Estructura del comprobante | Clave | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `origenReferencia` | string | Si | Sistema de origen (max. 64) | | `referenciaExterna` | string | Si | Referencia estable de tu sistema (max. 128) | | `infoTributaria` | object | Si | Identidad del documento | | `info` | object | Si | Cabecera de la guia | | `destinatarios` | array | Si | Destinatarios con sus lineas | | `infoAdicional` | array | No | Hasta 20 objetos `{nombre, valor}` | `infoTributaria`: `ruc` (13), `codDoc` (`06`), `estab` (3), `ptoEmi` (3) y `secuencial` (9) obligatorios. La clave de acceso la asigna la plataforma; no la envies. ## Campos de `info` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `dirEstablecimiento` | string | No | Direccion del establecimiento | | `dirPartida` | string | Si | Direccion de partida | | `razonSocialTransportista` | string | Si | Razon social del transportista | | `tipoIdentificacionTransportista` | string | Si | Catalogos de identificacion | | `rucTransportista` | string | Si | Identificacion del transportista | | `fechaIniTransporte` | string | Si | `dd/MM/yyyy` | | `fechaFinTransporte` | string | Si | `dd/MM/yyyy` | | `placa` | string | Si | Placa del vehiculo | La guia no lleva importes. ## `destinatarios` La guia lleva un unico destinatario. | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `identificacionDestinatario` | string | Si | Cedula o RUC del destinatario | | `razonSocialDestinatario` | string | Si | Razon social del destinatario | | `dirDestinatario` | string | Si | Direccion de destino | | `motivoTraslado` | string | Si | Motivo del traslado | | `docAduaneroUnico` | string | No | Documento aduanero unico | | `codEstabDestino` | string | No | Codigo del establecimiento de destino | | `ruta` | string | No | Ruta del traslado | | `detalles` | array | Si | Bienes trasladados (ver abajo) | ### `destinatarios[].detalles` Las lineas de la guia no usan importes ni impuestos. | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigoInterno` | string | Si | Codigo del bien | | `descripcion` | string | Si | Descripcion del bien | | `cantidad` | number | Si | Cantidad trasladada | ## `infoAdicional` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `nombre` | string | Si | Nombre del campo adicional | | `valor` | string | Si | Valor del campo adicional | ## Ejemplo de solicitud ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: GR-2026-0004" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "GR-2026-0004"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "GR-2026-0004", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'GR-2026-0004', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: GR-2026-0004', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "GR-2026-0004") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` ## Cuerpo de la Guia de remision ```json { "origenReferencia": "MiERP", "referenciaExterna": "GR-2026-0004", "infoTributaria": { "ruc": "0123456789001", "codDoc": "06", "estab": "002", "ptoEmi": "001", "secuencial": "000000004" }, "info": { "dirEstablecimiento": "AV. PRINCIPAL 123", "dirPartida": "BODEGA CENTRAL", "razonSocialTransportista": "TRANSPORTES DE EJEMPLO", "tipoIdentificacionTransportista": "04", "rucTransportista": "1790012345001", "fechaIniTransporte": "07/09/2026", "fechaFinTransporte": "08/09/2026", "placa": "PBA1234" }, "destinatarios": [ { "identificacionDestinatario": "0123456789", "razonSocialDestinatario": "DESTINATARIO DE EJEMPLO", "dirDestinatario": "SUCURSAL NORTE", "motivoTraslado": "VENTA", "codEstabDestino": "001", "ruta": "QUITO - GUAYAQUIL", "detalles": [ { "codigoInterno": "PROD-001", "descripcion": "Caja de producto", "cantidad": 10 } ] } ] } ``` ## Notas - La guia admite un solo destinatario. - Las lineas de la guia no usan importes ni impuestos. ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta `urlEstado` o espera el [webhook](/v1/guias/webhooks). ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 202 | Emision aceptada | Sigue el estado de la operacion | | `201` | 202 | Reintento con la misma `Idempotency-Key` y el mismo contenido | Sigue la misma operacion | | `301` | 400 | Falta `Idempotency-Key` o hay campos mal formados | Corrige la solicitud | | `302` | 422 | El comprobante no cumple una regla fiscal o de estructura | Revisa `errores` | | `401` | 409 | Numero o clave ya registrados (duplicado) | Consulta la operacion existente | | `403` | 409 | Misma `Idempotency-Key` con otro contenido | Usa la misma intencion | | `501` | 429 | Cupo agotado | Amplia tu cupo | | `101` | 401 | API Key invalida | Revisa la API Key | | `103` | 403 | Contribuyente o tipo no habilitado | Habilita el contribuyente | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descarga el ejemplo de solicitud](/downloads/v1/ejemplos/example-06.json) - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Comprobante de retencion El comprobante de retencion acredita los impuestos retenidos a un proveedor. > Ejemplo en el **ambiente de pruebas**. Los comprobantes no tienen validez fiscal. ``` POST /v1/contribuyentes/{identificacion}/comprobantes infoTributaria.codDoc = "07" ``` Usa `docsSustento` en lugar de `detalles`. La estructura del comprobante es la misma que en [Factura](/v1/guias/emision-01); cambia el contenido de `info` y el cuerpo de ejemplo (`comprobante.json`). ## Estructura del comprobante | Clave | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `origenReferencia` | string | Si | Sistema de origen (max. 64) | | `referenciaExterna` | string | Si | Referencia estable de tu sistema (max. 128) | | `infoTributaria` | object | Si | Identidad del documento | | `info` | object | Si | Cabecera de la retencion | | `docsSustento` | array | Si | Documentos de sustento | | `infoAdicional` | array | No | Hasta 20 objetos `{nombre, valor}` | `infoTributaria`: `ruc` (13), `codDoc` (`07`), `estab` (3), `ptoEmi` (3) y `secuencial` (9) obligatorios. La clave de acceso la asigna la plataforma; no la envies. ## Campos de `info` | Campo | Tipo | Obligatorio | Regla | |---|---|---|---| | `fechaEmision` | string | Si | `dd/MM/yyyy` | | `tipoIdentificacionSujetoRetenido` | string | Si | Catalogos de identificacion | | `identificacionSujetoRetenido` | string | Si | Documento del sujeto retenido | | `razonSocialSujetoRetenido` | string | Si | Razon social | | `periodoFiscal` | string | Si | `MM/yyyy` | | `parteRel` | string | Si | `SI` o `NO` | ## `docsSustento` El comprobante lleva un unico documento de sustento. | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codSustento` | string | Si | Codigo de sustento (el `10` se rechaza) | | `codDocSustento` | string | Si | Tipo del documento de sustento (el `41` se rechaza) | | `numDocSustento` | string | Si | Numero del documento, 15 digitos sin guiones | | `fechaEmisionDocSustento` | string | Si | `dd/MM/yyyy` | | `fechaRegistroContable` | string | Si | `dd/MM/yyyy` | | `numAutDocSustento` | string | Si | Autorizacion del sustento electronico, 49 digitos | | `pagoLocExt` | string | Si | Pago local `01` o al exterior | | `totalSinImpuestos` | number | Si | Base total del sustento | | `importeTotal` | number | Si | Total del sustento | | `impuestosDocSustento` | array | Si | Impuestos del sustento (ver abajo) | | `retenciones` | array | Si | Retenciones aplicadas (ver abajo) | | `pagos` | array | Si | Pagos del sustento (ver abajo) | ### `docsSustento[].impuestosDocSustento` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codImpuestoDocSustento` | string | Si | Impuesto: `2` IVA, `3` ICE | | `codigoPorcentaje` | string | Si | Codigo de tarifa | | `baseImponible` | number | Si | Base del impuesto | | `tarifa` | number | Si | Porcentaje de la tarifa | | `valorImpuesto` | number | Si | Valor del impuesto | ### `docsSustento[].retenciones` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `codigo` | string | Si | Impuesto retenido: `1` renta, `2` IVA | | `codigoRetencion` | string | Si | Codigo de retencion | | `baseImponible` | number | Si | Base sobre la que se retiene | | `porcentajeRetener` | number | Si | Porcentaje de retencion | | `valorRetenido` | number | Si | Valor retenido | ### `docsSustento[].pagos` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `formaPago` | string | Si | Codigo de forma de pago | | `total` | number | Si | Monto con esta forma de pago | | `plazo` | number | No | Plazo | | `unidadTiempo` | string | Condicional | Obligatorio si `plazo > 0` | ## `infoAdicional` | Campo | Tipo | Obligatorio | Descripcion | |---|---|---|---| | `nombre` | string | Si | Nombre del campo adicional | | `valor` | string | Si | Valor del campo adicional | Codigos de retencion en [Catalogos: retenciones](/v1/guias/catalogos). ## Ejemplo de solicitud ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Idempotency-Key: RET-2026-0001" \ -H "Content-Type: application/json" \ -d @comprobante.json ``` ```csharp using System.Text; using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); http.DefaultRequestHeaders.Add("Idempotency-Key", "RET-2026-0001"); string json = await File.ReadAllTextAsync("comprobante.json"); using var contenido = new StringContent(json, Encoding.UTF8, "application/json"); using var respuesta = await http.PostAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", contenido); Console.WriteLine($"{(int)respuesta.StatusCode} {await respuesta.Content.ReadAsStringAsync()}"); ``` ```python import json, os, requests headers = { "X-Api-Key": os.environ["ECUAFACT_API_KEY"], "Idempotency-Key": "RET-2026-0001", } with open("comprobante.json", encoding="utf-8") as archivo: cuerpo = json.load(archivo) r = requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes", headers=headers, json=cuerpo, timeout=30) print(r.status_code, r.text) ``` ```javascript import { readFile } from 'node:fs/promises'; const cuerpo = await readFile('comprobante.json', 'utf8'); const res = await fetch( 'https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes', { method: 'POST', headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY, 'Idempotency-Key': 'RET-2026-0001', 'Content-Type': 'application/json' }, body: cuerpo }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('ECUAFACT_API_KEY'), 'Idempotency-Key: RET-2026-0001', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('comprobante.json'), ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); String cuerpo = Files.readString(Path.of("comprobante.json")); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .header("Idempotency-Key", "RET-2026-0001") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; // client: cliente EcuafactClient - ver SDK .NET EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {resultado.Admission.IdOperacion} / codigo: {resultado.Admission.Codigo}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.emitir(comprobante) print(f"Operacion {resultado.admission.id_operacion} / codigo: {resultado.admission.codigo}") ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.emitir(comprobante); console.log(`Operacion ${resultado.admission.idOperacion} / codigo: ${resultado.admission.codigo}`); ``` ```php-sdk emitir($comprobante); echo 'Operacion ' . $resultado->admission->idOperacion . ' / codigo: ' . $resultado->admission->codigo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EmisionResultado resultado = client.emitir(comprobante); System.out.println("Operacion " + resultado.admission().idOperacion() + " / codigo: " + resultado.admission().codigo()); ``` ## Cuerpo del Comprobante de retencion ```json { "origenReferencia": "MiERP", "referenciaExterna": "RET-2026-0001", "infoTributaria": { "ruc": "0123456789001", "codDoc": "07", "estab": "002", "ptoEmi": "001", "secuencial": "000000001" }, "info": { "fechaEmision": "07/09/2026", "tipoIdentificacionSujetoRetenido": "04", "identificacionSujetoRetenido": "1790012345001", "razonSocialSujetoRetenido": "PROVEEDOR DE EJEMPLO", "periodoFiscal": "09/2026", "parteRel": "NO" }, "docsSustento": [ { "codSustento": "01", "codDocSustento": "01", "numDocSustento": "002001000000123", "fechaEmisionDocSustento": "01/09/2026", "fechaRegistroContable": "01/09/2026", "numAutDocSustento": "0709202601179212345600110010020000001231234567818", "pagoLocExt": "01", "totalSinImpuestos": 100, "importeTotal": 115, "impuestosDocSustento": [ { "codImpuestoDocSustento": "2", "codigoPorcentaje": "4", "baseImponible": 100, "tarifa": 15, "valorImpuesto": 15 } ], "retenciones": [ { "codigo": "1", "codigoRetencion": "303", "baseImponible": 100, "porcentajeRetener": 1, "valorRetenido": 1 } ], "pagos": [ { "formaPago": "01", "total": 115 } ] } ] } ``` ## Reglas especificas - `numDocSustento` de 15 digitos **sin** guiones. - `numAutDocSustento` es el sustento electronico de 49 digitos. - Se rechazan `codSustento` `10` y `codDocSustento` `41`. - Los `codSustento` condicionales (banano) no son validos. ## Respuesta La API responde `202 Accepted` con la operacion creada: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento | El `202` no es la autorizacion del SRI: el comprobante quedo **recibido**. Consulta `urlEstado` o espera el [webhook](/v1/guias/webhooks). ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 202 | Emision aceptada | Sigue el estado de la operacion | | `201` | 202 | Reintento con la misma `Idempotency-Key` y el mismo contenido | Sigue la misma operacion | | `301` | 400 | Falta `Idempotency-Key` o hay campos mal formados | Corrige la solicitud | | `302` | 422 | El comprobante no cumple una regla fiscal o de estructura | Revisa `errores` | | `401` | 409 | Numero o clave ya registrados (duplicado) | Consulta la operacion existente | | `403` | 409 | Misma `Idempotency-Key` con otro contenido | Usa la misma intencion | | `501` | 429 | Cupo agotado | Amplia tu cupo | | `101` | 401 | API Key invalida | Revisa la API Key | | `103` | 403 | Contribuyente o tipo no habilitado | Habilita el contribuyente | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descarga el ejemplo de solicitud](/downloads/v1/ejemplos/example-07.json) - [Seguimiento de una operacion](/v1/guias/consulta-operacion) - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Descargar PDF Devuelve la representacion impresa del comprobante electronico (RIDE) en PDF. `identificacion` es el RUC del emisor habilitado en tu API Key y `claveAcceso` la clave de 49 digitos del comprobante emitido. ``` GET /v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/ride ``` ```bash curl -s "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/ride" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -o ride.pdf ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); byte[] pdf = await http.GetByteArrayAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/ride"); await File.WriteAllBytesAsync("ride.pdf", pdf); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/ride", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=60) open("ride.pdf", "wb").write(r.content) ``` ```javascript const res = await fetch( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/ride", { headers: { "X-Api-Key": process.env.ECUAFACT_API_KEY } }); const bytes = Buffer.from(await res.arrayBuffer()); await require("node:fs/promises").writeFile("ride.pdf", bytes); ``` ```php true, CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("ECUAFACT_API_KEY")], ]); $pdf = curl_exec($ch); curl_close($ch); file_put_contents("ride.pdf", $pdf); ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/ride")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofByteArray()); Files.write(Path.of("ride.pdf"), respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET ArchivoComprobante ride = await client.DescargarRideAsync(identificacion, claveAcceso); await File.WriteAllBytesAsync("ride.pdf", ride.Contenido); ``` ```python-sdk # client: cliente EcuafactClient - ver SDK Python archivo = client.descargar_ride(identificacion, clave_acceso) open("ride.pdf", "wb").write(archivo.contenido) ``` ```javascript-sdk import { writeFile } from "node:fs/promises"; // client: cliente EcuafactClient - ver SDK Node.js const archivo = await client.descargarRide(identificacion, claveAcceso); await writeFile("ride.pdf", archivo.contenido); ``` ```php-sdk descargarRide($identificacion, $claveAcceso); file_put_contents("ride.pdf", $archivo->contenido); ``` ```java-sdk import com.ecuafact.sdk.contracts.ArchivoComprobante; import java.nio.file.Files; import java.nio.file.Path; // client: cliente EcuafactClient - ver SDK Java ArchivoComprobante archivo = client.descargarRide(identificacion, claveAcceso); Files.write(Path.of("ride.pdf"), archivo.contenido()); ``` ## Respuesta `200` con el archivo en `application/pdf`. ## Estados de este endpoint | Estado | `codigo` | Cuando | |---|---|---| | 200 | - | PDF del RIDE | | 400 | 301 | La clave de acceso no es valida | | 401 | 101 | API Key invalida | | 403 | 103 | El contribuyente no esta habilitado en tu API Key | | 404 | 502 | El comprobante no existe o no tiene RIDE disponible | | 429 | 104 | Limite de solicitudes excedido | ## Siguientes pasos - El XML del comprobante: [Descargar XML](/v1/guias/consulta-xml). --- # Descargar XML Devuelve el XML del comprobante emitido en `application/xml`. `identificacion` es el RUC del emisor habilitado en tu API Key y `claveAcceso` la clave de 49 digitos del comprobante. ``` GET /v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/xml ``` ## Contenido provisional Si el SRI aun no entrega el XML autorizado, la respuesta es `200` con un XML de marcacion: - el documento incluye el comentario `PROVISIONAL`, - la cabecera `X-Contenido` vale `provisional`. Ese contenido no es el XML firmado por el SRI. En los SDK, el indicador `provisional` queda en `true`. ```bash curl -s -D - "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/xml" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -o comprobante.xml ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); using HttpResponseMessage respuesta = await http.GetAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/xml"); byte[] xml = await respuesta.Content.ReadAsByteArrayAsync(); bool provisional = respuesta.Headers.TryGetValues("X-Contenido", out var valores) && valores.Contains("provisional"); await File.WriteAllBytesAsync("comprobante.xml", xml); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/xml", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=60) print(r.headers.get("X-Contenido")) open("comprobante.xml", "wb").write(r.content) ``` ```javascript const res = await fetch( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/xml", { headers: { "X-Api-Key": process.env.ECUAFACT_API_KEY } }); console.log(res.headers.get("X-Contenido")); const bytes = Buffer.from(await res.arrayBuffer()); await require("node:fs/promises").writeFile("comprobante.xml", bytes); ``` ```php true, CURLOPT_HEADER => true, CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("ECUAFACT_API_KEY")], ]); $raw = curl_exec($ch); $tamano = curl_getinfo($ch, CURLINFO_HEADER_SIZE); curl_close($ch); file_put_contents("comprobante.xml", substr($raw, $tamano)); ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/xml")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofByteArray()); String marca = respuesta.headers().firstValue("X-Contenido").orElse(""); Files.write(Path.of("comprobante.xml"), respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET ArchivoComprobante xml = await client.DescargarXmlAsync(identificacion, claveAcceso); if (xml.Provisional) { Console.WriteLine("XML provisional"); } await File.WriteAllBytesAsync("comprobante.xml", xml.Contenido); ``` ```python-sdk # client: cliente EcuafactClient - ver SDK Python archivo = client.descargar_xml(identificacion, clave_acceso) print(archivo.provisional) open("comprobante.xml", "wb").write(archivo.contenido) ``` ```javascript-sdk import { writeFile } from "node:fs/promises"; // client: cliente EcuafactClient - ver SDK Node.js const archivo = await client.descargarXml(identificacion, claveAcceso); console.log(archivo.provisional); await writeFile("comprobante.xml", archivo.contenido); ``` ```php-sdk descargarXml($identificacion, $claveAcceso); file_put_contents("comprobante.xml", $archivo->contenido); ``` ```java-sdk import com.ecuafact.sdk.contracts.ArchivoComprobante; import java.nio.file.Files; import java.nio.file.Path; // client: cliente EcuafactClient - ver SDK Java ArchivoComprobante archivo = client.descargarXml(identificacion, claveAcceso); Files.write(Path.of("comprobante.xml"), archivo.contenido()); ``` ## Respuesta `200` con el archivo en `application/xml` y `Content-Disposition: attachment`. ## Estados de este endpoint | Estado | `codigo` | Cuando | |---|---|---| | 200 | - | XML (provisional o autorizado) | | 400 | 301 | La clave de acceso no es valida | | 401 | 101 | API Key invalida | | 403 | 103 | El contribuyente no esta habilitado en tu API Key | | 404 | 502 | El comprobante no existe | | 429 | 104 | Limite de solicitudes excedido | ## Siguientes pasos - El PDF del comprobante: [Descargar PDF](/v1/guias/consulta-ride). --- # Reenviar correo Reenvia el comprobante autorizado al correo indicado. Se adjuntan el PDF y el XML. ``` POST /v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/correo ``` Se admiten varios destinatarios separados por comas; no se envia asunto ni cuerpo HTML. Solo si el comprobante ya esta autorizado. Si no, `400` y codigo `302`. ```json { "destinatario": "cliente@ejemplo.com, copia@ejemplo.com" } ``` ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/correo" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"destinatario\":\"cliente@ejemplo.com\"}" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); await http.PostAsJsonAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/correo", new { destinatario = "cliente@ejemplo.com" }); ``` ```python import os, requests requests.post( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/correo", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, json={"destinatario": "cliente@ejemplo.com"}, timeout=60) ``` ```javascript await fetch( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/correo", { method: "POST", headers: { "X-Api-Key": process.env.ECUAFACT_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ destinatario: "cliente@ejemplo.com" }) }); ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-Api-Key: " . getenv("ECUAFACT_API_KEY"), "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode(["destinatario" => "cliente@ejemplo.com"]), ]); curl_exec($ch); ``` ```java HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/comprobantes/0101202601170123456789012201234567890123456789012/correo")) .header("X-Api-Key", apiKey) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"destinatario\":\"cliente@ejemplo.com\"}")) .build(); HttpClient.newHttpClient().send(peticion, HttpResponse.BodyHandlers.ofString()); ``` ```csharp-sdk await client.EnviarCorreoAsync(identificacion, claveAcceso, "cliente@ejemplo.com"); ``` ```python-sdk client.enviar_correo(identificacion, clave_acceso, "cliente@ejemplo.com") ``` ```javascript-sdk await client.enviarCorreo(identificacion, claveAcceso, "cliente@ejemplo.com"); ``` ```php-sdk enviarCorreo($identificacion, $claveAcceso, "cliente@ejemplo.com"); ``` ```java-sdk client.enviarCorreo(identificacion, claveAcceso, "cliente@ejemplo.com"); ``` Respuesta `200`: ```json { "datos": { "claveAcceso": "0101202601170123456789012201234567890123456789012", "destinatario": "cliente@ejemplo.com,copia@ejemplo.com" } } ``` ## Siguientes pasos - [Comprobantes emitidos](/v1/guias/consulta-emitidos) --- # Perfil del emisor Lee y actualiza el perfil del emisor que usa el RIDE: nombre comercial, direccion, contacto y logo. `identificacion` es el RUC habilitado en tu API Key. ``` GET /v1/contribuyentes/{identificacion}/perfil PUT /v1/contribuyentes/{identificacion}/perfil POST /v1/contribuyentes/{identificacion}/perfil/logo ``` ## Obtener ```bash curl -s "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil"); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.json()) ``` ```javascript const res = await fetch( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil", { headers: { "X-Api-Key": process.env.ECUAFACT_API_KEY } }); console.log(await res.json()); ``` ```php true, CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("ECUAFACT_API_KEY")], ]); echo curl_exec($ch); curl_close($ch); ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET PerfilEmisor perfil = await client.GetPerfilAsync(identificacion); Console.WriteLine(perfil.NombreComercial); ``` ```python-sdk # client: cliente EcuafactClient - ver SDK Python perfil = client.get_perfil(identificacion) print(perfil.nombre_comercial) ``` ```javascript-sdk // client: cliente EcuafactClient - ver SDK Node.js const perfil = await client.getPerfil(identificacion); console.log(perfil.nombreComercial); ``` ```php-sdk getPerfil($identificacion); echo $perfil->nombreComercial; ``` ```java-sdk import com.ecuafact.sdk.contracts.PerfilEmisor; // client: cliente EcuafactClient - ver SDK Java PerfilEmisor perfil = client.getPerfil(identificacion); System.out.println(perfil.nombreComercial); ``` ```json { "datos": { "identificacion": "0123456789001", "razonSocial": "PRUEBA 2 ECUAFACT", "nombreComercial": "Prueba", "direccionMatriz": "Av. PRINCIPAL", "correo": "contacto@empresa.com", "telefono": "042345678", "ciudad": "GUAYAQUIL", "provincia": "GUAYAS", "logo": "0123456789001_Logo.jpg" } } ``` ## Actualizar Solo se cambian los campos que envias. `razonSocial` no se edita por esta via. ```bash curl -s -X PUT "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "nombreComercial": "Prueba", "correo": "contacto@empresa.com" }' ``` ```csharp-sdk using Ecuafact.Sdk.Contracts; PerfilEmisorUpdate cambios = new() { NombreComercial = "Prueba", Correo = "contacto@empresa.com" }; PerfilEmisor perfil = await client.ActualizarPerfilAsync(identificacion, cambios); ``` ```python-sdk from ecuafact.models import PerfilEmisorUpdate cambios = PerfilEmisorUpdate(nombreComercial="Prueba", correo="contacto@empresa.com") perfil = client.actualizar_perfil(identificacion, cambios) ``` ```javascript-sdk const perfil = await client.actualizarPerfil(identificacion, { nombreComercial: "Prueba", correo: "contacto@empresa.com", }); ``` ```php-sdk nombreComercial = "Prueba"; $cambios->correo = "contacto@empresa.com"; $perfil = $client->actualizarPerfil($identificacion, $cambios); ``` ```java-sdk import com.ecuafact.sdk.contracts.PerfilEmisorUpdate; PerfilEmisorUpdate cambios = new PerfilEmisorUpdate(); cambios.nombreComercial = "Prueba"; cambios.correo = "contacto@empresa.com"; client.actualizarPerfil(identificacion, cambios); ``` ## Logo `POST /v1/contribuyentes/{identificacion}/perfil/logo` con `multipart/form-data`. Campo `logo`. PNG o JPEG, hasta 512 KB. ```bash curl -s -X POST "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789001/perfil/logo" \ -H "X-Api-Key: $ECUAFACT_API_KEY" \ -F "logo=@logo.png;type=image/png" ``` ```csharp-sdk byte[] png = await File.ReadAllBytesAsync("logo.png"); await client.ActualizarLogoAsync(identificacion, png, "image/png", "logo.png"); ``` ## Estados de este endpoint | Estado | `codigo` | Cuando | |---|---|---| | 200 | - | Perfil leido o actualizado | | 400 | 301 | El logo esta vacio, excede 512 KB o no es PNG/JPEG | | 401 | 101 | API Key invalida | | 403 | 103 | El contribuyente no esta habilitado en tu API Key | | 429 | 104 | Limite de solicitudes excedido | | 502 | 003 | No se pudo completar la actualizacion | ## Siguientes pasos - [Emision](/v1/guias/emision) --- # Contexto Devuelve el cliente y los contribuyentes habilitados para tu API Key. Es el punto de partida para conocer que RUC y servicios puedes operar. ``` GET /v1/contexto ``` ```bash curl -s "https://staging-api.mynexusapi.com/v1/contexto" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync("https://staging-api.mynexusapi.com/v1/contexto"); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/contexto", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.status_code, r.text) ``` ```javascript const res = await fetch('https://staging-api.mynexusapi.com/v1/contexto', { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contexto")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET Contexto contexto = await client.GetContextoAsync(); foreach (Contribuyente contribuyente in contexto.Contribuyentes ?? []) { Console.WriteLine(contribuyente.Identificacion); } ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python contexto = client.get_contexto() for contribuyente in contexto.contribuyentes or []: print(contribuyente.identificacion) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const contexto = await client.getContexto(); for (const contribuyente of contexto.contribuyentes ?? []) { console.log(contribuyente.identificacion); } ``` ```php-sdk getContexto(); foreach ($contexto->contribuyentes ?? [] as $contribuyente) { echo $contribuyente->identificacion . PHP_EOL; } ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java Contexto contexto = client.getContexto(); for (Contribuyente contribuyente : contexto.contribuyentes()) { System.out.println(contribuyente.identificacion()); } ``` ```json { "datos": { "idCliente": "1f2e3d4c-5b6a-4789-9012-abcdef012345", "nombre": "Integracion de ejemplo", "contribuyentes": [ { "identificacion": "0123456789", "identificacionCompleta": "0123456789001", "puedeEmitir": true, "puedeRecibir": true, "tiposComprobante": ["01", "03", "04", "05", "06", "07"] } ] } } ``` | Campo | Significado | |---|---| | `idCliente` | Cliente de la integracion | | `contribuyentes[].identificacion` | Identificacion que usas en las rutas de emision y consulta | | `contribuyentes[].identificacionCompleta` | RUC completo de 13 digitos | | `contribuyentes[].puedeEmitir` | Indica si el contribuyente puede emitir | | `contribuyentes[].puedeRecibir` | Indica si el contribuyente puede recibir comprobantes | | `contribuyentes[].tiposComprobante` | Tipos de comprobante habilitados por el plan | ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Contexto devuelto | Usa `identificacion` en las rutas | | `101` | 401 | API Key invalida | Revisa la API Key | | `102` | 403 | Origen de la solicitud no autorizado | Revisa la IP registrada para tu API Key | Si un contribuyente no aparece en el contexto, no esta habilitado para tu integracion: solicita su alta. El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Primeros pasos](/v1/guias/primeros-pasos) - [Ambientes y contribuyentes](/v1/guias/ambientes) --- # Comprobantes emitidos Devuelve los comprobantes emitidos por el contribuyente, paginados. ``` GET /v1/contribuyentes/{identificacion}/comprobantes/emitidos ``` ## Parametros | Parametro | Regla | |---|---| | `desde`, `hasta` | `yyyy-MM-dd`. Rango maximo 366 dias. Sin fechas: ultimos 30 dias | | `codDoc` | Filtra por tipo (`01`, `03`, `04`, `05`, `06`, `07`) | | `buscar` | Hasta 100 caracteres | | `pagina` | 1 a 10000 | | `tamanoPagina` | 1 a 100 | Fuera de esos limites la API responde `400` (`codigo` `303`). ```bash curl -s "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes/emitidos?desde=2026-01-01&hasta=2026-01-31&pagina=1&tamanoPagina=20" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string url = "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes/emitidos" + "?desde=2026-01-01&hasta=2026-01-31&pagina=1&tamanoPagina=20"; string json = await http.GetStringAsync(url); Console.WriteLine(json); ``` ```python import os, requests params = {"desde": "2026-01-01", "hasta": "2026-01-31", "pagina": 1, "tamanoPagina": 20} r = requests.get( "https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes/emitidos", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, params=params, timeout=30) print(r.status_code, r.text) ``` ```javascript const url = new URL('https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes/emitidos'); url.search = new URLSearchParams({ desde: '2026-01-01', hasta: '2026-01-31', pagina: '1', tamanoPagina: '20' }); const res = await fetch(url, { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/contribuyentes/0123456789/comprobantes/emitidos" + "?desde=2026-01-01&hasta=2026-01-31&pagina=1&tamanoPagina=20")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET PaginaComprobantes pagina = await client.ListarEmitidosAsync(new ListadoRequest { Desde = new DateTime(2026, 1, 1), Hasta = new DateTime(2026, 1, 31), Pagina = 1, TamanoPagina = 20 }); Console.WriteLine(pagina.HayMas); ``` ```python-sdk from ecuafact import EcuafactClient, ListadoRequest # client: cliente EcuafactClient - ver SDK Python pagina = client.listar_emitidos(ListadoRequest(desde="2026-01-01", hasta="2026-01-31", pagina=1, tamano_pagina=20)) print(pagina.hay_mas) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const pagina = await client.listarEmitidos({ desde: '2026-01-01', hasta: '2026-01-31', pagina: 1, tamanoPagina: 20 }); console.log(pagina.hayMas); ``` ```php-sdk listarEmitidos( (new ListadoRequest())->desde('2026-01-01')->hasta('2026-01-31')->pagina(1)->tamanoPagina(20) ); echo $pagina->hayMas ? 'true' : 'false'; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; import com.ecuafact.sdk.ListadoRequest; // client: cliente EcuafactClient - ver SDK Java PaginaComprobantes pagina = client.listarEmitidos( new ListadoRequest().desde("2026-01-01").hasta("2026-01-31").pagina(1).tamanoPagina(20)); System.out.println(pagina.hayMas()); ``` ```json { "datos": { "comprobantes": [ { "claveAcceso": "0709202601179212345600110010020000001231234567818", "numeroDocumento": "002-001-000000123", "codDoc": "01", "fechaEmision": "07/09/2026", "identificacionContraparte": "0123456789", "razonSocialContraparte": "CONTRIBUYENTE DE EJEMPLO", "total": 115.00, "estadoAutorizacion": "autorizado", "fechaAutorizacion": "2026-09-07T10:35:12-05:00", "codigoError": null } ], "pagina": 1, "tamanoPagina": 20, "hayMas": false } } ``` ## Respuesta | Campo | Significado | |---|---| | `claveAcceso` | Clave de acceso del comprobante (49 digitos) | | `numeroDocumento` | Numero en formato `estab-ptoEmi-secuencial` | | `codDoc` | Tipo de comprobante | | `fechaEmision` | Fecha de emision (`dd/MM/yyyy`) | | `identificacionContraparte` | Identificacion del receptor | | `razonSocialContraparte` | Nombre o razon social del receptor | | `total` | Importe total | | `estadoAutorizacion` | Resultado del SRI (`autorizado`, `pendiente`, `error`, etc.) | | `fechaAutorizacion` | Fecha de autorizacion (si aplica) | | `codigoError` | Codigo de error (si el comprobante fallo) | | `pagina` | Pagina actual | | `tamanoPagina` | Filas por pagina | | `hayMas` | `true` si quedan mas resultados | `fechaEmision` es la fecha del comprobante. Una pagina puede quedar vacia con `hayMas=true`; en ese caso, pide la siguiente pagina. ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Consulta exitosa | Lee `comprobantes` y `hayMas` | | `303` | 400 | Filtros fuera de rango (`desde`/`hasta`, `pagina`, `tamanoPagina`, `buscar`) | Ajusta los filtros | | `103` | 403 | Contribuyente o servicio de emitidos no habilitado | Habilita el contribuyente | | `101` | 401 | API Key invalida | Revisa la API Key | El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Descargar XML](/v1/guias/consulta-xml) - [Descargar PDF](/v1/guias/consulta-ride) --- # Catalogos Devuelve los valores de un catalogo del SRI. ``` GET /v1/catalogos/{nombre} ``` La respuesta incluye `codigo` y `nombre`; `tarifa` solo viene en IVA e ICE. La guia [Catalogos](/v1/guias/catalogos) explica donde se usa cada codigo. | nombre | Contenido | |---|---| | `tipos-identificacion` | Tipo de identificacion del comprador | | `formas-pago` | Forma de pago | | `tipos-comprobante` | `codDoc` | | `tipos-impuesto` | Codigo de impuesto | | `tarifas-iva` | Porcentaje de IVA | | `tarifas-ice` | Tarifa de ICE | Otro nombre responde `404` (`codigo` `502`). ## Respuesta ```json { "datos": [ { "codigo": "2", "nombre": "IVA 15%", "tarifa": 15.0 }, { "codigo": "0", "nombre": "IVA 0%", "tarifa": 0.0 } ] } ``` La lista viene dentro de `datos`; `tarifa` solo esta en `tarifas-iva` y `tarifas-ice`. ```bash curl -s "https://staging-api.mynexusapi.com/v1/catalogos/tarifas-iva" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync("https://staging-api.mynexusapi.com/v1/catalogos/tarifas-iva"); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/catalogos/tarifas-iva", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.json()) ``` ```javascript const res = await fetch("https://staging-api.mynexusapi.com/v1/catalogos/tarifas-iva", { headers: { "X-Api-Key": process.env.ECUAFACT_API_KEY } }); console.log(await res.json()); ``` ```php true, CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("ECUAFACT_API_KEY")], ]); echo curl_exec($ch); ``` ```java HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/catalogos/tarifas-iva")) .header("X-Api-Key", apiKey) .GET() .build(); HttpClient.newHttpClient().send(peticion, HttpResponse.BodyHandlers.ofString()); ``` ```csharp-sdk IReadOnlyList iva = await client.ListarCatalogoAsync("tarifas-iva"); ``` ```python-sdk iva = client.listar_catalogo("tarifas-iva") ``` ```javascript-sdk const iva = await client.listarCatalogo("tarifas-iva"); ``` ```php-sdk listarCatalogo("tarifas-iva"); ``` ```java-sdk List iva = client.listarCatalogo("tarifas-iva"); ``` ## Siguientes pasos - [Catalogos](/v1/guias/catalogos) --- # Seguimiento de una operacion Devuelve el estado actual de la operacion. `id` es el `idOperacion` que devuelve la emision. ``` GET /v1/operaciones/{id} ``` Si la operacion no existe responde `404` (`codigo` `502`). ```bash curl -s "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync( "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f"); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.status_code, r.text) ``` ```javascript const res = await fetch( 'https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f', { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET Operation operacion = await client.GetOperacionAsync(idOperacion); Console.WriteLine(operacion.Estado); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python operacion = client.get_operacion(id_operacion) print(operacion.estado) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const operacion = await client.getOperacion(idOperacion); console.log(operacion.estado); ``` ```php-sdk getOperacion($idOperacion); echo $operacion->estado; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java Operation operacion = client.getOperacion(idOperacion); System.out.println(operacion.estado()); ``` ```json { "datos": { "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "ambiente": 1, "codDoc": "01", "estado": "enviado", "claveAcceso": "0709202601179212345600110010020000001231234567818", "codigoError": null, "fechaCreacion": "2026-09-07T15:04:05Z", "fechaActualizacion": "2026-09-07T15:04:20Z", "estadoAutorizacion": "autorizado", "fechaAutorizacion": "2026-09-07T15:04:18Z", "fechaConsulta": "2026-09-07T15:05:00Z", "codigoErrorConsulta": null } } ``` `ambiente` es un codigo numerico (`1` pruebas, `2` produccion). `estado` describe el ciclo de envio; `estadoAutorizacion` el resultado fiscal: | `estado` | Significado | |---|---| | `en_cola` | Registrada, aun no enviada | | `enviando` | En despacho | | `enviado` | Enviado para procesamiento fiscal | | `resultado_desconocido` | El resultado aun no se confirma | | `cancelado` | Operacion cancelada | | `desconocido` | Estado no reconocido | | `estadoAutorizacion` | Significado | |---|---| | `no_disponible` | Aun sin resultado fiscal | | `pendiente` | En procesamiento por el SRI | | `autorizado` | Autorizacion confirmada | | `error` | Rechazo fiscal | | `desconocido` | Estado no reconocido | > Un codigo de procesamiento intermedio del SRI no basta para considerar > `autorizado`. El resultado fiscal final tambien llega por > [webhook](/v1/guias/webhooks). ## Siguientes pasos - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) --- # Consumo y cuotas Devuelve el cupo efectivo disponible, sumando el plan y los paquetes de documentos comprados. Coincide con lo que muestra el portal. ``` GET /v1/consumo ``` ```bash curl -s "https://staging-api.mynexusapi.com/v1/consumo" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync("https://staging-api.mynexusapi.com/v1/consumo"); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/consumo", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.status_code, r.text) ``` ```javascript const res = await fetch('https://staging-api.mynexusapi.com/v1/consumo', { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); $estado = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $estado . ' ' . $respuesta; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/consumo")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET QuotaBucket cupo = await client.GetConsumoAsync("pruebas"); Console.WriteLine($"{cupo.Disponibles}/{cupo.LimiteDocumentos}"); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python cupo = client.get_consumo() print(cupo.disponibles, cupo.limite_documentos) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const cupo = await client.getConsumo(); console.log(cupo.disponibles, cupo.limiteDocumentos); ``` ```php-sdk getConsumo(); echo $cupo->disponibles . '/' . $cupo->limiteDocumentos; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java QuotaBucket cupo = client.getConsumo(); System.out.println(cupo.disponibles() + "/" + cupo.limiteDocumentos()); ``` ```json { "limiteDocumentos": 1500, "reservados": 14, "consumidos": 0, "disponibles": 1486, "vigenteHasta": "2027-01-01", "limiteExtra": 100, "reservadosExtra": 0, "consumidosExtra": 0, "disponiblesExtra": 100 } ``` | Campo | Significado | |---|---| | `limiteDocumentos` | Cupo total disponible (plan + paquetes) | | `reservados` | Documentos recibidos y aun no resueltos | | `consumidos` | Documentos resueltos (autorizados o rechazados) | | `disponibles` | Cupo utilizable | | `vigenteHasta` | Vigencia del plan; sin plan se informa `9999-12-31` | | `*Extra` | Desglose del paquete adicional | Reglas de cupo: - El cupo se descuenta al resolverse la operacion, sea autorizada o rechazada. - Un documento por comprobante: los reintentos no descuentan de nuevo. - Solo las operaciones que aun no se enviaron liberan el cupo. - Sin cupo disponible, la emision responde `429` (`codigo` `501`). ## Siguientes pasos - [Limite de request](/v1/guias/limite-de-request) --- # Contribuyente por identificacion Devuelve los datos de un contribuyente del SRI por cedula, RUC o pasaporte: nombre o razon social, regimen, actividad, representantes legales, establecimientos y estado tributario. ``` GET /v1/consultas/contribuyentes/{identificacion} ``` La respuesta es **plana**. Los valores reales dependen de lo que devuelva el SRI en cada consulta. ## Parametros | Parametro | Donde | Regla | |---|---|---| | `identificacion` | ruta | Cedula (10), RUC (13) o pasaporte | | `fusionar` | query, opcional | `true` (por defecto) combina las fuentes; `false` usa la primera que responda | | `fuente` | query, opcional | `Todas` (por defecto) o `SriCatastro` | ```bash curl -s "https://staging-api.mynexusapi.com/v1/consultas/contribuyentes/1760013210001?fusionar=true" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync( "https://staging-api.mynexusapi.com/v1/consultas/contribuyentes/1760013210001?fusionar=true"); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/consultas/contribuyentes/1760013210001", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, params={"fusionar": "true"}, timeout=30) print(r.status_code, r.text) ``` ```javascript const url = new URL('https://staging-api.mynexusapi.com/v1/consultas/contribuyentes/1760013210001'); url.search = new URLSearchParams({ fusionar: 'true' }); const res = await fetch(url, { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . $respuesta; curl_close($ch); ``` ```java HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/consultas/contribuyentes/1760013210001?fusionar=true")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET ContribuyenteConsulta contribuyente = await client.ConsultarContribuyenteAsync("1760013210001", fusionar: true); Console.WriteLine(contribuyente.RazonSocial + " " + contribuyente.Estado); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python contribuyente = client.consultar_contribuyente("1760013210001") print(contribuyente.razon_social, contribuyente.estado) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const contribuyente = await client.consultarContribuyente("1760013210001"); console.log(contribuyente.razonSocial, contribuyente.estado); ``` ```php-sdk consultarContribuyente('1760013210001'); echo $contribuyente->razonSocial . ' ' . $contribuyente->estado; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java ContribuyenteConsulta contribuyente = client.consultarContribuyente("1760013210001"); System.out.println(contribuyente.razonSocial() + " " + contribuyente.estado()); ``` ```json { "identificacion": "1760013210001", "tipoIdentificacion": "RucSociedadPublica", "nombreCompleto": "RAZON SOCIAL DEL CONTRIBUYENTE", "razonSocial": "RAZON SOCIAL DEL CONTRIBUYENTE", "nombreComercial": null, "clase": "SociedadPublica", "estado": "ACTIVO", "regimen": "GENERAL", "actividadEconomicaPrincipal": "ACTIVIDADES DE SERVICIOS", "obligadoLlevarContabilidad": true, "agenteRetencion": false, "contribuyenteEspecial": false, "fechaInicioActividades": "2000-01-01T00:00:00", "fechaCese": null, "fechaReinicioActividades": null, "fechaActualizacion": "2026-01-01T00:00:00", "representantesLegales": [ { "identificacion": "1712345678", "nombre": "PEREZ LOPEZ JUAN" } ], "establecimientos": [ { "numero": "001", "nombreComercial": "SUCURSAL CENTRO", "tipo": "MATRIZ", "direccionCompleta": "AV. PRINCIPAL 123", "estado": "ABIERTO", "esMatriz": true } ], "estadoTributario": { "tieneDeuda": false, "tieneImpugnacion": false, "tieneRemision": false, "resumenDeuda": null, "resumenImpugnacion": null, "resumenRemision": null, "consultadoEn": "2026-09-30T08:00:00" }, "fuenteOrigen": "SriCatastro", "consultadoEn": "2026-09-30T08:00:00" } ``` ## Respuesta | Campo | Significado | |---|---| | `identificacion` | Identificacion consultada | | `tipoIdentificacion` | Tipo (`RucSociedadPublica`, `Cedula`, `Pasaporte`, etc.) | | `nombreCompleto`, `razonSocial` | Nombre o razon social | | `nombreComercial` | Nombre comercial (si aplica) | | `clase` | Persona natural, sociedad o sociedad publica | | `estado` | Estado del contribuyente (`ACTIVO`, etc.) | | `regimen` | Regimen tributario | | `actividadEconomicaPrincipal` | Actividad economica principal | | `obligadoLlevarContabilidad` | Si esta obligado a llevar contabilidad | | `agenteRetencion` | Si es agente de retencion | | `contribuyenteEspecial` | Si es contribuyente especial | | `fechaInicioActividades` | Inicio de actividades | | `fechaCese` | Cese de actividades (si aplica) | | `fechaReinicioActividades` | Reinicio de actividades (si aplica) | | `fechaActualizacion` | Ultima actualizacion de los datos | | `representantesLegales` | Representantes legales (si aplica) | | `establecimientos` | Establecimientos del contribuyente | | `estadoTributario` | Deudas, impugnaciones y remisiones | | `fuenteOrigen` | Fuente que respondio | | `consultadoEn` | Momento de la consulta | ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Consulta exitosa | Lee los datos | | `301` | 400 | Identificacion mal formada | Corrige la identificacion | | `502` | 404 | Sin resultado o ninguna fuente respondio | Revisa la identificacion | | `104` | 429 | Limite de solicitudes | Espera y reintenta | | `003` / `004` | 502 / 504 | Fuente caida o sin respuesta | Reintenta | Esta consulta no consume cupo de documentos. El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Errores](/v1/guias/errores) --- # Contribuyentes por nombre Busca contribuyentes por apellidos y nombres (o razon social) y devuelve identificaciones con sus nombres. La respuesta es un **arreglo**. ``` GET /v1/consultas/contribuyentes?apellidos=...&nombres=...&clase=...&max=... ``` ## Parametros | Parametro | Regla | |---|---| | `apellidos` | Obligatorio. En sociedades, el **inicio** de la razon social: un fragmento interno o final puede no encontrar coincidencias | | `nombres` | Opcional | | `clase` | `PersonaNatural` (por defecto), `Sociedad` o `SociedadPublica` | | `max` | Por defecto 10; tope 30 | Codificar los espacios como `%20`. ```bash curl -s "https://staging-api.mynexusapi.com/v1/consultas/contribuyentes?apellidos=PEREZ%20LOPEZ&nombres=JUAN&clase=PersonaNatural&max=10" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string url = "https://staging-api.mynexusapi.com/v1/consultas/contribuyentes" + "?apellidos=PEREZ%20LOPEZ&nombres=JUAN&clase=PersonaNatural&max=10"; string json = await http.GetStringAsync(url); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/consultas/contribuyentes", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, params={"apellidos": "PEREZ LOPEZ", "nombres": "JUAN", "clase": "PersonaNatural", "max": 10}, timeout=30) print(r.status_code, r.text) ``` ```javascript const url = new URL('https://staging-api.mynexusapi.com/v1/consultas/contribuyentes'); url.search = new URLSearchParams({ apellidos: 'PEREZ LOPEZ', nombres: 'JUAN', clase: 'PersonaNatural', max: '10' }); const res = await fetch(url, { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . $respuesta; curl_close($ch); ``` ```java HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/consultas/contribuyentes" + "?apellidos=PEREZ%20LOPEZ&nombres=JUAN&clase=PersonaNatural&max=10")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET IReadOnlyList encontrados = await client.BuscarContribuyentesAsync("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10); foreach (BusquedaContribuyente c in encontrados) { Console.WriteLine(c.Identificacion + " " + c.NombreCompleto); } ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python encontrados = client.buscar_contribuyentes("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10) for contribuyente in encontrados: print(contribuyente.identificacion, contribuyente.nombre_completo) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const encontrados = await client.buscarContribuyentes("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10); for (const contribuyente of encontrados) { console.log(contribuyente.identificacion, contribuyente.nombreCompleto); } ``` ```php-sdk buscarContribuyentes('PEREZ LOPEZ', 'JUAN', 'PersonaNatural', 10); foreach ($encontrados as $contribuyente) { echo $contribuyente->identificacion . ' ' . $contribuyente->nombreCompleto . PHP_EOL; } ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; import java.util.List; // client: cliente EcuafactClient - ver SDK Java List encontrados = client.buscarContribuyentes("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10); for (BusquedaContribuyente contribuyente : encontrados) { System.out.println(contribuyente.identificacion() + " " + contribuyente.nombreCompleto()); } ``` ```json [ { "identificacion": "1712345678", "tipoIdentificacion": "Cedula", "nombreCompleto": "PEREZ LOPEZ JUAN CARLOS", "clase": "PersonaNatural", "estado": "ACTIVO", "fuenteOrigen": "SriCatastro" } ] ``` ## Respuesta La respuesta es un arreglo de coincidencias; cada elemento trae: | Campo | Significado | |---|---| | `identificacion` | Identificacion del contribuyente | | `tipoIdentificacion` | Tipo (`Cedula`, `RucSociedadPrivada`, etc.) | | `nombreCompleto` | Nombre o razon social | | `clase` | Persona natural, sociedad o sociedad publica | | `estado` | Estado del contribuyente | | `fuenteOrigen` | Fuente que respondio | ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Busqueda exitosa | Lee las coincidencias | | `303` | 400 | `apellidos` ausente, o `clase`/`max` fuera de rango | Ajusta los parametros | | `104` | 429 | Limite de solicitudes | Espera y reintenta | | `003` / `004` | 502 / 504 | Fuente caida o sin respuesta | Reintenta | Esta consulta no consume cupo de documentos. El catalogo completo esta en [Errores](/v1/guias/errores). ## Siguientes pasos - [Errores](/v1/guias/errores) --- # Establecimientos de un RUC Devuelve los establecimientos de un RUC. La respuesta es **plana**. ``` GET /v1/consultas/establecimientos/{ruc}?filtro=... ``` ## Parametros | Parametro | Donde | Regla | |---|---|---| | `ruc` | ruta | RUC del contribuyente (13) | | `filtro` | query, opcional | `Todos` (por defecto), `SoloActivos` (estado ABIERTO), `SoloCerrados`, `SoloMatriz`, `SoloSucursales` | ```bash curl -s "https://staging-api.mynexusapi.com/v1/consultas/establecimientos/1760013210001?filtro=SoloActivos" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string url = "https://staging-api.mynexusapi.com/v1/consultas/establecimientos/1760013210001?filtro=SoloActivos"; string json = await http.GetStringAsync(url); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/consultas/establecimientos/1760013210001", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, params={"filtro": "SoloActivos"}, timeout=30) print(r.status_code, r.text) ``` ```javascript const url = new URL('https://staging-api.mynexusapi.com/v1/consultas/establecimientos/1760013210001'); url.search = new URLSearchParams({ filtro: 'SoloActivos' }); const res = await fetch(url, { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . $respuesta; curl_close($ch); ``` ```java HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/consultas/establecimientos/1760013210001?filtro=SoloActivos")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET EstablecimientosContribuyente establecimientos = await client.ListarEstablecimientosAsync("1760013210001", "SoloActivos"); Console.WriteLine(establecimientos.Cantidad); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python establecimientos = client.listar_establecimientos("1760013210001", "SoloActivos") print(establecimientos.cantidad) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const establecimientos = await client.listarEstablecimientos("1760013210001", "SoloActivos"); console.log(establecimientos.cantidad); ``` ```php-sdk listarEstablecimientos('1760013210001', 'SoloActivos'); echo $establecimientos->cantidad; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java EstablecimientosContribuyente establecimientos = client.listarEstablecimientos("1760013210001", "SoloActivos"); System.out.println(establecimientos.cantidad()); ``` ```json { "ruc": "1760013210001", "filtro": "SoloActivos", "cantidad": 1, "establecimientos": [ { "numero": "001", "nombreComercial": "SUCURSAL CENTRO", "tipo": "MATRIZ", "direccionCompleta": "AV. PRINCIPAL 123", "estado": "ABIERTO", "esMatriz": true } ] } ``` ## Respuesta | Campo | Significado | |---|---| | `ruc` | RUC consultado | | `filtro` | Filtro aplicado | | `cantidad` | Numero de establecimientos devueltos | | `establecimientos[].numero` | Numero del establecimiento | | `establecimientos[].nombreComercial` | Nombre comercial | | `establecimientos[].tipo` | Matriz o sucursal | | `establecimientos[].direccionCompleta` | Direccion | | `establecimientos[].estado` | Estado (`ABIERTO`, `CERRADO`) | | `establecimientos[].esMatriz` | `true` si es la matriz | ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Consulta exitosa | Lee los establecimientos | | `303` | 400 | `filtro` invalido | Ajusta el filtro | | `502` | 404 | Sin resultado | Revisa el RUC | | `104` | 429 | Limite de solicitudes | Espera y reintenta | | `003` / `004` | 502 / 504 | Fuente caida o sin respuesta | Reintenta | El catalogo completo esta en [Errores](/v1/guias/errores). Esta consulta no consume cupo de documentos. ## Siguientes pasos - [Errores](/v1/guias/errores) --- # Validar identificacion Valida el formato de una cedula, un RUC o un pasaporte sin consultar al SRI. Comprueba el digito verificador cuando corresponde; un pasaporte es alfanumerico de 5 a 20. ``` GET /v1/consultas/identificacion/validar/{numero} ``` **No confirma que exista.** Responde `200` tambien cuando el numero no es valido: en ese caso, revisa el campo `valido`. ## Parametros | Parametro | Donde | Regla | |---|---|---| | `numero` | ruta | Cedula, RUC o pasaporte a validar | ```bash curl -s "https://staging-api.mynexusapi.com/v1/consultas/identificacion/validar/1760013210001" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync( "https://staging-api.mynexusapi.com/v1/consultas/identificacion/validar/1760013210001"); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/consultas/identificacion/validar/1760013210001", headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.status_code, r.text) ``` ```javascript const res = await fetch( 'https://staging-api.mynexusapi.com/v1/consultas/identificacion/validar/1760013210001', { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . $respuesta; curl_close($ch); ``` ```java HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/consultas/identificacion/validar/1760013210001")) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET ValidacionIdentificacion resultado = await client.ValidarIdentificacionAsync("1760013210001"); Console.WriteLine(resultado.Valido + " " + resultado.Tipo); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python resultado = client.validar_identificacion("1760013210001") print(resultado.valido, resultado.tipo) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const resultado = await client.validarIdentificacion("1760013210001"); console.log(resultado.valido, resultado.tipo); ``` ```php-sdk validarIdentificacion('1760013210001'); echo ($resultado->valido ? 'true' : 'false') . ' ' . $resultado->tipo; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java ValidacionIdentificacion resultado = client.validarIdentificacion("1760013210001"); System.out.println(resultado.valido() + " " + resultado.tipo()); ``` ```json { "entrada": "1760013210001", "normalizado": "1760013210001", "valido": true, "tipo": "RucSociedadPublica", "longitud": 13, "esSoloDigitos": true, "cedulaBaseDelRuc": null, "mensaje": "Formato de RUC de sociedad publica aceptado; confirme su registro en el SRI." } ``` ## Respuesta | Campo | Significado | |---|---| | `entrada` | Numero enviado | | `normalizado` | Numero sin separadores | | `valido` | Si el formato es valido | | `tipo` | Tipo detectado (ver abajo) | | `longitud` | Longitud del numero | | `esSoloDigitos` | Si el numero es solo digitos | | `cedulaBaseDelRuc` | Cedula base (solo en RUC de persona natural) | | `mensaje` | Descripcion del resultado | `tipo`: `Desconocido`, `Cedula`, `RucPersonaNatural` (trae `cedulaBaseDelRuc`), `RucSociedadPrivada`, `RucSociedadPublica`, `Pasaporte`. ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Validacion de formato realizada | Revisa `valido` | | `301` | 400 | Numero mal formado | Corrige el numero | | `104` | 429 | Limite de solicitudes | Espera y reintenta | `200` no significa que el numero exista en el SRI: solo valida su formato. El catalogo completo esta en [Errores](/v1/guias/errores). Esta consulta no consume cupo de documentos. ## Siguientes pasos - [Errores](/v1/guias/errores) --- # Decodificar clave de acceso Valida el formato y el digito verificador de una clave de acceso, y devuelve sus segmentos. ``` GET /v1/consultas/comprobantes/decodificar/{claveAcceso} ``` **No prueba la autorizacion fiscal.** Una clave invalida responde `400` (`301`). La respuesta es **plana**. ## Parametros | Parametro | Donde | Regla | |---|---|---| | `claveAcceso` | ruta | Clave de acceso de 49 digitos | ```bash curl -s "https://staging-api.mynexusapi.com/v1/consultas/comprobantes/decodificar/0709202601179212345600110010020000001231234567818" \ -H "X-Api-Key: $ECUAFACT_API_KEY" ``` ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); string json = await http.GetStringAsync( "https://staging-api.mynexusapi.com/v1/consultas/comprobantes/decodificar/" + claveAcceso); Console.WriteLine(json); ``` ```python import os, requests r = requests.get( "https://staging-api.mynexusapi.com/v1/consultas/comprobantes/decodificar/" + clave_acceso, headers={"X-Api-Key": os.environ["ECUAFACT_API_KEY"]}, timeout=30) print(r.status_code, r.text) ``` ```javascript const res = await fetch( 'https://staging-api.mynexusapi.com/v1/consultas/comprobantes/decodificar/' + claveAcceso, { headers: { 'X-Api-Key': process.env.ECUAFACT_API_KEY } }); console.log(res.status, await res.text()); ``` ```php true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('ECUAFACT_API_KEY')], ]); $respuesta = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . $respuesta; curl_close($ch); ``` ```java HttpClient cliente = HttpClient.newHttpClient(); HttpRequest peticion = HttpRequest.newBuilder() .uri(URI.create("https://staging-api.mynexusapi.com/v1/consultas/comprobantes/decodificar/" + claveAcceso)) .header("X-Api-Key", System.getenv("ECUAFACT_API_KEY")) .GET() .build(); HttpResponse respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString()); System.out.println(respuesta.statusCode() + " " + respuesta.body()); ``` ```csharp-sdk using Ecuafact.Sdk; using Ecuafact.Sdk.Contracts; // client: cliente EcuafactClient - ver SDK .NET ClaveAccesoDecodificada decodificada = await client.DecodificarClaveAccesoAsync(claveAcceso); Console.WriteLine(decodificada.Tipo + " " + decodificada.FechaEmision); ``` ```python-sdk from ecuafact import EcuafactClient # client: cliente EcuafactClient - ver SDK Python clave = client.decodificar_clave_acceso(clave_acceso) print(clave.tipo, clave.fecha_emision) ``` ```javascript-sdk import { EcuafactClient } from "@ecuafact/sdk"; // client: cliente EcuafactClient - ver SDK Node.js const clave = await client.decodificarClaveAcceso(claveAcceso); console.log(clave.tipo, clave.fechaEmision); ``` ```php-sdk decodificarClaveAcceso($claveAcceso); echo $clave->tipo . ' ' . $clave->fechaEmision; ``` ```java-sdk import com.ecuafact.sdk.EcuafactClient; // client: cliente EcuafactClient - ver SDK Java ClaveAccesoDecodificada clave = client.decodificarClaveAcceso(claveAcceso); System.out.println(clave.tipo() + " " + clave.fechaEmision()); ``` ```json { "entrada": "0709202601176001321000110010010000000010000000119", "normalizada": "0709202601176001321000110010010000000010000000119", "valido": true, "longitud": 49, "fechaEmision": "2026-09-07", "tipo": "Factura", "rucEmisor": "1760013210001", "ambiente": "Produccion", "establecimiento": "001", "puntoEmision": "001", "secuencial": "000000123", "codigoNumerico": "12345678", "tipoEmision": "1", "tipoEmisionDescripcion": "Normal", "digitoVerificador": "9", "mensaje": "Clave de acceso decodificada correctamente." } ``` ## Respuesta | Campo | Significado | |---|---| | `entrada` | Clave enviada | | `normalizada` | Clave sin separadores | | `valido` | Si el formato y el digito verificador son validos | | `longitud` | Longitud de la clave (49) | | `fechaEmision` | Fecha de emision del comprobante | | `tipo` | Tipo de comprobante | | `rucEmisor` | RUC del emisor | | `ambiente` | Ambiente fiscal de la clave | | `establecimiento`, `puntoEmision`, `secuencial` | Serie del comprobante | | `codigoNumerico`, `digitoVerificador` | Partes de control | | `tipoEmision` / `tipoEmisionDescripcion` | Tipo de emision | ## Estados de este endpoint | Codigo | HTTP | Cuando ocurre | Que hacer | |---|---|---|---| | `200` | 200 | Clave procesada | Revisa `valido` antes de usarla | | `301` | 400 | Clave invalida | Corrige la clave | | `104` | 429 | Limite de solicitudes | Espera y reintenta | | `003` / `004` | 502 / 504 | Fuente caida o sin respuesta | Reintenta | El catalogo completo esta en [Errores](/v1/guias/errores). Esta consulta no consume cupo de documentos. ## Siguientes pasos - [Errores](/v1/guias/errores) --- # Webhooks: eventos La API te avisa de los cambios de estado de la operacion con un webhook: una peticion HTTP `POST` a la URL que configures. Cada envio va firmado para que puedas comprobar que es autentico. Configura la URL y el secreto en el portal, seccion **Webhooks**. El secreto se muestra al crearlo; guardalo para validar la firma. ## Entrega - Metodo `POST`, `Content-Type: application/json`. - Cabecera de firma: `Ecuafact-Signature: t=,v1=`. El campo `t` es el momento del envio y `v1` la firma. - Reintentos con espera escalonada: 1, 2, 5, 15, 30, 60, 120 y 240 minutos, hasta 8 intentos. - Agotados los 8 intentos, el evento queda registrado como no entregado. - Tras 8 fallos consecutivos el destino se pausa; un `2xx` no reanuda un destino pausado. - La reentrega de un mismo evento conserva `resourceId` y contenido; cambia el timestamp y la firma. > Responde `2xx` solo despues de persistir el evento. Un timeout se considera > fallo y provoca reintento. ## Garantias de entrega - **Entrega al menos una vez.** Un mismo evento puede entregarse mas de una vez (reintento o reenvio manual). **Deduplica** por `eventType` + `resourceId`; el `occurredAtUtc` se conserva igual en las reentregas. - **Sin orden garantizado.** Los eventos pueden llegar desordenados. Si necesitas el estado actual, consulta el [seguimiento](/v1/guias/consulta-operacion) en lugar de asumir el ultimo recibido. - **Reintento y reenvio.** Hasta 8 intentos con la escalera de espera de `## Entrega`. Un destino puede reenviar una entrega ya registrada; conserva `resourceId` y cuerpo, y cambia el timestamp y la firma. - **Responde rapido.** Persiste y responde `2xx`; procesa en segundo plano. Un timeout se trata como fallo y reintenta. - **Un `2xx` no reanuda un destino pausado.** La pausa por fallos consecutivos se reanuda desde el portal. ## Cuerpo El cuerpo es un objeto JSON con el tipo de evento, su identificacion y los datos del documento: ```json { "eventType": "document.authorized", "resourceId": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "occurredAtUtc": "2026-09-07T15:04:05.1234567Z", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "estado": "authorized", "claveAcceso": "0709202601179212345600110010020000001231234567818" } ``` Todos los valores del cuerpo son cadenas. `document.authorized` y `document.rejected` incluyen ademas `idOperacion`, `estado` y `claveAcceso`; `document.failed` agrega `motivo` y, si aplica, `codigoError`. | Campo | Significado | |---|---| | `eventType` | Tipo de evento (ver catalogo) | | `resourceId` | Identificador del recurso del evento | | `occurredAtUtc` | Momento del hecho (UTC, ISO 8601) | | `idOperacion` | Identificador de seguimiento de la operacion | | `estado` | Resultado: `authorized`, `rejected` o `failed` | | `claveAcceso` | Clave de acceso del comprobante | | `motivo` | Solo en `document.failed`: `estructura_invalida`, `entrega_no_posible` o `resultado_desconocido` | | `codigoError` | Solo en `document.failed`: codigo del error asociado | > El orden de las claves puede cambiar y pueden agregarse campos nuevos. Valida > siempre la firma antes de interpretar el cuerpo. ## Catalogo de eventos | Evento | Cuando | `motivo` | |---|---|---| | `document.authorized` | El SRI autorizo el comprobante | - | | `document.rejected` | El SRI rechazo el comprobante | - | | `document.failed` | No se pudo emitir: estructura invalida, entrega no posible o resultado desconocido | `estructura_invalida`, `entrega_no_posible`, `resultado_desconocido` | Solo se entregan los eventos de esta tabla. Si recibes un `eventType` que no conoces, ignoralo sin detener el procesamiento. `document.failed` significa que el comprobante **no llego a autorizarse por una causa de la plataforma o del propio documento**, no que el SRI lo haya rechazado. ## Implementacion en tu servidor Recibe el `POST`, lee el **cuerpo crudo** (tal como llego), verifica la firma y responde `2xx` despues de persistir. Cada ejemplo incluye la verificacion y la respuesta. ### Node.js (Express) ```javascript const crypto = require('crypto'); const express = require('express'); const app = express(); app.post('/webhooks/ecuafact', express.raw({ type: 'application/json' }), // cuerpo crudo, sin parsear (req, res) => { const cabecera = req.header('Ecuafact-Signature') || ''; const cuerpo = req.body.toString('utf8'); if (!verificar(process.env.ECUAFACT_WEBHOOK_SECRET, cabecera, cuerpo)) { return res.sendStatus(401); } const evento = JSON.parse(cuerpo); switch (evento.eventType) { case 'document.authorized': /* persistir autorizacion */ break; case 'document.rejected': /* persistir rechazo */ break; default: /* no-op */ break; } res.sendStatus(200); }); function verificar(secreto, cabecera, cuerpoCrudo, toleranciaSegundos = 300) { const partes = Object.fromEntries(cabecera.split(',').map((p) => p.split('='))); if (!partes.t || !partes.v1) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(partes.t)) > toleranciaSegundos) return false; const esperado = crypto.createHmac('sha256', secreto).update(`${partes.t}.${cuerpoCrudo}`, 'utf8').digest('hex'); const a = Buffer.from(esperado); const b = Buffer.from(partes.v1); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ### Python (Flask) ```python import hashlib, hmac, time, os from flask import Flask, request, abort app = Flask(__name__) SECRETO = os.environ["ECUAFACT_WEBHOOK_SECRET"] def verificar(cabecera, cuerpo_crudo, tolerancia=300): partes = dict(p.split("=", 1) for p in cabecera.split(",") if "=" in p) t, v1 = partes.get("t"), partes.get("v1") if not t or not v1 or abs(int(time.time()) - int(t)) > tolerancia: return False esperado = hmac.new(SECRETO.encode(), f"{t}.{cuerpo_crudo}".encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(esperado, v1) @app.post("/webhooks/ecuafact") def webhook(): cuerpo_crudo = request.get_data(as_text=True) # cuerpo crudo if not verificar(request.headers.get("Ecuafact-Signature", ""), cuerpo_crudo): abort(401) evento = request.get_json() if evento.get("eventType") == "document.authorized": pass # persistir autorizacion return "", 200 ``` ### PHP ```php $tolerancia) { return false; } $esperado = hash_hmac('sha256', $t . '.' . $cuerpoCrudo, $secreto); return hash_equals($esperado, strtolower($v1)); } ``` ### Java (Spring Boot) ```java import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Map; @RestController public class WebhookController { @PostMapping("/webhooks/ecuafact") public ResponseEntity recibir( @RequestHeader("Ecuafact-Signature") String cabecera, @RequestBody String cuerpoCrudo) throws Exception { if (!verificar(System.getenv("ECUAFACT_WEBHOOK_SECRET"), cabecera, cuerpoCrudo, 300)) { return ResponseEntity.status(401).build(); } // procesar cuerpoCrudo segun eventType y persistir return ResponseEntity.ok().build(); } static boolean verificar(String secreto, String cabecera, String cuerpoCrudo, long tolerancia) throws Exception { java.util.Map partes = new java.util.HashMap<>(); for (String p : cabecera.split(",")) { String[] kv = p.split("=", 2); if (kv.length == 2) { partes.put(kv[0], kv[1]); } } String t = partes.get("t"), v1 = partes.get("v1"); if (t == null || v1 == null) { return false; } if (Math.abs(Instant.now().getEpochSecond() - Long.parseLong(t)) > tolerancia) { return false; } Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secreto.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] hash = mac.doFinal((t + "." + cuerpoCrudo).getBytes(StandardCharsets.UTF_8)); StringBuilder hex = new StringBuilder(); for (byte b : hash) { hex.append(String.format("%02x", b)); } return java.security.MessageDigest.isEqual( hex.toString().getBytes(StandardCharsets.US_ASCII), v1.toLowerCase().getBytes(StandardCharsets.US_ASCII)); } } ``` ### .NET (ASP.NET Core) ```csharp using System.IO; using System.Text; using Ecuafact.Sdk; using Microsoft.AspNetCore.Mvc; [ApiController] public sealed class WebhookController : ControllerBase { [HttpPost("/webhooks/ecuafact")] public async Task Recibir() { using var lector = new StreamReader(Request.Body, Encoding.UTF8); string cuerpoCrudo = await lector.ReadToEndAsync(); string cabecera = Request.Headers["Ecuafact-Signature"].ToString(); // Opcion 1: helper del SDK. bool valido = WebhookSignature.Verify( Environment.GetEnvironmentVariable("ECUAFACT_WEBHOOK_SECRET")!, cabecera, cuerpoCrudo, DateTimeOffset.UtcNow, TimeSpan.FromMinutes(5)); // Opcion 2: verificacion propia (ver "Verificacion de firma"). if (!valido) { return Unauthorized(); } // Deserializar cuerpoCrudo, procesar segun eventType y persistir. return Ok(); } } ``` > En ASP.NET Core no uses el binding automatico del modelo para la verificacion: > el HMAC se calcula sobre los bytes exactos recibidos. Lee siempre el cuerpo > crudo (`Request.Body`). Los SDK oficiales incluyen el verificador de firma; usalo en lugar de reimplementar el HMAC. El detalle esta en [Verificacion de firma](/v1/guias/webhooks-firma). ## Siguientes pasos - [Verificacion de firma](/v1/guias/webhooks-firma) - [Errores](/v1/guias/errores) - [Comprobantes emitidos](/v1/guias/consulta-emitidos) --- # Verificacion de firma Cada entrega incluye la cabecera: ``` Ecuafact-Signature: t=1757341445,v1=6a7f...e21c ``` - `t`: marca de tiempo Unix (segundos) del envio. - `v1`: HMAC-SHA256 en hexadecimal minusculas. ## Cadena firmada El valor `v1` es el HMAC-SHA256 de la concatenacion, separada por un punto: ``` + "." + ``` Usa los **bytes exactos** del cuerpo recibido, tal como llego. Calcula el HMAC sobre UTF-8 y compara en tiempo constante. Rechaza las entregas cuya marca de tiempo quede fuera de una ventana (por ejemplo, 5 minutos) para resistir repeticiones. ## Verificacion ```bash # t y cuerpo recibidos; genera el HMAC y comparalo con v1 t="1757341445" cuerpo='{"eventType":"document.authorized"}' printf '%s' "$t.$cuerpo" | openssl dgst -sha256 -hmac "$ECUAFACT_WEBHOOK_SECRET" ``` ```csharp using System.Security.Cryptography; using System.Text; static bool Verificar(string secreto, string cabecera, string cuerpoCrudo, TimeSpan tolerancia) { // cabecera: "t=1757341445,v1=6a7f..." Dictionary partes = cabecera .Split(',') .Select(p => p.Split('=', 2)) .Where(p => p.Length == 2) .ToDictionary(p => p[0], p => p[1]); if (!partes.TryGetValue("t", out string? t) || !partes.TryGetValue("v1", out string? v1)) { return false; } if (!long.TryParse(t, out long unix)) { return false; } long ahora = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(ahora - unix) > (long)tolerancia.TotalSeconds) { return false; } string firmado = t + "." + cuerpoCrudo; using HMACSHA256 hmac = new(Encoding.UTF8.GetBytes(secreto)); byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(firmado)); string esperado = Convert.ToHexString(hash).ToLowerInvariant(); return CryptographicOperations.FixedTimeEquals( Encoding.ASCII.GetBytes(esperado), Encoding.ASCII.GetBytes(v1.ToLowerInvariant())); } ``` ```python import hashlib, hmac, time def verificar(secreto: str, cabecera: str, cuerpo_crudo: str, tolerancia: int = 300) -> bool: partes = dict(p.split('=', 1) for p in cabecera.split(',') if '=' in p) t, v1 = partes.get('t'), partes.get('v1') if not t or not v1: return False if abs(int(time.time()) - int(t)) > tolerancia: return False firmado = f"{t}.{cuerpo_crudo}".encode('utf-8') esperado = hmac.new(secreto.encode('utf-8'), firmado, hashlib.sha256).hexdigest() return hmac.compare_digest(esperado, v1) ``` ```javascript const crypto = require('crypto'); function verificar(secreto, cabecera, cuerpoCrudo, toleranciaSegundos) { const partes = Object.fromEntries( cabecera.split(',').map((p) => p.split('=')) ); const t = partes.t; const v1 = partes.v1; if (!t || !v1) return false; const ahora = Math.floor(Date.now() / 1000); if (Math.abs(ahora - Number(t)) > toleranciaSegundos) return false; const esperado = crypto .createHmac('sha256', secreto) .update(`${t}.${cuerpoCrudo}`, 'utf8') .digest('hex'); const a = Buffer.from(esperado); const b = Buffer.from(v1); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```php $tolerancia) { return false; } $esperado = hash_hmac('sha256', $t . '.' . $cuerpoCrudo, $secreto); return hash_equals($esperado, strtolower($v1)); } ``` ```java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.HashMap; import java.util.Map; static boolean verificar(String secreto, String cabecera, String cuerpoCrudo, long tolerancia) throws Exception { Map partes = new HashMap<>(); for (String p : cabecera.split(",")) { String[] kv = p.split("=", 2); if (kv.length == 2) { partes.put(kv[0], kv[1]); } } String t = partes.get("t"); String v1 = partes.get("v1"); if (t == null || v1 == null) { return false; } if (Math.abs(Instant.now().getEpochSecond() - Long.parseLong(t)) > tolerancia) { return false; } Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secreto.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] hash = mac.doFinal((t + "." + cuerpoCrudo).getBytes(StandardCharsets.UTF_8)); StringBuilder hex = new StringBuilder(); for (byte b : hash) { hex.append(String.format("%02x", b)); } return java.security.MessageDigest.isEqual( hex.toString().getBytes(StandardCharsets.US_ASCII), v1.toLowerCase().getBytes(StandardCharsets.US_ASCII)); } ``` ```csharp-sdk using Ecuafact.Sdk; bool valido = WebhookSignature.Verify(secreto, cabecera, cuerpoCrudo, DateTimeOffset.UtcNow, TimeSpan.FromMinutes(5)); ``` ```python-sdk from ecuafact import verify valido = verify(secreto, cabecera, cuerpo_crudo, tolerance_seconds=300) ``` ```javascript-sdk import { verify } from "@ecuafact/sdk"; const valido = verify(secreto, cabecera, cuerpoCrudo, new Date(), 300); ``` ```php-sdk # SDKs Ademas de llamar la API directamente, puedes usar un cliente oficial que resuelve las tareas repetitivas de la integracion. Disponibles para **.NET, Node.js, Python, Java y PHP**. ## Que resuelve el SDK - Coloca la API Key y las cabeceras necesarias en cada peticion. - Genera la `Idempotency-Key` y la reutiliza en los reintentos. - Reintenta fallos transitorios (timeout, `429` y `5xx`) con espera, respetando `Retry-After`. - Expone los errores como una excepcion con `codigo`, `mensaje` y `errores`. - Verifica la firma de los webhooks entrantes. ## Ejemplo ```csharp using Ecuafact.Sdk; EcuafactClient client = EcuafactClient.Create(new EcuafactClientOptions { BaseAddress = new Uri("https://staging-api.mynexusapi.com/"), ApiKey = Environment.GetEnvironmentVariable("ECUAFACT_API_KEY")!, Identificacion = "1790012345001" }); // comprobante: la Estructura del comprobante (ver [Factura](/v1/guias/emision-01)). EmisionResultado resultado = await client.EmitirAsync(comprobante); Console.WriteLine(resultado.Admission.IdOperacion); ``` El resultado trae `Admission` con `idOperacion`, `codigo`, `mensaje`, `urlEstado` y `uid`; y `IdempotencyKey` con la clave usada. ## Que puedes hacer con el SDK | Accion | Metodo (varia por lenguaje) | |---|---| | Emitir un comprobante | `EmitirAsync` / `emitir` / `emitirEn` / `emitir_en` | | Consultar el seguimiento de la operacion | `GetOperacionAsync` / `getOperacion` / `get_operacion` | | Obtener el contexto de tu API Key | `GetContextoAsync` / `getContexto` / `get_contexto` | | Ver el cupo disponible | `GetConsumoAsync` / `getConsumo` / `get_consumo` | | Listar emitidos | `ListarEmitidosAsync` | | Consultar datos del SRI | `ConsultarContribuyenteAsync`, `BuscarContribuyentesAsync`, `ListarEstablecimientosAsync`, `ValidarIdentificacionAsync`, `DecodificarClaveAccesoAsync` | | Reenviar el correo | `EnviarCorreoAsync` / `enviarCorreo` / `enviar_correo` | | Leer un catalogo | `ListarCatalogoAsync` / `listarCatalogo` / `listar_catalogo` | | Subir el logo | HTTP o `ActualizarLogoAsync` (.NET) | | Verificar un webhook | `WebhookSignature.Verify` (nombre segun lenguaje) | | Fijar un contribuyente | `Para` / `para` | ## Paginas por lenguaje - [.NET](/v1/guias/sdk-dotnet) - [Node.js](/v1/guias/sdk-node) - [Python](/v1/guias/sdk-python) - [Java](/v1/guias/sdk-java) - [PHP](/v1/guias/sdk-php) ## Siguientes pasos - [Autenticacion](/v1/guias/autenticacion): obten y usa tu API Key. - [Emision](/v1/guias/emision): emite tu primer comprobante. - [Verificacion de firma](/v1/guias/webhooks-firma): valida los webhooks entrantes. --- # SDK .NET Cliente oficial de la API para .NET. Cubre autenticacion, idempotencia, reintentos y firma de webhook. ## Paquetes | Paquete | Uso | |---|---| | `Ecuafact.Sdk` | Cliente, modelos de datos, excepciones y firma de webhooks | | `Ecuafact.Sdk.Core` | Registro del cliente para inyeccion de dependencias (ASP.NET Core o Worker Service) | Compatible con `netstandard2.0` (.NET Framework 4.6.1+) y `net10.0`. ## Instalacion ```bash dotnet add package Ecuafact.Sdk dotnet add package Ecuafact.Sdk.Core # opcional, para inyeccion de dependencias ``` ## Uso basico Crea el cliente una vez y reutilizalo. ```csharp using Ecuafact.Sdk; var client = EcuafactClient.Create(new EcuafactClientOptions { BaseAddress = new Uri("https://staging-api.mynexusapi.com/"), ApiKey = Environment.GetEnvironmentVariable("ECUAFACT_API_KEY")!, Identificacion = "1790012345001" // solo integraciones de un RUC }); // comprobante: la Estructura del comprobante (ver [Factura](/v1/guias/emision-01)). EmisionResultado r = await client.EmitirAsync(comprobante); Console.WriteLine($"Operacion {r.Admission.IdOperacion} / codigo: {r.Admission.Codigo}"); DateTime desde = new DateTime(2026, 1, 1); DateTime hasta = new DateTime(2026, 1, 31); PaginaComprobantes emitidos = await client.ListarEmitidosAsync( new ListadoRequest { Desde = desde, Hasta = hasta }); QuotaBucket cupo = await client.GetConsumoAsync(); Contexto contexto = await client.GetContextoAsync(); ``` Para varias API Key o varios RUC de la misma cuenta: ```csharp IEcuafactContribuyente a = client.Para("1790012345001"); IEcuafactContribuyente b = client.Para("1790099987001"); await a.EmitirAsync(comprobanteA); await b.EmitirAsync(comprobanteB); // concurrente y seguro ``` ## Uso con inyeccion de dependencias En aplicaciones con `Microsoft.Extensions.DependencyInjection` (ASP.NET Core, Worker Service) registra el cliente y resuelve la interfaz. ```csharp using Ecuafact.Sdk.Core; builder.Services.AddEcuafactClient(o => { o.BaseAddress = new Uri(configuration["Ecuafact:BaseUrl"]!); o.ApiKey = configuration["Ecuafact:ApiKey"]!; // o.Identificacion = "1790012345001"; // solo un RUC }); ``` ```csharp public sealed class Facturacion(IEcuafactClient client) { public async Task EmitirAsync(ComprobanteRequest comprobante, CancellationToken ct) { EmisionResultado r = await client.EmitirAsync(comprobante, ct: ct); Console.WriteLine(r.Admission.IdOperacion); } } ``` Varias integraciones (varios clientes): ```csharp builder.Services.AddEcuafactClient("integracionA", o => { o.BaseAddress = new Uri(configuration["EcuafactA:BaseUrl"]!); o.ApiKey = configuration["EcuafactA:ApiKey"]!; }); builder.Services.AddEcuafactClient("integracionB", o => { o.BaseAddress = new Uri(configuration["EcuafactB:BaseUrl"]!); o.ApiKey = configuration["EcuafactB:ApiKey"]!; }); ``` ```csharp public sealed class Multi(IEcuafactClientProvider provider) { public Task Emitir(CancellationToken ct) => provider.GetClient("integracionA").EmitirAsync(comprobante, ct: ct); } ``` El cliente por defecto tambien queda inyectable como `IEcuafactClient`. ## Idempotencia `Idempotency-Key` es opcional. Si no se envia, el SDK la genera, la usa en todos los reintentos y la devuelve en `EmisionResultado.IdempotencyKey`; si la llamada falla, viaja en `EcuafactApiException.IdempotencyKey`. Al reintentar a nivel de aplicacion **reutiliza la clave devuelta**: generar otra puede duplicar la emision. Los fallos transitorios (timeout, 408/425/429/5xx) se reintentan con la misma clave respetando `Retry-After`; el resto de los 4xx no. La espera se ajusta con `RespectRetryAfter` y `MaxRetryDelay`. ## Consultas SRI Consultas al SRI (contribuyentes, establecimientos, identificaciones y claves de acceso). Son de solo lectura, no consumen cupo y requieren la misma API Key. ```csharp ContribuyenteConsulta contribuyente = await client.ConsultarContribuyenteAsync("1760013210001"); IReadOnlyList encontrados = await client.BuscarContribuyentesAsync("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10); EstablecimientosContribuyente establecimientos = await client.ListarEstablecimientosAsync("1760013210001", "SoloActivos"); ValidacionIdentificacion validacion = await client.ValidarIdentificacionAsync("1760013210001"); ClaveAccesoDecodificada decodificada = await client.DecodificarClaveAccesoAsync(claveAcceso); ``` ## Webhook saliente ```csharp using Ecuafact.Sdk; bool valido = WebhookSignature.Verify(secreto, cabecera, cuerpoCrudo, DateTimeOffset.UtcNow, TimeSpan.FromMinutes(5)); ``` ## Errores Los errores del API se exponen como `EcuafactApiException` (`Codigo`, `Mensaje`, `EstadoHttp`, `IdSeguimiento`, `Errores`). `Errores` es una lista de strings y solo trae mensajes en errores de validacion. Los errores locales de configuracion o transporte son `EcuafactSdkException`. En emisiones exitosas, `EmisionResultado.CorrelationId` conserva el `X-Correlation-Id` de la respuesta; guardalo al reportar un incidente. ## Siguientes pasos - [Vision general de los SDK](/v1/guias/sdk): compara lenguajes y metodos. - [Autenticacion](/v1/guias/autenticacion): obten y usa tu API Key. - [Emision](/v1/guias/emision): emite tu primer comprobante. - [Verificacion de firma](/v1/guias/webhooks-firma): valida los webhooks entrantes. --- # SDK Node.js Cliente oficial de la API para Node.js (TypeScript). Cubre autenticacion, idempotencia, reintentos y firma de webhook. - Paquete: `@ecuafact/sdk`. - Requiere Node.js 18+ (usa `fetch` nativo). ## Como se instala ```bash npm install @ecuafact/sdk ``` ## Configuracion ```javascript import { EcuafactClient } from "@ecuafact/sdk"; const client = new EcuafactClient({ baseAddress: "https://staging-api.mynexusapi.com/", apiKey: process.env.ECUAFACT_API_KEY, identificacion: "1790012345001" // solo integraciones de un RUC }); ``` | Opcion | Default | Descripcion | |---|---|---| | `baseAddress` | - | Direccion base del API (obligatoria; define el ambiente) | | `apiKey` | - | API Key (obligatoria) | | `identificacion` | - | RUC por defecto (opcional) | | `timeoutMs` | `100000` | Tiempo maximo por intento | | `userAgent` | `Ecuafact.Sdk/1.0` | User-Agent | | `retryTransientFailures` | `true` | Reintenta `408/425/429/5xx` | | `maxAttempts` | `3` | Intentos por solicitud | | `respectRetryAfter` | `true` | Respeta `Retry-After` en `429`/`503` | | `maxRetryDelayMs` | `60000` | Espera maxima entre reintentos | ## Metodos | Metodo | Que hace | Devuelve | |---|---|---| | `emitir(comprobante)` | Emite para el RUC por defecto | `EmisionResultado` (`admission`, `idempotencyKey`) | | `emitirEn(ruc, comprobante)` | Emite para un RUC explicito | `EmisionResultado` | | `para(ruc)` | Fija un contribuyente | `EcuafactContribuyente` | | `getOperacion(idOperacion)` | Consulta el seguimiento de la operacion | `Operation` | | `getContexto()` | Contexto de tu API Key | `Contexto` | | `getConsumo()` | Cupo disponible | `QuotaBucket` | | `listarEmitidos(...)` | Listan comprobantes | `PaginaComprobantes` | | `consultarContribuyente(id)` | Consulta un contribuyente en el SRI | `ContribuyenteConsulta` | | `buscarContribuyentes(...)` | Busca por nombre | `BusquedaContribuyente[]` | | `listarEstablecimientos(ruc)` | Establecimientos de un RUC | `EstablecimientosContribuyente` | | `validarIdentificacion(numero)` | Valida el formato (sin red) | `ValidacionIdentificacion` | | `decodificarClaveAcceso(clave)` | Descompone una clave de acceso | `ClaveAccesoDecodificada` | | `verify(...)` | Verifica la firma de un webhook | `boolean` | ## Emision ```javascript const comprobante = { origenReferencia: "MiERP", referenciaExterna: "FACTURA-2026-0001", infoTributaria: { ruc: "1790012345001", codDoc: "01", estab: "002", ptoEmi: "001", secuencial: "000000123" }, info: { fechaEmision: "01/01/2026", tipoIdentificacionComprador: "04", identificacionComprador: "1790012345001", razonSocialComprador: "Cliente Ejemplo", totalSinImpuestos: 100.0, totalDescuento: 0.0, totalConImpuestos: [ { codigo: "2", codigoPorcentaje: "4", baseImponible: 100.0, valor: 15.0 } ], importeTotal: 115.0, moneda: "DOLAR", pagos: [{ formaPago: "01", total: 115.0 }] }, detalles: [ { codigoPrincipal: "SERV-001", descripcion: "Servicio de ejemplo", cantidad: 1, precioUnitario: 100.0, descuento: 0.0, precioTotalSinImpuesto: 100.0, impuestos: [ { codigo: "2", codigoPorcentaje: "4", tarifa: 15.0, baseImponible: 100.0, valor: 15.0 } ] } ] }; const resultado = await client.emitir(comprobante); console.log(resultado.admission.idOperacion, resultado.admission.codigo); ``` Emision con RUC explicito: `client.emitirEn("1790012345001", comprobante)`. ## Multi-RUC ```javascript const ruc = client.para("1790099987001"); const pagina = await ruc.listarEmitidos({ pagina: 1, tamanoPagina: 20 }); ``` ## Estado y consultas ```javascript const operacion = await client.getOperacion("3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f"); const contexto = await client.getContexto(); const cupo = await client.getConsumo(); ``` ## Consultas SRI Consultas al SRI (contribuyentes, establecimientos, identificaciones y claves de acceso). Son de solo lectura, no consumen cupo y usan la misma API Key. ```javascript const contribuyente = await client.consultarContribuyente("1760013210001"); const encontrados = await client.buscarContribuyentes("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10); const establecimientos = await client.listarEstablecimientos("1760013210001", "SoloActivos"); const validacion = await client.validarIdentificacion("1760013210001"); const clave = await client.decodificarClaveAcceso(claveAcceso); ``` ## Idempotencia `idempotencyKey` es opcional: si no se envia, el SDK la genera, la reutiliza en los reintentos y la devuelve en `resultado.idempotencyKey` (y en el error, como `EcuafactApiException.idempotencyKey`). Al reintentar, **reutiliza la clave devuelta**. La correlacion de la respuesta queda en `resultado.correlationId` (o `EcuafactApiException.idSeguimiento` en error). Los fallos transitorios (`408/425/429/5xx`) se reintentan respetando `Retry-After`; el resto de los 4xx no. ## Webhooks ```javascript import { verify } from "@ecuafact/sdk"; const valido = verify(secreto, cabecera, cuerpoCrudo, new Date(), 300); ``` Ve [Verificacion de firma](/v1/guias/webhooks-firma) y [Webhooks](/v1/guias/webhooks). ## Errores ```javascript try { await client.emitir(comprobante); } catch (error) { if (error instanceof EcuafactApiException) { console.error(error.codigo, error.mensaje, error.errores, error.idSeguimiento); } } ``` `EcuafactApiException` trae `codigo`, `mensaje`, `estadoHttp`, `idSeguimiento`, `errores` e `idempotencyKey`. Los errores locales son `EcuafactSdkException`. ## Siguientes pasos - [Vision general de los SDK](/v1/guias/sdk): compara lenguajes y metodos. - [Autenticacion](/v1/guias/autenticacion): obten y usa tu API Key. - [Emision](/v1/guias/emision): emite tu primer comprobante. - [Verificacion de firma](/v1/guias/webhooks-firma): valida los webhooks entrantes. --- # SDK Python Cliente oficial de la API para Python. Cubre autenticacion, idempotencia, reintentos y firma de webhook. - Paquete: `ecuafact`. - Dependencias: `httpx` y `pydantic` (v2). ## Como se instala ```bash pip install ecuafact ``` ## Configuracion ```python import os from ecuafact import EcuafactClient, EcuafactClientOptions client = EcuafactClient(EcuafactClientOptions( base_address="https://staging-api.mynexusapi.com/", api_key=os.environ["ECUAFACT_API_KEY"], identificacion="1790012345001", # solo integraciones de un RUC )) ``` | Opcion | Default | Descripcion | |---|---|---| | `base_address` | - | Direccion base del API (obligatoria; define el ambiente) | | `api_key` | - | API Key (obligatoria) | | `identificacion` | `None` | RUC por defecto (opcional) | | `timeout` | `100.0` | Tiempo maximo por intento (segundos) | | `user_agent` | `Ecuafact.Sdk/1.0` | User-Agent | | `retry_transient_failures` | `True` | Reintenta `408/425/429/5xx` | | `max_attempts` | `3` | Intentos por solicitud | | `respect_retry_after` | `True` | Respeta `Retry-After` en `429`/`503` | | `max_retry_delay_ms` | `60000` | Espera maxima entre reintentos | ## Metodos | Metodo | Que hace | Devuelve | |---|---|---| | `emitir(comprobante)` | Emite para el RUC por defecto | `EmisionResultado` | | `emitir_en(ruc, comprobante)` | Emite para un RUC explicito | `EmisionResultado` | | `para(ruc)` | Fija un contribuyente | `EcuafactContribuyente` | | `get_operacion(id)` | Consulta el seguimiento de la operacion | `Operation` | | `get_contexto()` | Contexto de tu API Key | `Contexto` | | `get_consumo()` | Cupo disponible | `QuotaBucket` | | `listar_emitidos(...)` | Listan comprobantes | `PaginaComprobantes` | | `consultar_contribuyente(id)` | Consulta un contribuyente en el SRI | `ContribuyenteConsulta` | | `buscar_contribuyentes(...)` | Busca por nombre | `list[BusquedaContribuyente]` | | `listar_establecimientos(ruc)` | Establecimientos de un RUC | `EstablecimientosContribuyente` | | `validar_identificacion(numero)` | Valida el formato (sin red) | `ValidacionIdentificacion` | | `decodificar_clave_acceso(clave)` | Descompone una clave de acceso | `ClaveAccesoDecodificada` | | `verify(...)` | Verifica la firma de un webhook | `bool` | ## Emision ```python from ecuafact import ComprobanteRequest, InfoTributaria resultado = client.emitir(ComprobanteRequest( origen_referencia="MiERP", referencia_externa="FACTURA-2026-0001", info_tributaria=InfoTributaria( ruc="1790012345001", cod_doc="01", estab="002", pto_emi="001", secuencial="000000123" ), info={ "fechaEmision": "01/01/2026", "tipoIdentificacionComprador": "04", "identificacionComprador": "1790012345001", "razonSocialComprador": "Cliente Ejemplo", "totalSinImpuestos": 100.0, "totalDescuento": 0.0, "totalConImpuestos": [ {"codigo": "2", "codigoPorcentaje": "4", "baseImponible": 100.0, "valor": 15.0} ], "importeTotal": 115.0, "moneda": "DOLAR", "pagos": [{"formaPago": "01", "total": 115.0}], }, detalles=[{ "codigoPrincipal": "SERV-001", "descripcion": "Servicio de ejemplo", "cantidad": 1, "precioUnitario": 100.0, "descuento": 0.0, "precioTotalSinImpuesto": 100.0, "impuestos": [ {"codigo": "2", "codigoPorcentaje": "4", "tarifa": 15.0, "baseImponible": 100.0, "valor": 15.0} ], }], )) print(resultado.admission.id_operacion, resultado.admission.codigo) ``` Emision con RUC explicito: `client.emitir_en("1790012345001", comprobante)`. ## Multi-RUC ```python ruc = client.para("1790099987001") pagina = ruc.listar_emitidos() ``` ## Estado y consultas ```python operacion = client.get_operacion("3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f") contexto = client.get_contexto() cupo = client.get_consumo() ``` ## Consultas SRI Consultas al SRI (contribuyentes, establecimientos, identificaciones y claves de acceso). Son de solo lectura, no consumen cupo y usan la misma API Key. ```python contribuyente = client.consultar_contribuyente("1760013210001") encontrados = client.buscar_contribuyentes("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10) establecimientos = client.listar_establecimientos("1760013210001", "SoloActivos") validacion = client.validar_identificacion("1760013210001") clave = client.decodificar_clave_acceso(clave_acceso) ``` ## Idempotencia `idempotency_key` es opcional: si no se envia, el SDK la genera, la reutiliza en los reintentos y la devuelve en `resultado.idempotency_key` (y en el error, como `EcuafactApiException.idempotency_key`). Al reintentar, **reutiliza la clave devuelta**. La correlacion de la respuesta queda en `resultado.correlation_id` (o `EcuafactApiException.id_seguimiento` en error). Los fallos transitorios (`408/425/429/5xx`) se reintentan respetando `Retry-After`; el resto de los 4xx no. ## Webhooks ```python from ecuafact import verify valido = verify(secreto, cabecera, cuerpo_crudo, tolerance_seconds=300) ``` Ve [Verificacion de firma](/v1/guias/webhooks-firma) y [Webhooks](/v1/guias/webhooks). ## Errores ```python from ecuafact import EcuafactApiException try: client.emitir(comprobante) except EcuafactApiException as error: print(error.codigo, error.mensaje, error.errores, error.id_seguimiento) ``` `EcuafactApiException` trae `codigo`, `mensaje`, `estado_http`, `id_seguimiento`, `errores` e `idempotency_key`. Los errores locales son `EcuafactSdkException`. ## Siguientes pasos - [Vision general de los SDK](/v1/guias/sdk): compara lenguajes y metodos. - [Autenticacion](/v1/guias/autenticacion): obten y usa tu API Key. - [Emision](/v1/guias/emision): emite tu primer comprobante. - [Verificacion de firma](/v1/guias/webhooks-firma): valida los webhooks entrantes. --- # SDK Java Cliente oficial de la API para Java (17+). Cubre autenticacion, idempotencia, reintentos y firma de webhook. - Paquete: `com.ecuafact:sdk`. - Dependencia JSON: Jackson Databind. ## Como se instala Agrega la dependencia a tu proyecto: ```xml com.ecuafact sdk 1.0.0-beta1 ``` ## Configuracion ```java import com.ecuafact.sdk.EcuafactClient; import com.ecuafact.sdk.EcuafactClientOptions; EcuafactClient client = new EcuafactClient( EcuafactClientOptions.create("https://staging-api.mynexusapi.com/", System.getenv("ECUAFACT_API_KEY")) .identificacion("1790012345001")); // solo integraciones de un RUC ``` | Opcion | Default | Descripcion | |---|---|---| | `baseAddress` | - | Direccion base del API (obligatoria; define el ambiente) | | `apiKey` | - | API Key (obligatoria) | | `identificacion` | - | RUC por defecto (opcional) | | `timeout` | `PT100S` | Tiempo maximo por intento | | `userAgent` | `Ecuafact.Sdk/1.0` | User-Agent | | `retryTransientFailures` | `true` | Reintenta `408/425/429/5xx` | | `maxAttempts` | `3` | Intentos por solicitud | | `respectRetryAfter` | `true` | Respeta `Retry-After` en `429`/`503` | | `maxRetryDelay` | `PT60S` | Espera maxima entre reintentos | ## Metodos | Metodo | Que hace | Devuelve | |---|---|---| | `emitir(comprobante)` | Emite para el RUC por defecto | `EmisionResultado` | | `emitirEn(ruc, comprobante)` | Emite para un RUC explicito | `EmisionResultado` | | `para(ruc)` | Fija un contribuyente | `EcuafactContribuyente` | | `getOperacion(id)` | Consulta el seguimiento de la operacion | `Operation` | | `getContexto()` | Contexto de tu API Key | `Contexto` | | `getConsumo()` | Cupo disponible | `QuotaBucket` | | `listarEmitidos(...)` | Listan comprobantes | `PaginaComprobantes` | | `consultarContribuyente(id)` | Consulta un contribuyente en el SRI | `ContribuyenteConsulta` | | `buscarContribuyentes(...)` | Busca por nombre | `List` | | `listarEstablecimientos(ruc)` | Establecimientos de un RUC | `EstablecimientosContribuyente` | | `validarIdentificacion(numero)` | Valida el formato (sin red) | `ValidacionIdentificacion` | | `decodificarClaveAcceso(clave)` | Descompone una clave de acceso | `ClaveAccesoDecodificada` | | `verify(...)` | Verifica la firma de un webhook | `boolean` | ## Emision ```java import com.ecuafact.sdk.EmisionResultado; import com.ecuafact.sdk.contracts.ComprobanteRequest; import com.ecuafact.sdk.contracts.InfoTributaria; // Los ultimos argumentos son las colecciones que no usa una factura. ComprobanteRequest comprobante = new ComprobanteRequest( "MiERP", "FACTURA-2026-0001", new InfoTributaria("1790012345001", "01", "002", "001", "000000123"), null, null, null, null, null, null); EmisionResultado resultado = client.emitir(comprobante); System.out.println(resultado.admission().idOperacion() + " " + resultado.admission().codigo()); ``` Emision con RUC explicito: `client.emitirEn("1790012345001", comprobante)`. ## Multi-RUC ```java EcuafactContribuyente ruc = client.para("1790099987001"); PaginaComprobantes pagina = ruc.listarEmitidos(); ``` ## Estado y consultas ```java Operation operacion = client.getOperacion("3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f"); Contexto contexto = client.getContexto(); QuotaBucket cupo = client.getConsumo(); ``` ## Consultas SRI Consultas al SRI (contribuyentes, establecimientos, identificaciones y claves de acceso). Son de solo lectura, no consumen cupo y usan la misma API Key. ```java ContribuyenteConsulta contribuyente = client.consultarContribuyente("1760013210001"); List encontrados = client.buscarContribuyentes("PEREZ LOPEZ", "JUAN", "PersonaNatural", 10); EstablecimientosContribuyente establecimientos = client.listarEstablecimientos("1760013210001", "SoloActivos"); ValidacionIdentificacion validacion = client.validarIdentificacion("1760013210001"); ClaveAccesoDecodificada clave = client.decodificarClaveAcceso(claveAcceso); ``` ## Idempotencia `idempotencyKey` es opcional: si no se envia, el SDK la genera, la reutiliza en los reintentos y la devuelve en `resultado.idempotencyKey()` (y en el error, como `EcuafactApiException.idempotencyKey()`). Al reintentar, **reutiliza la clave devuelta**. La correlacion de la respuesta queda en `resultado.correlationId()` (o `EcuafactApiException.idSeguimiento()` en error). Los fallos transitorios (`408/425/429/5xx`) se reintentan respetando `Retry-After`; el resto de los 4xx no. ## Webhooks ```java import com.ecuafact.sdk.WebhookSignature; import java.time.Instant; boolean valido = WebhookSignature.verify(secreto, cabecera, cuerpoCrudo, Instant.now(), 300); ``` Ve [Verificacion de firma](/v1/guias/webhooks-firma) y [Webhooks](/v1/guias/webhooks). ## Errores `EcuafactApiException` trae `codigo()`, `mensaje()`, `estadoHttp()`, `idSeguimiento()`, `errores()` e `idempotencyKey()`. Los errores locales son `EcuafactSdkException`. ## Siguientes pasos - [Vision general de los SDK](/v1/guias/sdk): compara lenguajes y metodos. - [Autenticacion](/v1/guias/autenticacion): obten y usa tu API Key. - [Emision](/v1/guias/emision): emite tu primer comprobante. - [Verificacion de firma](/v1/guias/webhooks-firma): valida los webhooks entrantes. --- # SDK PHP Cliente oficial de la API para PHP. Cubre autenticacion, idempotencia, reintentos y firma de webhook. - Paquete: `ecuafact/sdk`. - Requiere PHP 8.2+ con `ext-curl`. ## Como se instala ```bash composer require ecuafact/sdk ``` ## Configuracion ```php use Ecuafact\Sdk\EcuafactClient; use Ecuafact\Sdk\EcuafactClientOptions; $client = new EcuafactClient(new EcuafactClientOptions( baseAddress: 'https://staging-api.mynexusapi.com/', apiKey: getenv('ECUAFACT_API_KEY'), identificacion: '1790012345001', // solo integraciones de un RUC )); ``` | Opcion | Default | Descripcion | |---|---|---| | `baseAddress` | - | Direccion base del API (obligatoria; define el ambiente) | | `apiKey` | - | API Key (obligatoria) | | `identificacion` | `null` | RUC por defecto (opcional) | | `timeout` | `100.0` | Tiempo maximo por intento (segundos) | | `userAgent` | `Ecuafact.Sdk/1.0` | User-Agent | | `retryTransientFailures` | `true` | Reintenta `408/425/429/5xx` | | `maxAttempts` | `3` | Intentos por solicitud | | `respectRetryAfter` | `true` | Respeta `Retry-After` en `429`/`503` | | `maxRetryDelaySeconds` | `60.0` | Espera maxima entre reintentos | ## Metodos | Metodo | Que hace | Devuelve | |---|---|---| | `emitir($comprobante)` | Emite para el RUC por defecto | `EmisionResultado` | | `emitirEn($ruc, $comprobante)` | Emite para un RUC explicito | `EmisionResultado` | | `para($ruc)` | Fija un contribuyente | `EcuafactContribuyente` | | `getOperacion($id)` | Consulta el seguimiento de la operacion | `Operation` | | `getContexto()` | Contexto de tu API Key | `Contexto` | | `getConsumo()` | Cupo disponible | `QuotaBucket` | | `listarEmitidos(...)` | Listan comprobantes | `PaginaComprobantes` | | `consultarContribuyente($id)` | Consulta un contribuyente en el SRI | `ContribuyenteConsulta` | | `buscarContribuyentes(...)` | Busca por nombre | `BusquedaContribuyente[]` | | `listarEstablecimientos($ruc)` | Establecimientos de un RUC | `EstablecimientosContribuyente` | | `validarIdentificacion($numero)` | Valida el formato (sin red) | `ValidacionIdentificacion` | | `decodificarClaveAcceso($clave)` | Descompone una clave de acceso | `ClaveAccesoDecodificada` | | `verify(...)` | Verifica la firma de un webhook | `bool` | ## Emision ```php use Ecuafact\Sdk\Contracts\ComprobanteRequest; use Ecuafact\Sdk\Contracts\Detalle; use Ecuafact\Sdk\Contracts\Impuesto; use Ecuafact\Sdk\Contracts\InfoDocumento; use Ecuafact\Sdk\Contracts\InfoTributaria; use Ecuafact\Sdk\Contracts\Pago; $comprobante = new ComprobanteRequest(); $comprobante->origenReferencia = 'MiERP'; $comprobante->referenciaExterna = 'FACTURA-2026-0001'; $comprobante->infoTributaria = new InfoTributaria(); $comprobante->infoTributaria->ruc = '1790012345001'; $comprobante->infoTributaria->codDoc = '01'; $comprobante->infoTributaria->estab = '002'; $comprobante->infoTributaria->ptoEmi = '001'; $comprobante->infoTributaria->secuencial = '000000123'; $totalConImpuestos = new Impuesto(); $totalConImpuestos->codigo = '2'; $totalConImpuestos->codigoPorcentaje = '4'; $totalConImpuestos->baseImponible = 100.0; $totalConImpuestos->valor = 15.0; $pago = new Pago(); $pago->formaPago = '01'; $pago->total = 115.0; $comprobante->info = new InfoDocumento(); $comprobante->info->fechaEmision = '01/01/2026'; $comprobante->info->tipoIdentificacionComprador = '04'; $comprobante->info->identificacionComprador = '1790012345001'; $comprobante->info->razonSocialComprador = 'Cliente Ejemplo'; $comprobante->info->totalSinImpuestos = 100.0; $comprobante->info->totalDescuento = 0.0; $comprobante->info->totalConImpuestos = [$totalConImpuestos]; $comprobante->info->importeTotal = 115.0; $comprobante->info->moneda = 'DOLAR'; $comprobante->info->pagos = [$pago]; $impuesto = new Impuesto(); $impuesto->codigo = '2'; $impuesto->codigoPorcentaje = '4'; $impuesto->tarifa = 15.0; $impuesto->baseImponible = 100.0; $impuesto->valor = 15.0; $detalle = new Detalle(); $detalle->codigoPrincipal = 'SERV-001'; $detalle->descripcion = 'Servicio de ejemplo'; $detalle->cantidad = 1; $detalle->precioUnitario = 100.0; $detalle->descuento = 0.0; $detalle->precioTotalSinImpuesto = 100.0; $detalle->impuestos = [$impuesto]; $comprobante->detalles = [$detalle]; $resultado = $client->emitir($comprobante); echo $resultado->admission->idOperacion . ' ' . $resultado->admission->codigo . PHP_EOL; ``` Emision con RUC explicito: `$client->emitirEn('1790012345001', $comprobante)`. ## Multi-RUC ```php $ruc = $client->para('1790099987001'); $pagina = $ruc->listarEmitidos(); ``` ## Estado y consultas ```php $operacion = $client->getOperacion('3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f'); $contexto = $client->getContexto(); $cupo = $client->getConsumo(); ``` ## Consultas SRI Consultas al SRI (contribuyentes, establecimientos, identificaciones y claves de acceso). Son de solo lectura, no consumen cupo y usan la misma API Key. ```php $contribuyente = $client->consultarContribuyente('1760013210001'); $encontrados = $client->buscarContribuyentes('PEREZ LOPEZ', 'JUAN', 'PersonaNatural', 10); $establecimientos = $client->listarEstablecimientos('1760013210001', 'SoloActivos'); $validacion = $client->validarIdentificacion('1760013210001'); $clave = $client->decodificarClaveAcceso($claveAcceso); ``` ## Idempotencia `idempotencyKey` es opcional: si no se envia, el SDK la genera, la reutiliza en los reintentos y la devuelve en `resultado->idempotencyKey` (y en el error, como `EcuafactApiException->idempotencyKey`). Al reintentar, **reutiliza la clave devuelta**. La correlacion de la respuesta queda en `resultado->correlationId` (o `EcuafactApiException->idSeguimiento` en error). Los fallos transitorios (`408/425/429/5xx`) se reintentan respetando `Retry-After`; el resto de los 4xx no. ## Webhooks ```php use Ecuafact\Sdk\WebhookSignature; $valido = WebhookSignature::verify($secreto, $cabecera, $cuerpoCrudo, time(), 300); ``` Ve [Verificacion de firma](/v1/guias/webhooks-firma) y [Webhooks](/v1/guias/webhooks). ## Errores `EcuafactApiException` trae `codigo`, `mensaje`, `estadoHttp`, `idSeguimiento`, `errores` e `idempotencyKey`. Los errores locales son `EcuafactSdkException`. ## Siguientes pasos - [Vision general de los SDK](/v1/guias/sdk): compara lenguajes y metodos. - [Autenticacion](/v1/guias/autenticacion): obten y usa tu API Key. - [Emision](/v1/guias/emision): emite tu primer comprobante. - [Verificacion de firma](/v1/guias/webhooks-firma): valida los webhooks entrantes. --- # Colecciones Importa la API en Postman (o Newman) con la coleccion lista para usar: incluye todos los endpoints, variables y ejemplos de request y respuesta. | Archivo | Contenido | |---|---| | [Ecuafact API v1 (Postman)](/downloads/v1/Ecuafact-API-v1.postman_collection.json) | Coleccion completa (importable en Postman, Newman o Bruno) | | [Environment](/downloads/v1/Ecuafact-API.postman_environment.json) | Variables `baseUrl` (pruebas), `baseUrlProduccion`, `apiKey`, `identificacion`, `ruc`, `estab`, `ptoEmi` | | [OpenAPI](/api/openapi.json) | Especificacion que alimenta la referencia | | [Catalogo de servicios](/.well-known/api-catalog) | Descubribilidad del contrato (RFC 9727): OpenAPI, documentacion y versiones | | [Versiones del contrato](/.well-known/api-versions.json) | Version vigente, fechas y retiro, apto para maquinas | | [llms.txt](/llms.txt) | Indice para asistentes | | [llms-full.txt](/llms-full.txt) | Toda la guia en un solo texto para asistentes | | [Emision para asistentes](/downloads/v1/emision-factura.md) | Contexto corto para emitir una factura | | [Ejemplos de solicitud](/downloads/v1/ejemplos/example-01.json) | `example-01.json` a `example-07.json`, uno por tipo | ## Como importar 1. En Postman: **Import** y arrastra ambos archivos. 2. Selecciona el environment **Ecuafact API - Plantilla**. 3. Completa `apiKey` y los datos de tu emisor. 4. Empieza por la carpeta de inicio, sigue con la de emision y luego con la de seguimiento. Con Newman: ```bash newman run Ecuafact-API-v1.postman_collection.json -e Ecuafact-API.postman_environment.json ``` ## Siguientes pasos - [Primeros pasos](/v1/guias/primeros-pasos) - [Emision](/v1/guias/emision) - [Datos de prueba](/v1/guias/datos-de-prueba) --- # Changelog Cambios del API y de la documentacion, con fecha. Toda guia o contrato publico que cambie se registra aqui, con el impacto y tu accion. Formato: - **Que cambio.** El cambio y el recurso afectado. - **Impacto.** Que puede romperse. - **Que hacer.** La accion concreta; si no hay que hacer nada, se dice. ## 2026-10-05 ### SDK .NET: paquetes publicados en NuGet - **Que cambio.** Los paquetes `Ecuafact.Sdk` y `Ecuafact.Sdk.Core` quedan publicados en NuGet: licencia MIT, README, icono, notas de version y `PackageProjectUrl` hacia [SDK .NET](/v1/guias/sdk-dotnet). Se agrega `scripts/Pack-Sdk.ps1` para generar los `.nupkg` y validar su metadata. - **Impacto.** Ninguno sobre el contrato del API. - **Que hacer.** Nada. Instalacion: `dotnet add package Ecuafact.Sdk`. ### SDK PHP: repositorio publico y paquete en Packagist - **Que cambio.** El SDK PHP queda en su propio repositorio publico ([Ecuanexus/ecuafact-sdk-php](https://github.com/Ecuanexus/ecuafact-sdk-php)), con licencia MIT, README publico y `composer.json` publicado en Packagist como `ecuafact/sdk`. Se aplica la misma semantica de reintentos que .NET: reintenta `429` y `503` respetando `Retry-After`, reintenta descargas transitorias y expone `EmisionResultado->correlationId`. - **Impacto.** Ninguno sobre el contrato del API. - **Que hacer.** Nada. Instalacion: `composer require ecuafact/sdk`. ### SDK Node.js y Java: publicados y con paridad de reintentos - **Que cambio.** Los SDK Node.js y Java ahora reintentan `429` respetando `Retry-After` (tambien en `503`), reintentan descargas transitorias, exponen la correlacion en `EmisionResultado` y usan la misma regla de reintento (solo `GET` o `POST/PUT` con `Idempotency-Key`). Node.js queda publicado en npm; Java queda publicado en Maven Central (compilado para **Java 17**). - **Impacto.** Ninguno sobre el contrato del API. - **Que hacer.** Nada. Instalacion: Node.js `npm install @ecuafact/sdk`; Java `com.ecuafact:sdk`. ### SDK Python: publicado en PyPI - **Que cambio.** El SDK Python (`ecuafact`) queda publicado en PyPI, con licencia MIT, README publico y la misma semantica de reintentos que los demas SDK: `429` respetando `Retry-After`, reintento en descargas y `EmisionResultado.correlation_id`. - **Impacto.** Ninguno sobre el contrato del API. - **Que hacer.** Nada. Instalacion: `pip install ecuafact`. ### SDK .NET: reintentos, correlacion y limpieza de contrato - **Que cambio.** El SDK .NET ahora reintenta `429` respetando `Retry-After` (tambien en `503`), aplica el reintento a descargas y al reemplazo de logo, expone `EmisionResultado.CorrelationId`, ya no muta `EcuafactClientOptions`, y **retira** `ListarRecibidosAsync` (el listado de recibidos no forma parte del contrato publico). Guia: [SDK .NET](/v1/guias/sdk-dotnet). - **Impacto.** Si usabas `ListarRecibidosAsync`, deja de estar disponible; ante `429` el cliente espera y reintenta en lugar de fallar de inmediato. - **Que hacer.** Sustituye `ListarRecibidosAsync` por `ListarEmitidosAsync`; si necesitas el identificador de soporte, guarda `EmisionResultado.CorrelationId`. ## 2026-10-03 ### Onboarding autoservicio: sandbox de pruebas - **Que cambio.** Nuevo alta autoservicio de **sandbox de pruebas** en el Portal del Cliente (`https://staging-portal.mynexusapi.com/sandbox`), en un wizard de 4 pasos (emisor con tu RUC, certificado digital obligatorio, cuenta y confirmacion). Crea un emisor de pruebas con tu RUC y tu certificado, una API Key y el plan **"Sandbox MyNexusApi"** (5.000 documentos, 2 meses), asignado automaticamente. Se registra la **IP** de la solicitud y, al confirmar, se envia un **correo** con la API Key, la URL del API de pruebas, la documentacion y el **usuario y clave autogenerada** del Portal del Cliente. Solo existe en el ambiente de pruebas. Detalle en [Primeros pasos](/v1/guias/primeros-pasos). - **Impacto.** Ninguno sobre el contrato del API. No habilita produccion; el alta de produccion sigue siendo asistida. - **Que hacer.** Nada. ### Contrato: version por fecha y cabecera X-Api-Version - **Que cambio.** La API versiona el contrato por fecha y devuelve la cabecera `X-Api-Version` en toda respuesta a `/v1`. La cabecera es opcional en la solicitud; una version inexistente o retirada responde `400` con `codigo` `306`. - **Impacto.** Aditivo y compatible: si no envias `X-Api-Version`, nada cambia. - **Que hacer.** Nada. Opcional: fija la version y lee la resuelta. Detalle en [Versionado](/v1/guias/versionado). ### Descubribilidad: OpenAPI, catalogo de servicios y calendario de versiones - **Que cambio.** Nuevos recursos `/.well-known/api-catalog` (RFC 9727) y `/.well-known/api-versions.json`, mas el encabezado `Link: rel="api-catalog"`. El documento OpenAPI lleva titulo, version por fecha, contacto y licencia. - **Impacto.** Ninguno sobre el contrato; son recursos nuevos. - **Que hacer.** Nada. Enlazables desde [Colecciones](/v1/guias/colecciones). ### Documentacion para agentes: metadatos citables - **Que cambio.** Cada guia expone `source_url` y `last_updated` en su version `.md`, en [llms.txt](/llms.txt) y en [llms-full.txt](/llms-full.txt). Se amplian las reglas de citacion en [Instrucciones para agentes](/v1/guias/agentes). - **Impacto.** Ninguno sobre el contrato. - **Que hacer.** Nada. ## 2026-10-02 ### Documentacion y guias - **Que cambio.** Nuevas guias: [Datos de prueba](/v1/guias/datos-de-prueba), [Instrucciones para agentes](/v1/guias/agentes) y este changelog. Se agrega el vector de prueba de firma en [Verificacion de firma](/v1/guias/webhooks-firma), el guardarrail de terminos en [Glosario](/v1/guias/glosario) y la [Consola de prueba](/v1/consola) ahora permite emitir una factura de prueba. - **Impacto.** Ninguno sobre el contrato. - **Que hacer.** Nada. ### Errores del API: codigos de negocio de base de datos - **Que cambio.** Varios errores de negocio que se reportaban genericos como `503` (`database_unavailable`) ahora devuelven su codigo correcto: duplicado (`409`/`401`), cupo (`429`/`501`), no encontrado (`404`/`502`), validacion (`400`/`301`) y conflicto de idempotencia (`409`/`403`). Sin cambio en el contrato de los codigos ya documentados. - **Impacto.** Si tu integracion trataba esos casos como "servicio no disponible", ahora recibe el codigo especifico del catalogo. - **Que hacer.** Usa el codigo devuelto para decidir reintento o correccion. Ver [Errores](/v1/guias/errores). ### Normalizacion editorial de la documentacion - **Que cambio.** Todas las guias se alinearon con `docs/ESCRITURA-DOCUMENTACION.md`: **lead** de 1-2 frases, **rotulo de ambiente de pruebas** en la emision y cierre **"Siguientes pasos"** en cada pagina. `primeros-pasos` queda como el quickstart y `datos-de-prueba` como referencia. - **Impacto.** Ninguno sobre el contrato. - **Que hacer.** Nada. ### Servidor MCP publicado - **Que cambio.** El servidor MCP quedo desplegado y disponible por HTTPS: `https://mcp.mynexusapi.com/mcp` (produccion) y `https://mcp-staging.mynexusapi.com/mcp` (pruebas), ademas del host interno de desarrollo. Se autentica con `X-Api-Key`. - **Impacto.** Ninguno sobre el API REST. - **Que hacer.** Nada. Detalle en [MCPs y asistentes](/v1/guias/mcp). ### Skill y MCP para asistentes de IA - **Que cambio.** Nuevo servidor MCP (Streamable HTTP) en `{BaseUrl}/mcp` autenticado con `X-Api-Key`; nueva guia [MCPs y asistentes](/v1/guias/mcp) y [skill para asistentes](/v1/guias/skill) (paquete `.zip`). `llms.txt` ahora enlaza la version `.md` de cada guia con una descripcion. - **Impacto.** Ninguno sobre el API REST. - **Que hacer.** Nada. ### Webhooks: nuevo `document.failed` - **Que cambio.** Nuevo evento `document.failed` (con `motivo`: `estructura_invalida`, `entrega_no_posible` o `resultado_desconocido`). Se retira `document.fiscal_status_changed`. - **Impacto.** Si escuchabas `document.fiscal_status_changed`, deja de llegar. - **Que hacer.** Trata `document.authorized`, `document.rejected` y `document.failed`; actualiza tu handler. Detalle en [Webhooks](/v1/guias/webhooks). ### Limite de emision: 2 MiB - **Que cambio.** La emision acepta cuerpos de hasta **2 MiB**; por encima responde `413` (`codigo` `305`). - **Impacto.** Los comprobantes mayores al limite se rechazan. - **Que hacer.** Reduce el comprobante. Detalle en [Limites](/v1/guias/limites). ### Correo a varios destinatarios - **Que cambio.** `POST /v1/contribuyentes/{identificacion}/comprobantes/{claveAcceso}/correo` acepta varios destinatarios separados por comas en `destinatario`. - **Impacto.** Ninguno; el campo sigue aceptando un solo correo. - **Que hacer.** Nada. Detalle en [Reenviar correo](/v1/guias/reenviar-correo). ### Limpieza de contrato - **Que cambio.** Se retira de la documentacion el listado de comprobantes recibidos. El campo `puedeRecibir` del contexto se mantiene. - **Impacto.** El endpoint ya no es parte del contrato publico. - **Que hacer.** Usa [Comprobantes emitidos](/v1/guias/consulta-emitidos). ## Como se gestiona - Cada entrada lleva fecha (`aaaa-mm-dd`). - Un cambio incompatible se marca como **incompatible** y trae la guia de migracion. - Los cambios de contrato se anuncian con antelacion y se listan aqui al publicarse. ## Siguientes pasos - [Introduccion](/v1/guias/introduccion) - [Errores](/v1/guias/errores) - [Webhooks](/v1/guias/webhooks) --- # MCP y asistentes El **servidor MCP** de Ecuafact expone esta API a asistentes compatibles con Model Context Protocol (Claude Desktop, Cursor, VS Code). El asistente puede consultar contexto, emitir comprobantes y reenviar correos en tu nombre usando la misma **API Key** y los mismos permisos que ya tienes. El servidor no almacena credenciales: reenvia la API Key que le entregas en cada peticion y aplica la autorizacion del API. ## Endpoint El servidor usa **Streamable HTTP** (remoto). Cada peticion incluye tu **API Key** en la cabecera `X-Api-Key`; sin ella responde `401`. | Ambiente | Endpoint | |---|---| | Pruebas | `https://mcp-staging.mynexusapi.com/mcp` | | Produccion | `https://mcp.mynexusapi.com/mcp` | ## Configuracion del cliente Registra el servidor con el tipo `http` y tu API Key. Usa el endpoint de produccion `https://mcp.mynexusapi.com/mcp`; para pruebas, `https://mcp-staging.mynexusapi.com/mcp`. ```json { "mcpServers": { "ecuafact": { "type": "http", "url": "https://mcp.mynexusapi.com/mcp", "headers": { "X-Api-Key": "tu-api-key" } } } } ``` En VS Code el archivo es `.vscode/mcp.json` y la clave raiz es `servers`: ```json { "servers": { "ecuafact": { "type": "http", "url": "https://mcp.mynexusapi.com/mcp", "headers": { "X-Api-Key": "tu-api-key" } } } } ``` ## Herramientas Lectura: | Herramienta | Para que sirve | |---|---| | `obtener_contexto` | Cliente y contribuyentes habilitados | | `obtener_consumo` | Cupo disponible y paquetes | | `consultar_operacion` | Estado de una emision por `idOperacion` | | `listar_emitidos` | Comprobantes emitidos (paginado y con filtros) | | `descargar_archivo` | RIDE (PDF) o XML de un comprobante | | `validar_identificacion` | Formato de cedula, RUC o pasaporte | | `consultar_contribuyente` | Catastro del SRI por identificacion | | `buscar_contribuyentes` | Catastro del SRI por nombre | | `listar_establecimientos` | Establecimientos de un RUC | | `decodificar_clave_acceso` | Segmentos de una clave de acceso | | `leer_catalogo` | Catalogos del SRI | Escritura: | Herramienta | Para que sirve | |---|---| | `emitir_comprobante` | Emite un comprobante | | `reenviar_correo` | Reenvia el PDF y el XML por correo | ## Emision desde un asistente `emitir_comprobante` exige `confirmar=true` y una `idempotencyKey` estable (reutilizala en reintentos). El cuerpo es la **Estructura del comprobante**; cada tipo tiene su pagina: | Tipo | Guia | |---|---| | Factura | [emision-01](/v1/guias/emision-01) | | Nota de credito | [emision-04](/v1/guias/emision-04) | | Comprobante de retencion | [emision-07](/v1/guias/emision-07) | Las reglas de emision son las mismas de la API: revisa [Primeros pasos](/v1/guias/primeros-pasos) y [Errores](/v1/guias/errores). ## Skill Si tu asistente integra el API sin el MCP, descarga el [skill para asistentes de IA](/v1/guias/skill). ## Seguridad - Usa una API Key con el alcance minimo necesario. - El servidor reenvia la clave por peticion; no la guarda ni la registra en logs. - Valida el header `Origin` y restringe los hosts permitidos en el servidor. - Para pruebas, apunta `Ecuafact:BaseUrl` al ambiente de pruebas. ## Siguientes pasos - [Skill para asistentes](/v1/guias/skill) - [Instrucciones para agentes](/v1/guias/agentes) - [Primeros pasos](/v1/guias/primeros-pasos) --- # Skill para asistentes de IA El **skill** de Ecuafact es un paquete de conocimiento para asistentes de IA (Claude Code, Cursor, GitHub Copilot y cualquier agente compatible con el estandar **Agent Skills**). Contiene un `SKILL.md` con el flujo de integracion y recursos de apoyo que el asistente lee solo cuando los necesita. No es exclusivo de un proveedor: es markdown portable. Funciona en cualquier agente que cargue skills del estandar; el asistente decide cuando consultar cada recurso. ## Descarga [Descargar el paquete (.zip)](/downloads/v1/skills/ecuafact.zip) El paquete incluye: | Archivo | Contenido | |---|---| | `SKILL.md` | Ambientes, autenticacion, ciclo de vida, emision, endpoints y mapa de recursos | | `reference/codigos-sri.md` | Tipos de identificacion, IVA, formas de pago y retenciones | | `reference/emision/*.md` | Campos y ejemplo por tipo (factura, liquidacion, NC, ND, guia, retencion) | | `reference/consultas.md` | Operacion, emitidos, cupo, catalogos, RIDE/XML, correo, perfil | | `reference/consultas-sri.md` | Contribuyente, establecimientos, validar identificacion, decodificar clave | | `reference/webhooks.md` | Entrega, firma y verificacion de webhooks | | `reference/errores-limites.md` | Catalogo de errores, limites y soporte | ## Instalacion Descomprime el paquete en la carpeta de skills de tu asistente. - **Claude Code:** `.claude/skills/ecuafact/SKILL.md` (con su carpeta `reference/`). - **Cursor / VS Code Copilot:** usa la carpeta de skills que indique tu herramienta y coloca ahi `SKILL.md` + `reference/`. - **Otros agentes:** cualquier convencion que cargue `SKILL.md` con su carpeta de recursos sirve. ## Documentacion actualizada Si tu asistente tiene acceso a internet, puede traer la documentacion vigente sin re-descargar el skill: - [llms.txt](/llms.txt) - indice de la documentacion para LLM. - [llms-full.txt](/llms-full.txt) - toda la documentacion en un archivo. - Markdown por pagina: agrega `.md` a cualquier guia (p. ej. `/v1/guias/emision-01.md`). ## Relacion con el MCP El skill es **conocimiento** (como integrar la API). El [servidor MCP](/v1/guias/mcp) es **ejecucion** (herramientas que el asistente invoca en vivo). Puedes usar ambos: el skill orienta al agente y el MCP ejecuta las llamadas. ## Siguientes pasos - [MCP y asistentes](/v1/guias/mcp) - [Instrucciones para agentes](/v1/guias/agentes) - [Primeros pasos](/v1/guias/primeros-pasos) --- # Instrucciones para agentes de IA Guia para asistentes (Claude, ChatGPT, Copilot, Cursor y agentes propios) que integran o consumen esta API. Describe como obtener la documentacion vigente, como ejecutar acciones y que reglas seguir. ## Documentacion para agentes - [llms.txt](/llms.txt): indice con la version `.md` de cada guia y una descripcion por entrada. - [llms-full.txt](/llms-full.txt): toda la documentacion en un archivo. - Markdown por pagina: agrega `.md` a cualquier guia (`/v1/guias/emision-01.md`) o envia `Accept: text/markdown`. - [sitemap.xml](/sitemap.xml): URLs de todas las guias. - [OpenAPI](/api/openapi.json): el contrato de la API en formato maquina. - [Catalogo de servicios](/.well-known/api-catalog): donde estan el OpenAPI, la documentacion y el calendario de versiones (RFC 9727). - [Versiones del contrato](/.well-known/api-versions.json): version vigente, fechas y retiro. - [Versionado](/v1/guias/versionado): politica y cabecera `X-Api-Version`. Cuando necesites el detalle actualizado, reconsulta la version `.md` de la guia; no dependas de una copia cacheada. ## Acciones (MCP) - Endpoint: `{BaseUrl}/mcp` (Streamable HTTP). - Autenticacion: cabecera `X-Api-Key`; reenviala por peticion, nunca la guardes. - Herramientas de lectura y escritura; la emision exige confirmacion e `Idempotency-Key`. - Detalle en [MCPs y asistentes](/v1/guias/mcp). ## Skill Paquete de conocimiento descargable (SKILL.md + recursos) para cargar en el asistente. Detalle en [Skill para asistentes de IA](/v1/guias/skill). ## Reglas para agentes 1. **No inventes contrato.** Endpoints, campos y codigos salen de esta documentacion; ante duda, lee la guia `.md` o usa las herramientas de consulta. 2. **Cita la fuente canonica.** Usa la URL canonica de la guia (`https://docsapi.ecuafact.com/v1/guias/{slug}`) y, si tu respuesta depende de un detalle fiscal o de contrato, la fecha `last_updated` de esa guia. 3. **El sandbox no tiene validez fiscal.** Rotula cualquier ejemplo de pruebas. 4. **Confirma antes de emitir.** Pide confirmacion explicita al usuario y reutiliza la misma `Idempotency-Key` en reintentos. 5. **No expongas secretos.** No imprimas ni registres la API Key. 6. **Deduplica webhooks** por `eventType` + `resourceId`; no asumas orden. 7. **Distingue rechazo de notificacion**; no reintentes un `4xx` sin corregir. 8. **Respeta `Retry-After`** en `429`/`503`. ## Metadatos Cada guia expone dos campos citables: - `source_url`: la URL canonica de la guia (`.../v1/guias/{slug}`). - `last_updated`: la fecha (aaaa-mm-dd) del ultimo cambio de esa guia. Aparecen en el encabezado de la version `.md` de cada guia, en la entrada correspondiente de [llms.txt](/llms.txt) y junto a cada seccion de [llms-full.txt](/llms-full.txt). Cita la URL canonica siempre; agrega la fecha cuando tu respuesta dependa de un detalle fiscal o de contrato. La version del contrato (`X-Api-Version`) y su calendario estan en [Versionado](/v1/guias/versionado). ## Siguientes pasos - [MCPs y asistentes](/v1/guias/mcp) - [Skill para asistentes de IA](/v1/guias/skill) - [Introduccion](/v1/guias/introduccion) --- # Respuestas del API Forma comun de las respuestas del API. Aqui ves como leer el cuerpo segun el endpoint, la correlacion y como se reportan los errores. ## Forma de la respuesta Dependiendo del endpoint, la respuesta llega de una de estas dos formas: | Forma | Endpoints | Como se lee | |---|---|---| | **Plana** | Emision (`POST /v1/contribuyentes/{identificacion}/comprobantes`), cupo (`GET /v1/consumo`) y consultas al SRI (`GET /v1/consultas/...`) | Los datos estan en el primer nivel del JSON | | **Con `datos`** | Contexto, listados, estado de operacion, perfil del emisor, reenvio de correo y catalogos | Los datos vienen en la clave `datos` | ## Cuando emites: la solicitud Al emitir, la API responde `202 Accepted` con un objeto como este: ```json { "codigo": "200", "mensaje": "Operacion exitosa.", "idOperacion": "3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "urlEstado": "https://staging-api.mynexusapi.com/v1/operaciones/3f1c8c1e-6f2a-4b7e-9c3d-2a1b0c9d8e7f", "uid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ``` | Campo | Significado | |---|---| | `codigo` | `200` si es una emision nueva; `201` si es un reintento con la misma `Idempotency-Key` | | `mensaje` | Descripcion del resultado | | `idOperacion` | Identificador de seguimiento de la emision (guardalo) | | `urlEstado` | Ruta para consultar el estado de esta operacion | | `uid` | Identificador del documento (util para soporte) | `202` significa que la solicitud fue **recibida**; todavia no es la autorizacion del SRI. El resultado final se obtiene consultando `urlEstado` o esperando el [webhook](/v1/guias/webhooks). ## Respuestas de error ```json { "codigo": "301", "mensaje": "Solicitud invalida.", "errores": ["Idempotency-Key: obligatoria."] } ``` El detalle de los codigos esta en [Errores](/v1/guias/errores). ## Correlacion Toda respuesta incluye la cabecera `X-Correlation-Id`. Guardala al reportar un incidente. Las respuestas a `/v1` incluyen ademas las cabeceras de limite (`X-RateLimit-Limit`, `X-RateLimit-Remaining` y, al superarlo, `X-RateLimit-Reset` y `Retry-After`); ve [Limite de request](/v1/guias/limite-de-request). ## Version del contrato Toda respuesta a `/v1` trae la cabecera `X-Api-Version` con la version del contrato que respondio. Puedes fijar la version en la solicitud con la misma cabecera (opcional): si la omites, se usa la vigente. Una version inexistente o retirada responde `400` con `codigo` `306`. Ve [Versionado](/v1/guias/versionado). ## Siguientes pasos - [Webhooks](/v1/guias/webhooks) - [Errores](/v1/guias/errores) - [Emision](/v1/guias/emision) --- # Errores Cuando una peticion no se puede procesar, la API responde con un JSON como este: ```json { "codigo": "301", "mensaje": "Solicitud invalida.", "errores": ["Idempotency-Key: obligatoria."] } ``` - `codigo`: codigo del catalogo (ver abajo). - `mensaje`: descripcion breve del codigo. - `errores`: lista de mensajes. Aparece **solo** en errores de validacion; en el resto se omite. Cada respuesta incluye la cabecera `X-Correlation-Id`. Guardala: ayuda a soporte a rastrear el caso. ## Catalogo de codigos | Codigo | HTTP | Significado | Que hacer | Guia | |---|---|---|---|---| | `001` | 500 | Error interno | Reintenta; si persiste, reporta el `X-Correlation-Id` | [Soporte](#como-pedir-soporte) | | `002` | 503 | Servicio temporalmente no disponible | Reintenta con espera | [Reintentos](/v1/guias/buenas-practicas) | | `003` | 502 | El SRI reporto un error | Reintenta; si persiste, reporta | [Seguimiento](/v1/guias/consulta-operacion) | | `004` | 504 | El SRI no respondio a tiempo | Reintenta | [Seguimiento](/v1/guias/consulta-operacion) | | `005` | 501 | Funcionalidad no implementada | No reintentar; consulta el estado de la operacion | [Seguimiento](/v1/guias/consulta-operacion) | | `101` | 401 | API Key invalida o no autorizada para la IP | Revisa la API Key y la IP registrada | [Autenticacion](/v1/guias/autenticacion) | | `102` | 403 | Origen de la solicitud no autorizado | Revisa la IP registrada | [Autenticacion](/v1/guias/autenticacion) | | `103` | 403 | Contribuyente, tipo o plan no habilitados | Habilita el contribuyente o el servicio | [Ambientes](/v1/guias/ambientes) | | `104` | 429 | Limite de solicitudes excedido | Espera el tiempo de `Retry-After` y reintenta | [Limite de request](/v1/guias/limite-de-request) | | `200` | 200 / 202 | Operacion exitosa | Continua con el seguimiento | [Seguimiento](/v1/guias/consulta-operacion) | | `201` | 202 | Reintento idempotente: misma intencion, mismo resultado | Continua con el seguimiento | [Buenas practicas](/v1/guias/buenas-practicas) | | `301` | 400 | Solicitud invalida (formato, cabeceras o campos) | Revisa `errores` y corrige la solicitud | [Primeros pasos](/v1/guias/primeros-pasos) | | `302` | 422 | Comprobante invalido (regla fiscal o estructura) | Revisa `errores` y corrige el comprobante | [Emision](/v1/guias/emision) | | `303` | 400 | Filtros invalidos en una consulta | Revisa los filtros | [Comprobantes emitidos](/v1/guias/consulta-emitidos) | | `305` | 413 | El cuerpo excede el limite (2 MiB en emision) | Reduce el tamano del comprobante | [Limites](/v1/guias/limites) | | `306` | 400 | Version de API no admitida | Envia `X-Api-Version` con la version vigente (u omitela) | [Versionado](/v1/guias/versionado) | | `401` | 409 | Comprobante duplicado | No reenvies; consulta la operacion existente | [Buenas practicas](/v1/guias/buenas-practicas) | | `403` | 409 | Conflicto de idempotencia o de referencia | Usa la misma `Idempotency-Key` y el mismo contenido | [Buenas practicas](/v1/guias/buenas-practicas) | | `501` | 429 | Cupo agotado | Consulta tu cupo o amplialo | [Consumo y cuotas](/v1/guias/consumo) | | `502` | 404 | Recurso no encontrado | Revisa el identificador | [Seguimiento](/v1/guias/consulta-operacion) | | `503` | 409 | Conciliacion requerida | Consulta el estado antes de reenviar | [Seguimiento](/v1/guias/consulta-operacion) | ## Como interpretar un error - **`4xx` (400-499):** la solicitud tiene un problema. Corrigela antes de reintentar. Reintentar igual no cambia el resultado. - **`5xx` (500-599):** la plataforma o una dependencia fallo. Reintenta con la misma `Idempotency-Key`. - **`409` (duplicado / conflicto):** no es un fallo de red. Consulta la operacion existente antes de volver a enviar. ## Como pedir soporte Abre el ticket con estos cinco datos; con ellos se ubica el caso sin mas ida y vuelta: - La `identificacion` del emisor. - La URL completa de la peticion (la URL indica el ambiente). - El `X-Correlation-Id` de la respuesta. - El `codigo` y el `mensaje` del error. - El cuerpo que enviaste, sin la API Key. No se requieren capturas. El `X-Correlation-Id` alcanza para encontrar el log. ## Siguientes pasos - [Limites](/v1/guias/limites) - [Primeros pasos](/v1/guias/primeros-pasos) - [Emision](/v1/guias/emision) --- # Catalogos Tablas de referencia del SRI usadas por la API, agrupadas por donde se aplican. El mismo contenido, en JSON, esta en [Catalogos](/v1/guias/consulta-catalogos). ## Emision ### Tipos de comprobante Codigos de `infoTributaria.codDoc`. | codDoc | Comprobante | Soportado | |---|---|---| | `01` | Factura | Si | | `03` | Liquidacion de compra | Si | | `04` | Nota de credito | Si | | `05` | Nota de debito | Si | | `06` | Guia de remision | Si | | `07` | Comprobante de retencion | Si | ### Tipos de identificacion Codigos de `tipoIdentificacionComprador`, `tipoIdentificacionProveedor`, `tipoIdentificacionTransportista` y `tipoIdentificacionSujetoRetenido`. | Codigo | Tipo | |---|---| | `04` | RUC | | `05` | Cedula | | `06` | Pasaporte | | `07` | Venta a consumidor final | | `08` | Identificacion del exterior | Notas: consumidor final usa `9999999999999`; la liquidacion no recibe consumidor final; notas de credito/debito y retencion requieren identificar al receptor. ### Impuestos Codigo de impuesto (`impuestos[].codigo`, `totalConImpuestos[].codigo`): | Codigo | Impuesto | Soportado | |---|---|---| | `2` | IVA | Si | | `3` | ICE | Si (porcentual) | | `5` | IRBPNR | No soportado | IVA (`codigoPorcentaje`): | Tarifa | codigoPorcentaje | |---|---| | 0% | `0` | | 12% | `2` | | 14% | `3` | | 15% (general vigente) | `4` | | 5% | `5` | | No objeto de impuesto | `6` | | Exento de IVA | `7` | | IVA diferenciado | `8` | | 13% | `10` | ICE: `codigoPorcentaje` segun la Tabla 18 (ej. `3011`, `3021`, `3053`, `3081`, `3101`, `3491`, `3720`-`3726`). ISD: `4580` (5%). La `tarifa` de `totalConImpuestos`, si llega, debe coincidir con la del detalle. ### Formas de pago Codigos de `pagos[].formaPago`: | Codigo | Forma de pago | |---|---| | `01` | Sin utilizacion del sistema financiero | | `15` | Compensacion de deudas | | `16` | Tarjeta de debito | | `17` | Dinero electronico | | `18` | Tarjeta prepago | | `19` | Tarjeta de credito | | `20` | Otros con utilizacion del sistema financiero | | `21` | Endoso de titulos | ### Retenciones y sustento IVA retenido (`retenciones[].codigoRetencion`; `codigo` = `2`): | Retencion | Codigo | Porcentaje | |---|---|---| | IVA 10% | `9` | 10 | | IVA 20% | `10` | 20 | | IVA 30% | `1` | 30 | | IVA 50% | `11` | 50 | | IVA 70% | `2` | 70 | | IVA 100% | `3` | 100 | Renta (`codigoRetencion`), principales: `303` honorarios, `312` mano de obra, `320` compras de bienes, `322`/`323` servicios, `324`/`325` entre sociedades, `327A` arrendamiento, `329` seguros, `332D`/`332E` transporte privado, `344A` otras, `346` estandarizado. Sustento: | Campo | Regla | |---|---| | `codDocSustento` | Tipo del documento (el `41` se rechaza) | | `codSustento` | Codigo de sustento (el `10` se rechaza) | | `numDocSustento` | 15 digitos, sin guiones | | `numAutDocSustento` | Sustento electronico de 49 digitos | ## Estados ### Estado de la operacion Valores de `GET /v1/operaciones/{id}` en `estado`. | Estado | Significado | |---|---| | `en_cola` | Registrada, aun no enviada | | `enviando` | En despacho | | `enviado` | Enviado para procesamiento fiscal | | `resultado_desconocido` | El resultado aun no se confirma | | `cancelado` | Operacion cancelada | | `desconocido` | Estado no reconocido | ### Estado de autorizacion Valores de `estadoAutorizacion` en listados y operacion. | Valor | Significado | |---|---| | `no_disponible` | Aun sin resultado fiscal | | `pendiente` | En procesamiento por el SRI | | `autorizado` | El SRI autorizo el comprobante | | `error` | El SRI rechazo el comprobante | | `desconocido` | Estado no reconocido | ### Estado SRI de procesamiento Siglas del SRI; la API las resume en `estadoAutorizacion`. | Sigla | Significado | |---|---| | `PPR` | En procesamiento | | `AUT` | Autorizado | | `NAT` | No autorizado | > Un codigo de procesamiento intermedio del SRI (`PPR`) no significa que el > comprobante ya este `autorizado`; espera a `AUT`. ## Otras tablas ### Tipo de emision y ambiente `tipoEmision` siempre es `1` (emision normal). Usa la URL de pruebas para ensayar y la de produccion para emitir con validez fiscal; ve [Ambientes](/v1/guias/ambientes). | Ambiente | Codigo SRI | |---|---| | Pruebas | 1 | | Produccion | 2 | ### Unidades de tiempo `pagos[].unidadTiempo` es obligatoria cuando `plazo > 0`. Valores habituales: `dias`, `meses`, `anios`. ### Monedas `info.moneda` usa el codigo de la moneda (por ejemplo `DOLAR`). ### Paises (comercio exterior) `paisOrigen`, `paisDestino`, `paisAdquisicion` y `codPaisPagoProveedorReembolso` usan el codigo numerico de tres digitos del catalogo de paises del SRI. Ecuador es `593`. Fuente: SRI, catalogo de paises. | Codigo | Descripcion | |---|---| | 016 | AMERICAN SAMOA | | 074 | BOUVET ISLAND | | 101 | ARGENTINA | | 102 | BOLIVIA | | 103 | BRASIL | | 104 | CANADA | | 105 | COLOMBIA | | 106 | COSTA RICA | | 107 | CUBA | | 108 | CHILE | | 109 | ANGUILA | | 110 | ESTADOS UNIDOS | | 111 | GUATEMALA | | 112 | HAITI | | 113 | HONDURAS | | 114 | JAMAICA | | 115 | MALVINAS ISLAS | | 116 | MEXICO | | 117 | NICARAGUA | | 118 | PANAMA | | 119 | PARAGUAY | | 120 | PERU | | 121 | PUERTO RICO | | 122 | REPUBLICA DOMINICANA | | 123 | EL SALVADOR | | 124 | TRINIDAD Y TOBAGO | | 125 | URUGUAY | | 126 | VENEZUELA | | 127 | CURAZAO | | 129 | BAHAMAS | | 130 | BARBADOS | | 131 | GRANADA | | 132 | GUYANA | | 133 | SURINAM | | 134 | ANTIGUA Y BARBUDA | | 135 | BELICE | | 136 | DOMINICA | | 137 | SAN CRISTOBAL Y NEVIS | | 138 | SANTA LUCIA | | 139 | SAN VICENTE Y LAS GRANAD. | | 140 | ANTILLAS HOLANDESAS | | 141 | ARUBA | | 142 | BERMUDA | | 143 | GUADALUPE | | 144 | GUYANA FRANCESA | | 145 | ISLAS CAIMAN | | 146 | ISLAS VIRGENES (BRITANICAS) | | 147 | JOHNSTON ISLA | | 148 | MARTINICA | | 149 | MONTSERRAT ISLA | | 151 | TURCAS Y CAICOS ISLAS | | 152 | VIRGENES, ISLAS (NORT.AMER.) | | 201 | ALBANIA | | 202 | ALEMANIA | | 203 | AUSTRIA | | 204 | BELGICA | | 205 | BULGARIA | | 207 | ALBORAN Y PEREJIL | | 208 | DINAMARCA | | 209 | ESPANA | | 211 | FRANCIA | | 212 | FINLANDIA | | 213 | REINO UNIDO | | 214 | GRECIA | | 215 | PAISES BAJOS (HOLANDA) | | 216 | HUNGRIA | | 217 | IRLANDA | | 218 | ISLANDIA | | 219 | ITALIA | | 220 | LUXEMBURGO | | 221 | MALTA | | 222 | NORUEGA | | 223 | POLONIA | | 224 | PORTUGAL | | 225 | RUMANIA | | 226 | SUECIA | | 227 | SUIZA | | 228 | CANARIAS ISLAS | | 229 | UCRANIA | | 230 | RUSIA | | 231 | YUGOSLAVIA | | 233 | ANDORRA | | 234 | LIECHTENSTEIN | | 235 | MONACO | | 237 | SAN MARINO | | 238 | VATICANO (SANTA SEDE) | | 239 | GIBRALTAR | | 241 | BELARUS | | 242 | BOSNIA Y HERZEGOVINA | | 243 | CROACIA | | 244 | ESLOVENIA | | 245 | ESTONIA | | 246 | GEORGIA | | 247 | GROENLANDIA | | 248 | LETONIA | | 249 | LITUANIA | | 250 | MOLDOVA | | 251 | MACEDONIA | | 252 | ESLOVAQUIA | | 253 | ISLAS FAROE | | 260 | FRENCH SOUTHERN TERRITORIES | | 301 | AFGANISTAN | | 302 | ARABIA SAUDITA | | 303 | MYANMAR (BURMA) | | 304 | CAMBOYA | | 306 | COREA NORTE | | 307 | TAIWAN (CHINA) | | 308 | FILIPINAS | | 309 | INDIA | | 310 | INDONESIA | | 311 | IRAK | | 312 | IRAN (REPUBLICA ISLAMICA) | | 313 | ISRAEL | | 314 | JAPON | | 315 | JORDANIA | | 316 | KUWAIT | | 317 | LAOS, REP. POP. DEMOC. | | 318 | LIBANO | | 319 | MALASIA | | 321 | MONGOLIA (MANCHURIA) | | 322 | PAKISTAN | | 323 | SIRIA | | 325 | TAILANDIA | | 327 | BAHREIN | | 328 | BANGLADESH | | 329 | BUTAN | | 330 | COREA DEL SUR | | 331 | CHINA POPULAR | | 332 | CHIPRE | | 333 | EMIRATOS ARABES UNIDOS | | 334 | QATAR | | 335 | MALDIVAS | | 336 | NEPAL | | 337 | OMAN | | 338 | SINGAPUR | | 339 | SRI LANKA (CEILAN) | | 341 | VIETNAM | | 342 | YEMEN | | 343 | ISLAS HEARD Y MCDONALD | | 344 | BRUNEI DARUSSALAM | | 346 | TURQUIA | | 347 | AZERBAIJAN | | 348 | KAZAJSTAN | | 349 | KIRGUIZISTAN | | 350 | TAJIKISTAN | | 351 | TURKMENISTAN | | 352 | UZBEKISTAN | | 353 | PALESTINA | | 354 | HONG KONG | | 355 | MACAO | | 356 | ARMENIA | | 382 | MONTENEGRO | | 402 | BURKINA FASO | | 403 | ARGELIA | | 404 | BURUNDI | | 405 | CAMERUN | | 406 | CONGO | | 407 | ETIOPIA | | 408 | GAMBIA | | 409 | GUINEA | | 410 | LIBERIA | | 412 | MADAGASCAR | | 413 | MALAWI | | 414 | MALI | | 415 | MARRUECOS | | 416 | MAURITANIA | | 417 | NIGERIA | | 419 | ZIMBABWE (RHODESIA) | | 420 | SENEGAL | | 421 | SUDAN | | 422 | SUDAFRICA (CISKEI) | | 423 | SIERRA LEONA | | 425 | TANZANIA | | 426 | UGANDA | | 427 | ZAMBIA | | 429 | BENIN | | 430 | BOTSWANA | | 431 | REPUBLICA CENTROAFRICANA | | 432 | COSTA DE MARFIL | | 433 | CHAD | | 434 | EGIPTO | | 435 | GABON | | 436 | GHANA | | 437 | GUINEA-BISSAU | | 438 | GUINEA ECUATORIAL | | 439 | KENIA | | 440 | LESOTHO | | 441 | MAURICIO | | 442 | MOZAMBIQUE | | 443 | MAYOTTE | | 444 | NIGER | | 445 | RWANDA | | 446 | SEYCHELLES | | 447 | SAHARA OCCIDENTAL | | 448 | SOMALIA | | 449 | SANTO TOME Y PRINCIPE | | 450 | SWAZILANDIA | | 451 | TOGO | | 452 | TUNEZ | | 453 | ZAIRE | | 454 | ANGOLA | | 456 | CABO VERDE | | 458 | COMORAS | | 459 | DJIBOUTI | | 460 | NAMIBIA | | 463 | ERITREA | | 464 | MOROCCO | | 465 | REUNION | | 466 | SANTA ELENA | | 499 | JERSEY | | 501 | AUSTRALIA | | 503 | NUEVA ZELANDA | | 504 | SAMOA OCCIDENTAL | | 506 | FIJI | | 507 | PAPUA NUEVA GUINEA | | 508 | TONGA | | 509 | PALAO (BELAU) ISLAS | | 510 | KIRIBATI | | 511 | MARSHALL ISLAS | | 512 | MICRONESIA | | 513 | NAURU | | 514 | SALOMON ISLAS | | 515 | TUVALU | | 516 | VANUATU | | 517 | GUAM | | 518 | ISLAS COCOS (KEELING) | | 519 | ISLAS COOK | | 520 | ISLAS NAVIDAD | | 521 | MIDWAY ISLAS | | 522 | NIUE ISLA | | 523 | NORFOLK ISLA | | 524 | NUEVA CALEDONIA | | 525 | PITCAIRN, ISLA | | 526 | POLINESIA FRANCESA | | 529 | TIMOR DEL ESTE | | 530 | TOKELAI | | 531 | WAKE ISLA | | 532 | WALLIS Y FUTUNA, ISLAS | | 590 | SAINT BARTHELEMY | | 593 | ECUADOR | | 594 | AGUAS INTERNACIONALES | | 595 | ALTO VOLTA | | 596 | BIELORRUSIA | | 597 | COTE D'IVOIRE | | 598 | CYPRUS | | 599 | REPUBLICA CHECA | | 600 | FALKLAND ISLANDS | | 601 | LATVIA | | 602 | LIBIA | | 603 | NORTHERN MARIANA ISL | | 604 | ST. PIERRE AND MIQUE | | 605 | SYRIAN ARAB REPUBLIC | | 606 | TERRITORIO ANTARTICO BRITANICO | | 607 | TERRITORIO BRITANICO OCEANO IN | | 688 | SERBIA | | 831 | GUERNSEY | | 832 | JERSEY | | 833 | ISLE OF MAN | ### Unidad de medida `detalles[].unidadMedida` es un texto libre de hasta 50 caracteres (el SRI no publica un catalogo cerrado para este campo). Valores habituales: `Kilos`, `Litros`, `Unidades`, `Cajas`. ## Siguientes pasos - [Emision](/v1/guias/emision) - [Glosario](/v1/guias/glosario) - [Primeros pasos](/v1/guias/primeros-pasos) --- # Limites Limites de la API: tamano de solicitud, paginacion y cupo. Aqui ves los topes por peticion y por consulta, y como responde la API al superarlos. ## Solicitud | Limite | Valor | |---|---| | Tamano del cuerpo | 2 MiB en emision; 1 MiB en el resto | | Profundidad JSON | 32 | | `Idempotency-Key` | ASCII estable, maximo 128 caracteres | | `origenReferencia` | maximo 64 caracteres | | `referenciaExterna` | maximo 128 caracteres | | `infoAdicional` | hasta 20 objetos | Un cuerpo mayor responde `413` (`codigo` `305`). Un JSON mas profundo o con campos desconocidos se rechaza `422` (`codigo` `302`). Un cuerpo mal formado responde `400` (`codigo` `301`). ## Consultas | Limite | Valor | |---|---| | Rango de fechas | 366 dias | | `pagina` | 1 a 10000 | | `tamanoPagina` emitidos | 1 a 100 | | `buscar` | 100 caracteres | | Sin fechas | ultimos 30 dias | ## Tasa de peticiones Los limites de solicitudes y el manejo de la respuesta `429` se explican en [Limite de request](/v1/guias/limite-de-request). Cada respuesta de `/v1` incluye `X-RateLimit-Limit` y `X-RateLimit-Remaining`; al superar el limite, `X-RateLimit-Reset` y `Retry-After`. ## Cupo Sin documentos disponibles en el ambiente, la emision responde `429` (`codigo` `501`). Consulta el saldo en [`/v1/consumo`](/v1/guias/consumo). ## Excepcion de pruebas En el ambiente de pruebas puedes usar una excepcion para ensayar: `secuencial` de 9 ceros junto con una clave de acceso de 49 ceros. No aplica a produccion. ## Siguientes pasos - [Errores](/v1/guias/errores) - [Emision](/v1/guias/emision) - [Primeros pasos](/v1/guias/primeros-pasos) --- # Glosario Terminos y convenciones de nombres de la API. Usa estos nombres al integrar y al reportar un caso. | Termino | Significado | |---|---| | **API Key** | Clave de tu integracion. Se envia en la cabecera `X-Api-Key`. | | **Ambiente** | Entorno de trabajo: `pruebas` (sandbox) o `produccion`. Cada ambiente tiene su propia URL. | | **Contribuyente** | Persona natural o juridica habilitada para emitir o recibir comprobantes. | | **Identificacion** | Valor del contribuyente que se usa en la ruta. El RUC completo de 13 digitos va en `infoTributaria.ruc`. | | **Emisor** | Contribuyente que emite el comprobante. | | **Receptor / contraparte** | Contribuyente que recibe el comprobante (comprador, proveedor o sujeto retenido). | | **Comprobante electronico** | Documento fiscal (factura, nota de credito o debito, guia, retencion o liquidacion) emitido y transmitido al SRI. | | **Clave de acceso** | Identificador unico de 49 digitos del comprobante. Lo asigna la plataforma; no lo envias. | | **Establecimiento / punto de emision** | Serie donde se numera un comprobante (`estab` y `ptoEmi`, 3 digitos cada uno). | | **Secuencial** | Numero consecutivo del comprobante dentro de la serie (9 digitos). | | **Operacion** | El proceso de emision de un comprobante. Se identifica con `idOperacion`. | | **Seguimiento** | Consultar el estado de la operacion (`GET /v1/operaciones/{id}`) o recibirlo por webhook. | | **Autorizacion** | Resultado del SRI: `no_disponible`, `pendiente`, `autorizado` o `error`. | | **Idempotency-Key** | Cabecera que identifica una intencion de emision y evita duplicados al reintentar. | | **Cupo** | Documentos disponibles para tu integracion (plan mas paquetes comprados). | | **Retencion** | Impuesto que el emisor descuenta al proveedor y acredita al SRI. | | **Comprobante de sustento** | Documento de referencia que respalda una retencion o una nota. | | **RIDE** | Representacion grafica del comprobante (el PDF con el detalle y el codigo de autorizacion). | | **Webhook** | Aviso HTTP que la plataforma te envia cuando una operacion cambia de estado. | | **Evento (`eventType`)** | El tipo de cambio notificado por un webhook (`document.authorized`, `document.rejected`, etc.). | | **`hayMas`** | Campo de los listados que indica si quedan mas resultados despues de la pagina actual. | | **SRI** | Servicio de Rentas Internas del Ecuador, autoridad fiscal. | ## Terminos fijos Usa siempre el mismo termino para el mismo concepto. No inventes sinonimos. Estos son los nombres canonicos: **comprobante**, **emision**, **operacion**, **autorizacion**, **claveAcceso**, **RIDE**, **contribuyente**, **ambiente**. ## No llames X a Y | No uses | Usa | Por que | |---|---|---| | "recibido" como sinonimo de autorizado | "recibido" (aun sin resultado) y "autorizado" (resultado fiscal) | Un comprobante recibido **no** esta autorizado. | | "factura" para cualquier comprobante | "comprobante" o el tipo (factura, nota de credito, retencion...) | Hay seis tipos con reglas propias. | | "SRI" para el comprobante o para la API | "el SRI" solo es la autoridad; el comprobante es "comprobante" | Evita confusion entre autoridad y documento. | | "credenciales" (plural) para varios RUC | "una credencial por integracion" | Una credencial cubre todos sus RUC habilitados. | | "estado fiscal" generico | `estado` (ciclo de envio) y `estadoAutorizacion` (resultado del SRI) | Son dos cosas distintas. | Lo mismo aplica a errores y microcopy: un codigo, un mensaje y una accion por caso. ## Siguientes pasos - [Primeros pasos](/v1/guias/primeros-pasos) - [Emision](/v1/guias/emision) - [Errores](/v1/guias/errores) --- # Preguntas frecuentes **La emision respondio `202`: el comprobante ya esta autorizado?** No. `202` es la solicitud; el SRI autoriza despues. Consulta el estado en `GET /v1/operaciones/{id}` o espera el [webhook](/v1/guias/webhooks). **Como evito duplicar un comprobante si mi proceso reintenta?** Envia siempre la misma `Idempotency-Key` y el mismo `referenciaExterna` en cada reintento de la misma intencion. La respuesta devuelve la misma operacion. **La clave de acceso la genero yo?** No. La plataforma la genera (49 digitos con digito verificador). No la envies en la solicitud. **Por que recibo `409`?** Hay un conflicto: reintento con otra intencion, numero o identidad fiscal ya registrada, o conflicto de idempotencia. Ve [Errores](/v1/guias/errores). **Por que un listado viene vacio pero con `hayMas` en `true`?** Una pagina puede quedar vacia y aun haber mas resultados; en ese caso, pide la siguiente pagina. **Como firmo/verifico los webhooks?** Con HMAC-SHA256 sobre `timestamp + "." + cuerpo`. Ve [Verificacion de firma](/v1/guias/webhooks-firma). **Hay SDK?** Si: .NET, Node.js, Python, Java y PHP. Ve [SDKs](/v1/guias/sdk). ## Siguientes pasos - [Errores](/v1/guias/errores) - [Webhooks](/v1/guias/webhooks) - [Emision](/v1/guias/emision) --- # Versionado La API versiona el contrato por **fecha**. La version forma parte de la ruta (`/v1`) y se identifica con una fecha de publicacion. Puedes fijar la version con la cabecera opcional `X-Api-Version`; si no la envias, se usa la version vigente. ## Politica - La version forma parte del contrato: un cambio **incompatible** se publica en una nueva version (nueva fecha), no dentro de la vigente. - Una version vigente solo recibe cambios **compatibles**: campos opcionales nuevos, endpoints nuevos y codigos documentados. Un integrador que ignora lo que no conoce no se rompe. - Un `eventType` de webhook desconocido se ignora sin detener el procesamiento. - **Ventana de deprecacion:** cuando una version se reemplaza, se anuncia aqui, se mantiene operativa durante el periodo de solapamiento y luego se retira. El anuncio y el retiro quedan fechados. ## Version vigente | Version | Fecha | Estado | Retiro | |---|---|---|---| | V1 | 2026-10-03 | Actual | - | El calendario en formato de maquina esta en [api-versions.json](/.well-known/api-versions.json) y el detalle de cambios en el [Changelog](/v1/guias/changelog). ## Cabecera X-Api-Version | Uso | Valor | |---|---| | Solicitud (opcional) | La fecha de la version deseada, p. ej. `2026-10-03`. Si se omite, se usa la vigente. | | Respuesta (siempre) | La version del contrato que respondio. | Si envias una version que no existe o ya se retiro, la API responde `400` con `codigo` `306` ([Errores](/v1/guias/errores)). No la fijes a ciegas: lee la version resuelta en la respuesta. ## Cabeceras de deprecacion (RFC 8594) Cuando una version entra en deprecacion, la respuesta puede incluir: - `Deprecation`: fecha en que la version quedo obsoleta. - `Sunset`: fecha en que la version dejara de responder. Consumelas para planificar la migracion antes del retiro. ## Cambios relevantes de V1 - No incluyas `version` ni `formato` en la Estructura del comprobante; si los envias, la API responde `422`. - Toda emision se realiza como normal (`1`); no informes `tipoEmision`. - La cabecera del tipo de comprobante va en `info`. - Errores con forma estable `codigo`, `mensaje` y `errores` (solo validacion); la correlacion va en la cabecera `X-Correlation-Id`. ## Siguientes pasos - [Changelog](/v1/guias/changelog) - [Respuestas del API](/v1/guias/respuestas) - [Errores](/v1/guias/errores) ---