# Introducción > Open Factura es una plataforma de código abierto para emitir Documentos Tributarios Electrónicos (DTE) de El Salvador desde tu sistema, tu terminal o tu asistente de IA. Open Factura emite **Documentos Tributarios Electrónicos (DTE)** ante el Ministerio de Hacienda (MH) de El Salvador según la Normativa de Cumplimiento 2.0. Tú envías JSON; Open Factura calcula los impuestos, asigna el número de control, firma, transmite y te devuelve el documento con su sello de recepción, su JSON oficial y su PDF. La API es de código abierto: úsala en la nube (`openfactura.co`), con el panel incluido, o despliégala en tu propia cuenta de Cloudflare ([autoalojamiento](https://docs.openfactura.co/autoalojamiento/)). ## Antes de empezar - Una cuenta en [app.openfactura.co](https://app.openfactura.co) con una organización. - El emisor registrado y sus credenciales del ambiente de pruebas del MH (contraseña de API y certificado `.crt`). - Una clave de API de pruebas (`of_test_…`). ¿Todavía no estás autorizado por Hacienda? Lee [Ser emisor ante Hacienda](https://docs.openfactura.co/hacienda/ser-emisor/). ## Tres formas de conectarse Las tres usan los mismos servicios, los mismos permisos y la misma clave de API. **API REST** JSON sobre HTTPS en `https://app.openfactura.co/api/v1`, descrita con OpenAPI 3.1. **MCP** Herramientas para Claude, Cursor y otros asistentes, remotas en `/mcp` o locales por stdio. **CLI** `openfactura dte emitir factura.json` desde la terminal o un pipeline de integración continua. ## Empieza aquí - [Inicio rápido](https://docs.openfactura.co/inicio-rapido/): De la cuenta nueva a la primera factura de prueba en minutos. - [Emitir documentos](https://docs.openfactura.co/guias/emitir/): Factura, Crédito Fiscal, notas, exportación, sujeto excluido y más. - [Conectar un asistente (MCP)](https://docs.openfactura.co/herramientas/mcp/): Que Claude valide, confirme y emita por ti. - [Referencia de la API](https://docs.openfactura.co/api/): Cada ruta, parámetro, respuesta y código de error. ## Explora - [Autenticación y claves](https://docs.openfactura.co/autenticacion/) - [Ambientes: pruebas y producción](https://docs.openfactura.co/ambientes/) - [Idempotencia](https://docs.openfactura.co/guias/idempotencia/) - [Contingencia](https://docs.openfactura.co/guias/contingencia/) - [Invalidación](https://docs.openfactura.co/guias/invalidacion/) - [Evento de retorno](https://docs.openfactura.co/guias/retorno/) - [CLI](https://docs.openfactura.co/herramientas/cli/) - [Habilidades para agentes](https://docs.openfactura.co/herramientas/habilidades/) - [SDK @openfactura/dte](https://docs.openfactura.co/herramientas/sdk/) - [Autoalojamiento](https://docs.openfactura.co/autoalojamiento/) - [Errores](https://docs.openfactura.co/errores/) --- # Inicio rápido > Crea tu cuenta, registra el emisor, genera una clave de pruebas y emite tu primera factura con curl, la CLI o MCP. En esta guía emites una **Factura (01)** en el ambiente de pruebas del MH. Nada de lo que hagas aquí tiene valor fiscal. 1. **Crea tu cuenta y tu organización.** Entra a [app.openfactura.co](https://app.openfactura.co), regístrate (la contraseña debe tener al menos 10 caracteres) y crea una organización. Tú quedas como propietario, con todos los permisos. 2. **Registra el emisor.** Al crear la organización, el alta guiada te pide los datos fiscales tal como aparecen en tu tarjeta de NRC: NIT, NRC, nombre, actividad económica, dirección (departamento, municipio y distrito), teléfono y correo. Al guardar se crean el establecimiento `M001` y el punto de venta `P001`. Después puedes editarlos en **Emisor → Datos fiscales**. 3. **Conecta con Hacienda.** En el último paso del alta carga, para el ambiente `00`: - el **certificado** `.crt` de pruebas y la contraseña de tu llave privada, - la **contraseña de API** que personalizaste en el Sitio Emisores DTE. Al pulsar **Conectar con Hacienda**, Open Factura verifica que la contraseña abra la llave y que el NIT del certificado sea el del emisor, y luego autentica contra el MH. Los secretos se guardan cifrados y la API nunca los devuelve. Si eliges «Lo haré después», el alta sigue disponible desde **Inicio**, y las credenciales también se cargan en **Emisor → Credenciales del MH**. > **Nota** > > ¿Aún no tienes credenciales de Hacienda? Ve [Ser emisor ante Hacienda](https://docs.openfactura.co/hacienda/ser-emisor/). > Para desarrollar sin ellas puedes [ejecutar Open Factura en local](https://docs.openfactura.co/autoalojamiento/#en-tu-equipo) > con el simulador del MH. 4. **Crea una clave de API de pruebas.** En **Desarrolladores → Nueva clave**, elige el modo **pruebas** y los permisos `dte:read` y `dte:emit`. Copia la clave `of_test_…`: se muestra una sola vez. ```bash export OPENFACTURA_API_KEY=of_test_... ``` 5. **Valida y emite tu primera factura.** En una Factura, `precioUni` **incluye IVA**. Open Factura calcula el IVA, los redondeos y el total. #### curl Primero valida: no consume número de control, no firma y no transmite. ```bash curl https://app.openfactura.co/api/v1/dte/validar \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tipoDte": "01", "datos": { "items": [{ "descripcion": "Servicio", "cantidad": 2, "precioUni": 11.3 }] } }' ``` Si `problemas` viene vacío, emite con una `Idempotency-Key` propia: ```bash curl https://app.openfactura.co/api/v1/dte \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: primera-factura-001" \ -d '{ "tipoDte": "01", "datos": { "items": [{ "descripcion": "Servicio", "cantidad": 2, "precioUni": 11.3 }] } }' ``` #### CLI ```bash npm i -g @openfactura/cli openfactura login --url https://app.openfactura.co # pide la clave sin mostrarla ``` Guarda el cuerpo en `factura.json`: ```json { "tipoDte": "01", "datos": { "items": [{ "descripcion": "Servicio", "cantidad": 2, "precioUni": 11.3 }] } } ``` ```bash openfactura dte validar factura.json openfactura dte emitir factura.json ``` La CLI genera y muestra una `Idempotency-Key` antes de enviar. #### MCP Conecta tu agente (Claude Code, Claude Desktop, Cursor, VS Code, Codex, Gemini CLI, Windsurf, Zed, Cline o Goose) con la CLI, que usa la clave guardada por `login`: ```bash npx @openfactura/cli login npx @openfactura/cli mcp install --todos # o un agente: cursor, codex, vscode… ``` Cualquier otro cliente MCP se conecta a `https://openfactura.co/mcp` con la cabecera `Authorization: Bearer of_test_...`. La configuración de cada agente está en [Conecta tu agente](https://docs.openfactura.co/herramientas/agentes/). Y pídele, por ejemplo: > Emite una factura de consumidor final por 2 servicios de $11.30 cada uno, IVA incluido. El asistente usa `dte_validar`, te muestra el resumen y llama `dte_emitir` sólo cuando tú lo confirmas. ## Respuesta `201 Created` con el `Documento` procesado por el MH: ```json { "id": "…", "codigoGeneracion": "0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4", "numeroControl": "DTE-01-M001P001-000000000000001", "tipoDte": "01", "ambiente": "00", "estado": "procesado", "fechaEmision": "2026-11-12", "horaEmision": "01:38:12", "receptorNombre": null, "receptorDocumento": null, "total": 22.6, "selloRecibido": "20265EE94C1B2C4CBB56F56121E37F5AC8CA7D27", "origen": "api", "error": null, "creado": 1794469092559, "urlConsulta": "https://admin.factura.gob.sv/…" } ``` Si el MH no responde, recibes `202 Accepted` con estado `contingencia`: el documento ya está firmado y es válido; se transmite después (ve [Contingencia](https://docs.openfactura.co/guias/contingencia/)). ## Entrega el documento Al receptor se le entregan el **JSON oficial** y su **representación gráfica** (PDF): #### curl ```bash curl -o factura.json https://app.openfactura.co/api/v1/dte/0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4/json \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" curl -o factura.pdf https://app.openfactura.co/api/v1/dte/0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4/pdf \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" ``` #### CLI ```bash openfactura dte json 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 -o factura.json openfactura dte pdf 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 -o factura.pdf ``` #### MCP `dte_obtener` con el `codigoGeneracion` devuelve el estado, el sello y la respuesta del MH. Para los archivos usa la CLI o la API. ## Siguientes pasos - [Emitir cada tipo de documento](https://docs.openfactura.co/guias/emitir/) - [Idempotencia](https://docs.openfactura.co/guias/idempotencia/): reintentar sin emitir dos veces. - [Ambientes](https://docs.openfactura.co/ambientes/): pasar a producción. --- # Autenticación y claves > Claves de API of_test_ y of_live_, permisos, sesión del panel y cómo se guarda cada secreto. Toda la API (`/api/v1/*`) y el servidor MCP (`/mcp`) exigen autenticación. Hay dos formas: | Quién | Cómo | Ambiente del MH | | ----------- | --------------------------------------- | --------------------------------- | | Integración | `Authorization: Bearer of_test_…` | `00` pruebas | | Integración | `Authorization: Bearer of_live_…` | `01` producción | | Panel web | Cookie de sesión (`/api/auth/*`) | El ambiente guardado en el emisor | El servidor MCP remoto sólo acepta claves de API, nunca la cookie del panel. ```bash curl https://app.openfactura.co/api/v1/yo -H "Authorization: Bearer $OPENFACTURA_API_KEY" ``` > **Nota** > > En Open Factura Cloud la API y el panel viven en `https://app.openfactura.co`. El servidor MCP remoto > usa la dirección corta `https://openfactura.co/mcp` (también responde en > `https://app.openfactura.co/mcp`). Por compatibilidad, `https://openfactura.co/api/v1/*` sigue > funcionando como alias de la API con claves de API; usa `app.openfactura.co` en integraciones nuevas. `GET /yo` responde quién eres: actor (`usuario` o `clave`), organización, permisos y ambiente activo. Úsalo para comprobar una clave. ## Crear una clave En el panel: **Desarrolladores → Nueva clave**. Elige nombre, modo (pruebas u producción), permisos y, si quieres, una caducidad en días. La clave completa se muestra **una sola vez**; el servidor guarda sólo su SHA-256 y, en adelante, muestra únicamente su prefijo. También se pueden administrar por la API (`GET|POST /claves`, `DELETE /claves/:id`), pero sólo desde una sesión del panel con el permiso `claves:manage`: una clave de API nunca puede crear otras claves. Revocar una clave tiene efecto inmediato. Una clave revocada o expirada responde `401 no_autenticado`. ## Permisos | Permiso | Permite | | ----------------- | --------------------------------------------------- | | `dte:read` | Consultar documentos, eventos, su JSON y su PDF | | `dte:emit` | Validar y emitir documentos, retornos y eventos 17 | | `dte:invalidate` | Invalidar documentos y transmitir contingencias | | `clientes:read` | Consultar clientes | | `clientes:write` | Crear, editar y eliminar clientes | | `productos:read` | Consultar productos | | `productos:write` | Crear, editar y eliminar productos | | `emisor:read` | Ver el emisor y sus establecimientos | | `emisor:write` | Editar el emisor, establecimientos y credenciales | | `claves:manage` | Crear y revocar claves de API | | `auditoria:read` | Ver la bitácora de auditoría | | `registros:read` | Ver los registros de peticiones hechas con claves de API | Una clave de API **nunca** puede llevar `emisor:write`, `claves:manage` ni `auditoria:read`: eso se hace en el panel. Roles del panel: **Propietario** y **Administrador** tienen todos los permisos; **Facturador** (`member`) tiene `dte:read`, `dte:emit`, `clientes:*`, `productos:*` y `emisor:read`. Si falta un permiso, la respuesta es `403 sin_permiso` con el permiso que falta en `details.permiso`. ### Permisos sugeridos - **Sólo consultar:** `dte:read`, `clientes:read`, `productos:read`, `emisor:read`. - **Facturar:** lo anterior más `dte:emit`, `clientes:write`, `productos:write`. - **Invalidar:** agrega `dte:invalidate` sólo si de verdad lo necesitas. ## Sesión del panel El panel usa Better Auth en `/api/auth/*` (registro, inicio de sesión, organizaciones e invitaciones). Un usuario sin organización recibe `409 sin_organizacion`; si pertenece a varias y no tiene una activa, `409 sin_organizacion_activa` con la lista en `details.organizaciones`. Las peticiones con cookie que modifican datos deben venir del mismo origen del panel; si no, `403 origen_no_permitido`. ## Credenciales del MH La contraseña de API del MH y la llave privada del certificado se guardan cifradas con AES-256-GCM y **nunca** se devuelven: `GET /emisor` sólo informa si están cargadas. No se registran en la auditoría ni en los logs. > **Precaución** > > No guardes claves en repositorios ni en el código del navegador. Usa variables de entorno o un gestor > de secretos. --- # Ambientes > Pruebas (00) y producción (01) del Ministerio de Hacienda, y cómo pasar de uno a otro. El MH tiene dos ambientes, y Open Factura los mantiene separados de punta a punta: | Ambiente | Código | Clave de API | Valor fiscal | | -------- | ------ | ------------ | ------------ | | Pruebas | `00` | `of_test_…` | Ninguno | | Producción | `01` | `of_live_…` | Legal | - **El prefijo de la clave decide el ambiente.** Una clave de pruebas nunca toca producción, ni al revés. - El panel usa el ambiente guardado en el emisor (**Emisor → Datos fiscales → Ambiente**). - Las credenciales del MH (contraseña de API y certificado) son **distintas en cada ambiente**: las de pruebas no sirven en producción. - Cada clave ve sólo los documentos y eventos de su ambiente. Las claves de idempotencia también son independientes por ambiente. - Los PDF del ambiente de pruebas advierten que el documento no tiene valor legal. Si operas sobre un documento de otro ambiente, la API responde `409 ambiente_distinto`. ## Pasar a producción 1. Completa las pruebas mínimas que exige Hacienda y obtén la autorización como emisor (ve [Ser emisor ante Hacienda](https://docs.openfactura.co/hacienda/ser-emisor/)). 2. En el Sitio Emisores DTE, ambiente productivo, genera el certificado y la contraseña de API de producción. 3. En **Emisor → Credenciales del MH**, cárgalos para el ambiente `01` (`PUT /api/v1/emisor/credenciales/01`). 4. Para el panel, cambia el ambiente del emisor a producción. Para integraciones, crea una clave `of_live_…` con los permisos mínimos. 5. Emite un primer documento real y verifica su sello y su `urlConsulta`. > **Precaución** > > Con una clave `of_live_…` cada emisión tiene efecto fiscal y consume un número de control. La CLI pide > confirmación en producción; un asistente MCP debe pedirte un "sí" explícito antes de emitir. --- # Emitir documentos > Cuerpo de emisión, qué tipo de DTE corresponde a cada operación e IVA en los precios, con ejemplos de cada tipo. Todos los documentos se emiten con la misma ruta, `POST /dte`, y se validan antes con `POST /dte/validar`: ```json { "tipoDte": "01", "datos": { "items": [{ "descripcion": "Servicio", "cantidad": 2, "precioUni": 11.3 }] }, "clienteId": "opcional", "puntoVentaId": "opcional" } ``` - `tipoDte`: el tipo de documento (tabla abajo). - `datos`: la entrada del constructor del SDK [`@openfactura/dte`](https://docs.openfactura.co/herramientas/sdk/) para ese tipo (`FacturaInput`, `CreditoFiscalInput`, `NotaInput`…). - `clienteId`: si lo envías y falta `datos.receptor`, el receptor se toma del cliente guardado. - `puntoVentaId`: punto de venta que emite; por defecto el primero del emisor. > **Consejo** > > No calcules impuestos ni totales. Open Factura calcula IVA, retenciones, redondeos (cuerpo a 8 > decimales, resumen a 2) y el total en letras. Muestra siempre el `total` que devuelve la API. ## Flujo recomendado 1. `POST /dte/validar` → `{ dte, problemas[], total, receptorNombre }`. No consume número de control. 2. Corrige cada `problemas[]` (`ruta`, `mensaje`) y repite hasta que venga vacío. 3. `POST /dte` con una [`Idempotency-Key`](https://docs.openfactura.co/guias/idempotencia/). 4. Resultado: - `201` estado `procesado`, con `selloRecibido`. - `202` estado `contingencia`: el MH no respondió; ver [Contingencia](https://docs.openfactura.co/guias/contingencia/). - `422 dte_rechazado`: el MH lo rechazó; `details.mh.observaciones` dice por qué. ## ¿Qué documento corresponde? | Situación | `tipoDte` | Receptor | | ------------------------------------------------------------ | --------- | ------------------------------------------------------------------------- | | Venta a consumidor final o a quien no pide crédito fiscal | `01` | Opcional (obligatorio desde $25,000) | | Venta a un contribuyente inscrito en IVA que pide crédito fiscal | `03` | NIT, NRC, nombre, actividad (`codActividad` + `descActividad`) y dirección | | Traslado de mercadería sin venta | `04` | Identificado, con `bienTitulo` | | Corregir a la baja un CCF ya emitido | `05` | El del documento original | | Aumentar el valor de un CCF ya emitido | `06` | El del documento original | | Retención de IVA a un proveedor | `07` | El sujeto retenido | | Venta al exterior | `11` | Extranjero, con `codPais` | | Compra a una persona no inscrita en IVA | `14` | `sujetoExcluido` | | Como comisionista, liquidar ventas por cuenta de un tercero | `08` | El mandante, con su NIT | | Como agente perceptor, liquidar a un afiliado | `09` | El afiliado: NIT, actividad y correo | | Acreditar una donación recibida | `15` | El donante | ¿Una devolución sobre una Factura, Exportación o Sujeto Excluido? No se emite una nota: se registra un [Evento de Retorno](https://docs.openfactura.co/guias/retorno/). ## IVA en los precios - **Factura (01):** `precioUni` **incluye** IVA (13 %). Es el precio que paga el consumidor. - **Crédito Fiscal (03), notas (05/06) y remisión (04):** `precioUni` va **sin** IVA. - **Productos guardados:** su `precioUnitario` está **sin** IVA. ## Campos comunes de los ítems `descripcion`, `cantidad`, `precioUni`, `tipoItem` (1 bien, 2 servicio, 3 ambos, 4 otros tributos; por defecto 1), `codigo`, `uniMedida` (CAT-014; 59 = unidad), `descuento` (total de la línea, no por unidad) y `tipoVenta` (`gravada` por defecto, `exenta` o `noSujeta`). Pago: por defecto contado en efectivo por el total. A crédito: `"condicionOperacion": 2` con `"credito": { "plazo": "01", "periodo": 30 }` (30 días). Otras formas de pago: `pagos` con códigos de CAT-017. Direcciones: `{ departamento, municipio, distrito, complemento }` con códigos de CAT-012, CAT-013 y CAT-008 (consulta `GET /catalogos/CAT-013?departamento=06`). NIT, DUI y NRC pueden ir con o sin guiones. ## Ejemplos #### 01 Factura ```json { "tipoDte": "01", "datos": { "items": [ { "descripcion": "Servicio de consultoría", "cantidad": 3, "precioUni": 56.5, "tipoItem": 2 } ] } } ``` Con `"retencionIva": true` se aplica la retención de IVA del 1 % cuando el receptor es agente de retención. #### 03 Crédito Fiscal Con un cliente guardado (el receptor sale del cliente): ```json { "tipoDte": "03", "clienteId": "c6de4c6c-d772-4f19-811a-5419655dcd06", "datos": { "items": [{ "descripcion": "Licencia anual", "cantidad": 1, "precioUni": 100, "tipoItem": 2 }] } } ``` O con el receptor completo: ```json { "tipoDte": "03", "datos": { "receptor": { "nit": "0614-290786-103-1", "nrc": "234567-8", "nombre": "CLIENTE CONTRIBUYENTE, S.A. DE C.V.", "codActividad": "46900", "descActividad": "Venta al por mayor de otros productos", "direccion": { "departamento": "06", "municipio": "23", "distrito": "14", "complemento": "Av. Olímpica #100" }, "correo": "compras@cliente.com.sv" }, "items": [{ "descripcion": "Licencia anual", "cantidad": 1, "precioUni": 100, "tipoItem": 2 }] } } ``` `percepcionIva` y `retencionIva` (`true` o un monto) aplican el 1 % cuando corresponde. #### 04 Remisión ```json { "tipoDte": "04", "clienteId": "c6de4c6c-d772-4f19-811a-5419655dcd06", "datos": { "bienTitulo": "04", "items": [{ "descripcion": "Sillas plásticas", "cantidad": 20, "precioUni": 8 }] } } ``` `bienTitulo` (CAT-025): `01` depósito, `02` propiedad, `03` consignación, `04` traslado, `05` otros. #### 05/06 Notas Nota de crédito sobre un CCF electrónico (la de débito es igual con `"tipoDte": "06"`): ```json { "tipoDte": "05", "clienteId": "c6de4c6c-d772-4f19-811a-5419655dcd06", "datos": { "documentosRelacionados": [ { "tipoDocumento": "03", "tipoGeneracion": 2, "numeroDocumento": "", "fechaEmision": "2026-11-12" } ], "items": [{ "descripcion": "Devolución parcial", "cantidad": 1, "precioUni": 20 }] } } ``` Con un solo documento relacionado, cada ítem lo hereda; con varios, cada ítem debe indicar su `numeroDocumento`. #### 07 Retención ```json { "tipoDte": "07", "clienteId": "c6de4c6c-d772-4f19-811a-5419655dcd06", "datos": { "items": [ { "tipoDte": "03", "tipoGeneracion": 2, "numeroDocumento": "", "fechaEmision": "2026-11-10", "montoSujetoGrav": 1000, "codigoRetencionMH": "22", "descripcion": "Compra de suministros" } ] } } ``` `codigoRetencionMH` (CAT-006): `22` retención del 1 %, `C4` del 13 %, `C9` otros (indica `ivaRetenido`). `montoSujetoGrav` va sin IVA. #### 11 Exportación ```json { "tipoDte": "11", "datos": { "receptor": { "nombre": "CLIENTE DEL EXTERIOR INC.", "tipoDocumento": "37", "numDocumento": "99-1234567", "codPais": "US", "complemento": "100 Main St, Miami, FL", "tipoPersona": 2, "descActividad": "Comercio al por mayor" }, "tipoItemExpor": 2, "items": [{ "descripcion": "Desarrollo de software", "cantidad": 1, "precioUni": 1500, "tipoItem": 2 }] } } ``` Para bienes indica además `recintoFiscal` (CAT-027), `tipoRegimen` (CAT-033), `regimen` (CAT-028) y, si aplica, `codIncoterms`, `seguro` y `flete`. #### 14 Sujeto Excluido El emisor es quien **compra** a una persona no inscrita en IVA: ```json { "tipoDte": "14", "datos": { "sujetoExcluido": { "tipoDocumento": "13", "numDocumento": "01234567-8", "nombre": "JUAN PÉREZ", "direccion": { "departamento": "06", "municipio": "23", "distrito": "14", "complemento": "Colonia Escalón" } }, "items": [{ "descripcion": "Servicio de jardinería", "cantidad": 1, "precioUni": 150, "tipoItem": 2 }], "retencionRenta": true } } ``` `retencionRenta: true` aplica el 10 % de renta sobre el subtotal (o indica el monto). #### 08 Liquidación El **comisionista** liquida al **mandante** (el receptor, con su NIT) las ventas que hizo por su cuenta en el periodo. Cada documento va con los montos en positivo, como figuran en él: las notas de crédito, los retornos y los anulados (`anulado: true`) restan solos. Las gravadas van sin IVA. ```json { "tipoDte": "08", "clienteId": "", "datos": { "documentos": [ { "tipoDte": "03", "numeroDocumento": "", "fechaEmision": "2026-11-03", "ventaGravada": 500, "ivaPercibido": 5 }, { "tipoDte": "05", "numeroDocumento": "", "fechaEmision": "2026-11-04", "ventaGravada": 100 } ] } } ``` En Factura y Retorno indica además `iva` (el "IVA 13 %" de su resumen); FOVIAL, COTRANS y otros tributos de la sección 1 de CAT-015 van en `tributos: [{ codigo, valor }]`. #### 09 Doc. Contable de Liquidación El **agente perceptor** (p. ej. un procesador de pagos con tarjeta) liquida al **afiliado** las operaciones de un periodo. El SDK calcula el IVA, la percepción del 2 %, el IVA de la comisión y el líquido a pagar: ```json { "tipoDte": "09", "datos": { "receptor": { "nit": "0614-290786-103-1", "nombre": "COMERCIO AFILIADO, S.A. DE C.V.", "codActividad": "47190", "descActividad": "Venta al por menor", "correo": "pagos@comercio.com.sv" }, "periodo": { "inicio": "2026-11-01", "fin": "2026-11-07" }, "valorOperaciones": 1130, "porcentComision": 3, "entrega": { "nombre": "Ana López", "documento": "01234567-8" } } } ``` #### 15 Donación La entidad autorizada a recibir donaciones (**donatario**) acredita al **donante** lo que recibió. La resolución que la autoriza (art. 6 LISR) es obligatoria, y un donante domiciliado debe llevar actividad económica y dirección (Hacienda rechaza el comprobante sin ellas): ```json { "tipoDte": "15", "datos": { "receptor": { "tipoDocumento": "13", "numDocumento": "01234567-8", "nombre": "María Gómez", "codActividad": "10005", "descActividad": "Otros", "direccion": { "departamento": "06", "municipio": "23", "distrito": "14", "complemento": "Colonia Escalón" } }, "documentosAsociados": [{ "descripcion": "Resolución", "detalle": "RES-01-2026" }], "items": [ { "tipoDonacion": 1, "descripcion": "Donación en efectivo", "valorUni": 100 }, { "tipoDonacion": 2, "descripcion": "Computadoras usadas", "cantidad": 3, "valorUni": 400, "depreciacion": 150 } ] } } ``` `tipoDonacion`: 1 efectivo (cantidad 1), 2 bien, 3 servicio. Las formas de pago cubren sólo lo donado en efectivo (por defecto, billetes y monedas). ## Consultar - `GET /dte?tipo=&estado=&q=&desde=&hasta=&pagina=&porPagina=` lista los documentos del ambiente activo. `q` busca por receptor, número de control o código de generación. - `GET /dte/{codigo}` devuelve el documento con su DTE, la respuesta del MH y sus eventos. - `GET /dte/{codigo}/json` y `GET /dte/{codigo}/pdf`: los archivos para el receptor. Estados: `pendiente` (esperando confirmación del MH) · `procesado` (válido, con sello) · `contingencia` (válido, se transmite después) · `rechazado` (sin valor) · `invalidado` (anulado). --- # Idempotencia > Reintenta una emisión con la misma Idempotency-Key y nunca emita dos veces el mismo documento. Una emisión tiene efecto fiscal. Si la red falla justo después de enviarla, no sabes si el documento se emitió. Por eso estas rutas aceptan el encabezado `Idempotency-Key`: - `POST /dte` - `POST /dte/{codigo}/retorno` - `POST /eventos/operaciones-especiales` ```bash curl https://app.openfactura.co/api/v1/dte \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-1042" \ -d @factura.json ``` ## Reglas - La clave es tuya: un UUID o un identificador de tu sistema (`pedido-1042`), de hasta 200 caracteres. - **Misma clave y mismo cuerpo** → la respuesta original, con el encabezado `Idempotent-Replayed: true`. No se emite dos veces. - **Misma clave y otro cuerpo** → `409 idempotencia_distinta`. - **El primer intento sigue en curso** → `409 idempotencia_en_curso`: espera unos segundos y repite. - Las claves se conservan **24 horas** y son **independientes por ambiente**: una clave usada en pruebas no devuelve nada en producción. - Se guardan como respuesta definitiva los éxitos, `dte_rechazado` y `emision_incierta`. Los demás errores liberan la clave para reintentar. > **Consejo** > > Regla de oro: **reintenta sólo con la misma clave**. Una clave nueva puede crear un segundo documento > con valor fiscal. Usa una clave nueva únicamente para un documento distinto, o después de un > `dte_rechazado` cuando corriges los datos. ## En la CLI y en MCP - La CLI genera un UUID para cada `dte emitir` y lo muestra por stderr **antes** de enviar. Para reintentar: `openfactura dte emitir factura.json --idempotency-key `. - Las herramientas MCP `dte_emitir`, `dte_retorno` y `operaciones_especiales_informar` exigen el argumento `idempotencyKey`. --- # Contingencia > Qué pasa cuando el MH no responde, cómo se transmiten después los documentos y en qué se diferencia de una emisión incierta. Si el MH no responde al emitir, Open Factura no pierde la venta: emite el documento en **contingencia** (modelo diferido). Responde `202 Accepted` con estado `contingencia`. - El documento **ya es válido**: quedó firmado y tiene número de control. Entrégalo normalmente. - **No lo vuelvas a emitir.** - Debe transmitirse al MH después, con el evento de contingencia. ## Transmitir #### curl ```bash curl -X POST https://app.openfactura.co/api/v1/contingencia/transmitir \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" ``` #### CLI ```bash openfactura contingencia transmitir ``` #### Panel Abre **Contingencia** en el panel y transmite los documentos pendientes. Requiere el permiso `dte:invalidate` y el **responsable** configurado en los datos del emisor. Open Factura envía el evento de contingencia con hasta 40 documentos del ambiente activo y luego transmite cada uno. La respuesta: ```json { "evento": { "id": "…", "codigoGeneracion": "…" }, "procesados": [], "rechazados": [], "restantes": 0 } ``` - `procesados`: documentos que recibieron sello. - `rechazados`: documentos que el MH rechazó. - `restantes`: si es mayor que 0, vuelve a llamar. - Si el MH vuelve a caer, lo que falta sigue en contingencia para el próximo intento. - Si el MH rechaza el evento: `422 contingencia_rechazada` con `details.mh`. ## Contingencia no es emisión incierta | Situación | Código / estado | Qué hacer | | ----------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------- | | El MH no respondió | `202`, estado `contingencia` | Entrega el documento y transmite la contingencia. | | Resultado desconocido | `502 emision_incierta`, `pendiente` | **No emitas otro.** Consulta `GET /dte/{details.codigoGeneracion}`; el servidor lo concilia solo con el MH cada pocos minutos. | | El MH no estaba disponible antes de firmar | `502 mh_no_disponible` | No se consumió número de control: reintenta con la misma `Idempotency-Key`. | > **Nota** > > Los documentos en contingencia y los eventos de contingencia cuentan para las pruebas mínimas de > Hacienda: ejercítalos en el ambiente de pruebas. --- # Invalidación > Anular un DTE o un evento procesado, con sus tipos, plazos y el orden correcto cuando existe un retorno. > **Precaución** > > La invalidación es **irreversible** y tiene efecto fiscal. Requiere el permiso `dte:invalidate`. ## Antes de invalidar 1. Consulta el documento (`GET /dte/{codigo}`) y confirma que está `procesado`: sólo esos se invalidan; si no, `409 estado_invalido`. 2. Si tiene un **evento de retorno** vigente, la API responde `409 retorno_vigente` con `details.codigoGeneracion`: invalida primero ese retorno. 3. Revisa el plazo: - Factura (01), Exportación (11) y Sujeto Excluido (14): **3 meses** desde el sello. - Los demás (Crédito Fiscal 03, notas 05/06, etc.): hasta el **10.º día hábil del mes siguiente** a la emisión. Esto incluye Liquidación (08), Doc. Contable de Liquidación (09) y Donación (15). - Fuera de plazo el MH la rechaza (`422 invalidacion_rechazada`). Para un CCF vencido, lo normal es una Nota de Crédito (05). ## Tipo de anulación | `tipoAnulacion` | Cuándo | Requiere | | --------------- | -------------------------------------------- | ------------------------------------------------ | | `1` | Error en la información | `codigoGeneracionReemplazo` (emite antes el correcto) | | `2` | Rescindir la operación (la venta no ocurrió) | — | | `3` | Otro | `motivoAnulacion` y `codigoGeneracionReemplazo` | La Nota de Crédito (05), el Comprobante de Liquidación (08) y los eventos no llevan documento de reemplazo: se invalidan y después se emite el correcto, así que `codigoGeneracionReemplazo` se ignora. Opcionales: `solicitante` (por defecto, el receptor del documento) y `responsable` (por defecto, el configurado en el emisor), cada uno `{ nombre, tipoDocumento, numDocumento }`. ## Invalidar #### curl ```bash curl https://app.openfactura.co/api/v1/dte/0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4/invalidar \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tipoAnulacion": 2, "motivoAnulacion": "Operación rescindida" }' ``` #### CLI ```bash openfactura dte invalidar 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 --tipo 2 --motivo "Operación rescindida" openfactura dte invalidar --tipo 1 --reemplazo ``` #### MCP `dte_invalidar` con `codigoGeneracion`, `tipoAnulacion` y, si aplica, `motivoAnulacion` y `codigoGeneracionReemplazo`. El asistente debe resumirte la operación y pedirte un "sí" explícito. La respuesta es el documento, ahora en estado `invalidado`. ## Invalidar un evento La misma ruta invalida un evento de retorno u operaciones especiales si recibe su código de generación. Entonces responde `{ "invalidado": true, "tipo": "retorno", "codigoGeneracion": "…" }`. Un retorno se invalida dentro de los 4 días siguientes a su sello. --- # Evento de retorno > Devoluciones, reembolsos y envases sobre una Factura, Factura de Exportación o Sujeto Excluido, sin invalidar el documento. El **Evento de Retorno (18)** registra la devolución de bienes, un reembolso o la devolución de envases sobre una **Factura (01)**, **Factura de Exportación (11)** o **Sujeto Excluido (14)** procesados. El documento original sigue vigente. - Requiere el permiso `dte:emit` y admite [`Idempotency-Key`](https://docs.openfactura.co/guias/idempotencia/). - `precioUni` va como en el documento original: en Factura (01) **con** IVA; en 11 y 14 sin IVA. - Receptor, terceros y datos de exportación se copian del documento. Las retenciones se aplican si el documento las tuvo. - Plazo: **3 meses** desde la fecha del documento (2 años para Factura y Exportación de las actividades 21001, 21008, 46482, 46484, 46491 y 47721). - La suma de los retornos no puede superar el documento: `409 retorno_excedido` con `details.disponible`. - Si el cliente cambia el producto, además emite un documento nuevo por la nueva venta. ## Registrar un retorno #### curl ```bash curl https://app.openfactura.co/api/v1/dte/0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4/retorno \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: devolucion-1042" \ -d '{ "items": [{ "descripcion": "Silla", "cantidad": 1, "precioUni": 22.6 }] }' ``` #### CLI ```bash echo '{ "items": [{ "descripcion": "Silla", "cantidad": 1, "precioUni": 22.6 }] }' \ | openfactura dte retorno 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 - ``` #### MCP `dte_retorno` con el `codigoGeneracion` del documento, `idempotencyKey` e `items`. Campos de cada ítem: `descripcion`, `cantidad`, `precioUni` y, opcionales, `tipoItem`, `codigo`, `uniMedida`, `descuento`, `tipoVenta`, `seguro` y `flete`. Respuesta: `201` con el `Evento`: ```json { "id": "…", "tipo": "retorno", "tipoEvento": "18", "codigoGeneracion": "…", "ambiente": "00", "estado": "procesado", "selloRecibido": "…", "documentoId": "…", "creado": 1794469092559 } ``` Si el MH rechaza el evento: `422 evento_rechazado` con `details.mh`. ## Consultar y entregar - `GET /eventos?tipo=retorno` — los 50 más recientes del ambiente activo. - `GET /eventos/{codigo}` — el evento con su JSON y la respuesta del MH. - `GET /eventos/{codigo}/json` y `GET /eventos/{codigo}/pdf` — archivo y representación gráfica para el receptor. > **Nota** > > Un documento con un retorno vigente no se puede invalidar hasta invalidar antes el retorno (ve > [Invalidación](https://docs.openfactura.co/guias/invalidacion/#invalidar-un-evento)). --- # Operaciones especiales > Informar cada periodo las facturas simplificadas y los comprobantes de control interno con el evento 17. El **Evento de Operaciones Especiales (17)** informa al MH, por periodo, documentos que no son DTE: - **Facturas de venta simplificadas** (`tipoDocumento: "02"`): siempre por rangos `docDel`–`docAl`. - **Comprobantes de control interno** (`tipoDocumento: "97"`): uno a uno (`numDocumento` + `fechaEmision`) o por rangos. No se mezclan las dos formas. Requiere `dte:emit` y admite [`Idempotency-Key`](https://docs.openfactura.co/guias/idempotencia/). Los precios van **sin IVA**, también en las facturas simplificadas. ## Cuerpo ```json { "tipoDocumento": "02", "fecha": "2026-10-31", "items": [ { "descripcion": "Facturas simplificadas", "docDel": "1", "docAl": "25", "precioUni": 10 } ] } ``` - `fecha` (AAAA-MM-DD) define el periodo informado. Por defecto hoy; nunca una fecha futura. - Cada ítem: `descripcion`, `precioUni` y, según el caso, `cantidad`, `numDocumento`, `docDel`, `docAl`, `fechaEmision`, `tipoVenta`, `codigoGeneracionRef`. Hasta 2000 ítems. - Los documentos **anulados** van en un evento aparte, todos con `codigoGeneracionRef`: el código del evento donde se informaron activos. ## Enviar #### curl ```bash curl https://app.openfactura.co/api/v1/eventos/operaciones-especiales \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: simplificadas-2026-10" \ -d @simplificadas.json ``` #### CLI ```bash openfactura eventos operaciones-especiales simplificadas.json ``` #### MCP `operaciones_especiales_informar` con `tipoDocumento`, `fecha`, `items` e `idempotencyKey`. Respuesta: `201` con el `Evento` (`tipo: "operaciones_especiales"`, `tipoEvento: "17"`). Si el MH lo rechaza: `422 evento_rechazado`. Consulta los enviados con `GET /eventos?tipo=operaciones_especiales`. --- # Webhooks > Recibe un aviso firmado en tu servidor cuando un documento o evento cambia de estado. Open Factura avisa a tu sistema cuando un documento cambia de estado, sin que tengas que consultar. Configúralos en el panel (**Administración → Webhooks**), con la API, con `openfactura webhooks` o con las herramientas MCP. Requieren el permiso `webhooks:manage`. ## Rutas (`webhooks:manage`) - `GET /webhooks` → `[{ id, url, descripcion, eventos[], activo, creado, actualizado }]` (nunca el secreto) - `POST /webhooks` — `{ url, eventos: [...], descripcion?, activo? }` → lo anterior más `secreto` (`whsec_…`, sólo esta vez). Hasta 10 por organización (`409 limite_webhooks`). - `PATCH /webhooks/:id` — cualquiera de `{ url, descripcion, eventos, activo }` - `DELETE /webhooks/:id` → `{ eliminado: true }`; borra también su historial de entregas - `POST /webhooks/:id/rotar-secreto` → el webhook con un `secreto` nuevo; el anterior deja de firmar de inmediato - `POST /webhooks/:id/probar` → envía ya un evento `webhook.prueba` y responde `{ entregado, status, error, entrega }`. No se reintenta. - `GET /webhooks/:id/entregas` → las 50 más recientes: `[{ id, webhookId, evento, estado: 'pendiente'|'entregado'|'fallido', intentos, ultimoStatus, ultimoError, proximoIntento, creado, entregado }]` - `POST /webhooks/entregas/:id/reenviar` → reenvía ahora el mismo cuerpo con una firma nueva y reinicia sus reintentos Crear, cambiar, eliminar y rotar quedan en la auditoría (sólo el host de la URL, nunca el secreto). ## Eventos | Evento | Cuándo | `datos` | | ---------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `dte.procesado` | El MH selló un documento: al emitir, al transmitir una contingencia o al conciliar un pendiente | Documento (como `GET /dte/:codigo` sin `dte`, `respuestaMh` ni `eventos`) | | `dte.rechazado` | El MH rechazó un documento (al emitir o al transmitir la contingencia), o la conciliación lo descartó | Documento | | `dte.contingencia` | El MH no respondió y el documento quedó firmado en contingencia | Documento | | `dte.invalidado` | Se invalidó un documento | Documento | | `retorno.procesado` | El MH selló un evento de retorno (18) | Evento (como `GET /eventos/:codigo` sin `dte`) | | `operaciones_especiales.procesado` | El MH selló un evento de operaciones especiales (17) | Evento | `webhook.prueba` sólo se envía con `POST /webhooks/:id/probar`. Los nombres de los eventos son contrato público y no cambian. ## Cuerpo ```json { "id": "1f0c…", "evento": "dte.procesado", "creado": "2026-11-12T16:04:05.123Z", "ambiente": "00", "organizacion": "org_…", "datos": { "codigoGeneracion": "C25227C9-…", "numeroControl": "DTE-01-M001P001-000000000000001", "tipoDte": "01", "estado": "procesado", "selloRecibido": "2026…", "total": 22.6, "…": "…" } } ``` `id` es el id de la entrega (igual a la cabecera `OpenFactura-Delivery`): úsalo para descartar duplicados, porque una entrega puede llegar más de una vez. `ambiente` permite ignorar los de pruebas (`00`). El cuerpo nunca lleva el JSON del DTE ni datos de las credenciales del MH: si necesitas el documento completo, pídelo con `GET /dte/:codigo`. ## Cabeceras ``` POST Content-Type: application/json User-Agent: OpenFactura-Webhooks/1 OpenFactura-Event: dte.procesado OpenFactura-Delivery: 1f0c… OpenFactura-Signature: t=1794500645,v1=5d41402abc4b2a76b9719d911017c592… ``` `v1` es el HMAC-SHA256, en hexadecimal, del texto `"."` con el secreto completo (incluido `whsec_`) como llave. `t` es la hora del envío en segundos Unix. ## Verificar la firma (Node 18+, Workers, Deno o Bun, con WebCrypto) ```ts const codificar = (texto: string) => new TextEncoder().encode(texto) /** Devuelve true si la petición viene de Open Factura y tiene menos de 5 minutos. */ export async function verificarFirma(cuerpoCrudo: string, cabecera: string, secreto: string) { const partes = Object.fromEntries( cabecera.split(',').map((p) => p.split('=') as [string, string]), ) const t = Number(partes.t) const v1 = partes.v1 ?? '' if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false if (!/^[0-9a-f]{64}$/.test(v1)) return false const llave = await crypto.subtle.importKey( 'raw', codificar(secreto), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'], ) const firma = Uint8Array.from(v1.match(/../g) ?? [], (h) => Number.parseInt(h, 16)) // `verify` compara en tiempo constante. return crypto.subtle.verify('HMAC', llave, firma, codificar(`${t}.${cuerpoCrudo}`)) } // Ejemplo con un handler estándar (Hono, Workers, Next.js route handlers…): export async function POST(peticion: Request) { const cuerpo = await peticion.text() // el cuerpo CRUDO, antes de JSON.parse const firma = peticion.headers.get('OpenFactura-Signature') ?? '' if (!(await verificarFirma(cuerpo, firma, process.env.OPENFACTURA_WEBHOOK_SECRET!))) { return new Response('firma inválida', { status: 400 }) } const aviso = JSON.parse(cuerpo) // … encola el trabajo y responde rápido; ignora `aviso.id` si ya lo procesaste. return new Response(null, { status: 204 }) } ``` ## Entrega y reintentos - Cualquier respuesta 2xx en menos de 10 s cuenta como entregada. Las redirecciones no se siguen y cuentan como fallo. - El primer intento sale justo después de la operación, sin retrasar la respuesta de la API. Una operación que cambia muchos documentos a la vez (p. ej. una contingencia grande) intenta los primeros 10 de inmediato y deja el resto al cron. - Si falla, se reintenta desde el cron (cada 5 minutos) con espera creciente: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h. Tras 8 intentos (casi dos días) la entrega queda `fallido`; puedes reenviarla a mano con `POST /webhooks/entregas/:id/reenviar`. - Desactivar un webhook detiene sus reintentos (las entregas pendientes quedan `fallido`). - Cada ejecución del cron envía como mucho 20 entregas. ## Seguridad - Sólo URLs `https://` con nombre de dominio público. Se rechazan `localhost`, `*.localhost`, `*.local`, `*.internal`, IPs privadas, de loopback, de enlace local y reservadas (IPv4 e IPv6, en cualquier notación), nombres sin dominio y URLs con usuario o contraseña. En desarrollo, `WEBHOOKS_LOCAL=1` permite además `http://localhost`. - El secreto se guarda cifrado (AES-256-GCM con la llave maestra) y sólo se muestra al crearlo o rotarlo. Si se filtra, rótalo. - El cuerpo de la respuesta de tu servidor no se lee ni se guarda; el historial sólo conserva el estado HTTP y un motivo breve. --- # Límites de peticiones > Cuántas peticiones admite cada clave, los encabezados RateLimit y cómo reintentar tras un 429. | Qué | Límite | | ------------------------------------- | ------------------------------- | | Cada clave de API | 120 peticiones por minuto | | Cada sesión del panel | 600 peticiones por minuto | | Intentos con claves inválidas, por IP | 30 por minuto | | Rutas de sesión (`/api/auth/*`) | Límite por IP | | Tamaño del cuerpo | 512 KB | En una instalación propia, el límite por clave se cambia con la variable `LIMITE_CLAVE_POR_MINUTO`. ## Encabezados Toda respuesta autenticada con clave de API trae: ```http RateLimit-Limit: 120 RateLimit-Remaining: 117 RateLimit-Reset: 42 ``` `RateLimit-Reset` son los segundos que faltan para reiniciar la ventana. ## Al exceder el límite `429 limite_excedido` con el encabezado `Retry-After` (segundos) y `details.reintentarEn`: ```json { "error": { "code": "limite_excedido", "message": "…", "details": { "reintentarEn": 18 } } } ``` Espera `Retry-After` segundos y reintenta. Si era una emisión, usa la **misma** [`Idempotency-Key`](https://docs.openfactura.co/guias/idempotencia/). La CLI termina con código de salida `4` y muestra el `Retry-After`. Un cuerpo de más de 512 KB recibe `413` con el código `datos_invalidos`. --- # Conecta tu agente > Conecta Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Codex, Gemini CLI o Zed al servidor MCP de Open Factura. El servidor MCP de Open Factura funciona con cualquier cliente [MCP](https://modelcontextprotocol.io). Hay dos formas de conectarlo: - **Remota (HTTP):** `https://openfactura.co/mcp` con el encabezado `Authorization: Bearer of_test_…`. No instalas nada. - **Local (stdio):** `npx -y @openfactura/cli mcp`. Usa la clave que guardaste con `openfactura login` (o las variables `OPENFACTURA_API_KEY` y `OPENFACTURA_URL`). Sirve para clientes que no envían encabezados. Mientras pruebas, usa una clave `of_test_…`: nada de lo que emitas tiene valor fiscal. La clave decide qué puede hacer el agente; ve [Autenticación y claves](https://docs.openfactura.co/autenticacion/) y las herramientas disponibles en [Servidor MCP](https://docs.openfactura.co/herramientas/mcp/). ## La forma más rápida ```bash npx @openfactura/cli login --url https://app.openfactura.co # pide la clave sin mostrarla npx @openfactura/cli mcp install cursor # un agente npx @openfactura/cli mcp install --todos # todos los que encuentre en tu equipo npx @openfactura/cli mcp install cursor --imprimir # sólo muestra la configuración ``` Agentes admitidos: `claude-code`, `claude-desktop`, `cursor`, `vscode`, `windsurf`, `codex`, `gemini`, `zed`, `cline` y `goose`. Con `npx @openfactura/cli mcp agentes` ves cuáles detecta en tu equipo. Antes de escribir, la CLI guarda una copia de respaldo del archivo de configuración, y nunca elimina los otros servidores que ya tengas. ## Configuración manual Reemplaza `of_test_...` por tu clave. Después de editar un archivo, reinicia el agente o recarga sus servidores MCP. #### Claude Code Remoto: ```bash claude mcp add --transport http openfactura https://openfactura.co/mcp \ --header "Authorization: Bearer of_test_..." ``` Local (stdio), con la clave de `openfactura login`: ```bash claude mcp add --transport stdio openfactura -- npx -y @openfactura/cli mcp ``` Añade `--scope user` para tenerlo en todos tus proyectos o `--scope project` para compartirlo en el `.mcp.json` del repositorio. Más detalles en la [documentación de Claude Code](https://code.claude.com/docs/en/mcp). #### Claude Desktop Claude Desktop usa el servidor local. Abre **Settings → Developer → Edit Config**, o edita `claude_desktop_config.json`: | Sistema | Archivo | | ------- | ------------------------------------------------------------------ | | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | | Windows | `%APPDATA%\Claude\claude_desktop_config.json` | | Linux | `~/.config/Claude/claude_desktop_config.json` | ```json { "mcpServers": { "openfactura": { "command": "npx", "args": ["-y", "@openfactura/cli", "mcp"], "env": { "OPENFACTURA_API_KEY": "of_test_...", "OPENFACTURA_URL": "https://app.openfactura.co" } } } } ``` Si ya ejecutaste `openfactura login` en ese equipo puedes omitir `env`. Cierra Claude Desktop por completo y vuelve a abrirlo. #### Cursor En un clic (después reemplaza `of_test_TU_CLAVE` por tu clave en `mcp.json`): [Instalar en Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=openfactura&config=eyJ1cmwiOiJodHRwczovL29wZW5mYWN0dXJhLmNvL21jcCIsImhlYWRlcnMiOnsiQXV0aG9yaXphdGlvbiI6IkJlYXJlciBvZl90ZXN0X1RVX0NMQVZFIn19) O a mano, en `~/.cursor/mcp.json` (todos los proyectos) o `.cursor/mcp.json` (sólo uno): ```json { "mcpServers": { "openfactura": { "url": "https://openfactura.co/mcp", "headers": { "Authorization": "Bearer ${env:OPENFACTURA_API_KEY}" } } } } ``` Cursor sustituye `${env:OPENFACTURA_API_KEY}` por esa variable de entorno, así la clave no queda en el archivo. También puedes escribir la clave directamente. Ve la [documentación de Cursor](https://cursor.com/docs/context/mcp). #### VS Code En un clic (después reemplaza `of_test_TU_CLAVE` por tu clave en `mcp.json`): [Instalar en VS Code](vscode:mcp/install?%7B%22name%22%3A%22openfactura%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fopenfactura.co%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20of_test_TU_CLAVE%22%7D%7D) O a mano, en `.vscode/mcp.json` de tu proyecto (o en el `mcp.json` de tu perfil de usuario). Con `inputs`, VS Code te pide la clave la primera vez y la guarda de forma segura: ```json { "inputs": [ { "type": "promptString", "id": "openfactura-key", "description": "Clave de API de Open Factura (of_test_…)", "password": true } ], "servers": { "openfactura": { "type": "http", "url": "https://openfactura.co/mcp", "headers": { "Authorization": "Bearer ${input:openfactura-key}" } } } } ``` Ve la [referencia de configuración MCP de VS Code](https://code.visualstudio.com/docs/agents/reference/mcp-configuration). #### Windsurf Abre el archivo `mcp_config.json` desde el panel MCP de Cascade y añade: ```json { "mcpServers": { "openfactura": { "serverUrl": "https://openfactura.co/mcp", "headers": { "Authorization": "Bearer ${env:OPENFACTURA_API_KEY}" } } } } ``` La ubicación del archivo cambió entre versiones (`~/.codeium/windsurf/mcp_config.json` en las anteriores); consulta la [documentación de Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp). #### Codex CLI En `~/.codex/config.toml`: ```toml [mcp_servers.openfactura] url = "https://openfactura.co/mcp" bearer_token_env_var = "OPENFACTURA_API_KEY" ``` Codex envía el contenido de `OPENFACTURA_API_KEY` como `Authorization: Bearer …`. Para el servidor local: ```bash codex mcp add openfactura -- npx -y @openfactura/cli mcp ``` Ve la [documentación de Codex](https://developers.openai.com/codex/mcp). #### Gemini CLI ```bash gemini mcp add --transport http --header "Authorization: Bearer of_test_..." \ openfactura https://openfactura.co/mcp ``` O en `~/.gemini/settings.json` (todos los proyectos) o `.gemini/settings.json` (sólo uno): ```json { "mcpServers": { "openfactura": { "httpUrl": "https://openfactura.co/mcp", "headers": { "Authorization": "Bearer of_test_..." } } } } ``` Usa `httpUrl` (Streamable HTTP), no `url`, que en Gemini CLI significa SSE. Ve la [documentación de Gemini CLI](https://geminicli.com/docs/tools/mcp-server/). #### Zed Abre tu configuración con la acción `zed: open settings file` y añade: ```json { "context_servers": { "openfactura": { "url": "https://openfactura.co/mcp", "headers": { "Authorization": "Bearer of_test_..." } } } } ``` Ve la [documentación de Zed](https://zed.dev/docs/ai/mcp). ## ChatGPT y claude.ai (web) ChatGPT y claude.ai en el navegador se conectan a servidores MCP mediante conectores que exigen OAuth. El servidor de Open Factura todavía sólo acepta claves de API; el inicio de sesión con OAuth llegará pronto. Mientras tanto, usa Claude Code, Claude Desktop, Cursor, VS Code u otro de los agentes de esta página. ## Enseña a tu agente a facturar Las [habilidades para agentes](https://docs.openfactura.co/herramientas/habilidades/) le enseñan a validar antes de emitir, a confirmar contigo y a manejar los errores: ```bash npx skills add xaviigna/openfactura ``` --- # Servidor MCP > Conecta Claude, Cursor, VS Code, Codex, Gemini o cualquier cliente MCP a Open Factura, de forma remota o local por stdio. Open Factura expone sus operaciones como herramientas [MCP](https://modelcontextprotocol.io) para que un asistente de IA consulte, valide y emita documentos. Cada herramienta exige el mismo permiso que su ruta REST, así que **la clave de API decide qué puede hacer el asistente**. Empieza siempre con una clave `of_test_…`. ## Dos formas de conectarse | Forma | Dónde | Úsala cuando | | ---------------- | ------------------------------------ | ---------------------------------------------------- | | **Remota** | `https://openfactura.co/mcp` | Tu cliente puede enviar un encabezado HTTP fijo | | **Local (stdio)** | `npx -y @openfactura/cli mcp` | Tu cliente no envía encabezados (p. ej. Claude Desktop) | El servidor remoto es Streamable HTTP sin estado y sólo acepta claves de API (no la cookie del panel). El local expone las mismas herramientas y las ejecuta llamando a la API REST con tu clave. ## Conecta cualquier agente con un comando Funciona con cualquier cliente MCP. La CLI escribe la configuración de Claude Code, Claude Desktop, Cursor, VS Code, Codex, Gemini CLI, Windsurf, Zed, Cline y Goose: ```bash npx @openfactura/cli login npx @openfactura/cli mcp install --todos # o un agente: cursor, codex, vscode… ``` La configuración manual de cada agente está en [Conecta tu agente](https://docs.openfactura.co/herramientas/agentes/). Ejemplos con Claude: #### Claude Code (remoto) ```bash claude mcp add --transport http openfactura https://openfactura.co/mcp \ --header "Authorization: Bearer of_test_..." ``` #### Claude Code (stdio) ```bash claude mcp add openfactura \ --env OPENFACTURA_API_KEY=of_test_... \ --env OPENFACTURA_URL=https://app.openfactura.co \ -- npx -y @openfactura/cli mcp ``` #### Claude Desktop En `claude_desktop_config.json` (o en un `.mcp.json` de proyecto): ```json { "mcpServers": { "openfactura": { "command": "npx", "args": ["-y", "@openfactura/cli", "mcp"], "env": { "OPENFACTURA_API_KEY": "of_test_...", "OPENFACTURA_URL": "https://app.openfactura.co" } } } } ``` Si ya ejecutaste `openfactura login` en ese equipo puedes omitir `env`. ## Herramientas | Herramienta | Qué hace | Permiso | | --------------------------------- | ----------------------------------------------- | ---------------- | | `emisor_obtener` | Emisor, ambiente activo, puntos de venta | `emisor:read` | | `catalogo_buscar` | Busca en catálogos del MH (CAT-xxx) | — | | `clientes_buscar` | Busca clientes guardados | `clientes:read` | | `cliente_crear` | Guarda un cliente | `clientes:write` | | `productos_buscar` | Busca productos | `productos:read` | | `producto_crear` | Guarda un producto (precio sin IVA) | `productos:write` | | `dte_validar` | Valida sin emitir; devuelve problemas y total | `dte:emit` | | `dte_emitir` | Emite (requiere `idempotencyKey`) | `dte:emit` | | `dte_listar` | Lista documentos del ambiente activo | `dte:read` | | `dte_obtener` | Estado, sello, respuesta del MH y eventos | `dte:read` | | `dte_invalidar` | Invalida un documento o un evento | `dte:invalidate` | | `certificacion_estado` | Avance de las pruebas mínimas (sólo Cloud) | `dte:read` | | `certificacion_avanzar` | Siguiente tanda de pruebas mínimas (sólo Cloud) | `dte:emit` y `dte:invalidate` | | `dte_retorno` | Evento de retorno (requiere `idempotencyKey`) | `dte:emit` | | `operaciones_especiales_informar` | Evento 17 (requiere `idempotencyKey`) | `dte:emit` | | `eventos_listar` | Lista eventos de retorno y operaciones especiales | `dte:read` | | `evento_obtener` | Un evento con su JSON y la respuesta del MH | `dte:read` | ## Cómo debe trabajar el asistente 1. `emisor_obtener` para saber el ambiente. En `01`, avisarte que emitirá **en producción**. 2. Buscar receptor y productos; no adivinar NIT, NRC ni actividad económica. 3. `dte_validar` y corregir los problemas. 4. Mostrarte el resumen (tipo, receptor, líneas, total, ambiente) y esperar un "sí" explícito. 5. `dte_emitir` con una `idempotencyKey`, y reutilizarla si reintenta. Las [habilidades para agentes](https://docs.openfactura.co/herramientas/habilidades/) enseñan exactamente este flujo. > **Precaución** > > Con una clave `of_live_…`, `dte_emitir`, `dte_retorno`, `operaciones_especiales_informar` y > `dte_invalidar` tienen efecto fiscal. No hay confirmación de la CLI: la das tú al aprobar la llamada > en tu cliente MCP. --- # CLI > openfactura, la línea de comandos para emitir, consultar e invalidar DTE desde la terminal, scripts e integración continua. `@openfactura/cli` (MIT) habla con Open Factura sólo por su API REST: no firma ni transmite por su cuenta y no necesita el certificado de Hacienda en tu equipo. Requiere Node.js 20 o superior (también funciona con Bun). ```bash npm i -g @openfactura/cli openfactura --version # o sin instalar npx @openfactura/cli --help ``` ## Iniciar sesión ```bash openfactura login --url https://app.openfactura.co # Clave de API (of_test_… u of_live_…): ← se teclea sin mostrarse ``` `login` verifica la clave y la guarda en `~/.config/openfactura/config.json` con permisos `0600`. Otras formas: ```bash echo "$CLAVE" | openfactura login --url https://app.openfactura.co # desde una tubería openfactura login --profile produccion # otro perfil openfactura logout ``` En tu propio servidor (sin panel), el operador crea la organización y una clave de pruebas con el `ADMIN_TOKEN` del servidor; la clave queda guardada como con `login` y el token no se guarda: ```bash OPENFACTURA_ADMIN_TOKEN=… openfactura alta "Mi Empresa" --url https://TU-DOMINIO ``` En integración continua basta con variables de entorno: ```bash export OPENFACTURA_URL=https://app.openfactura.co export OPENFACTURA_API_KEY=of_test_... openfactura whoami ``` | Valor | Prioridad | | ---------------- | ---------------------------------------------------------------------------- | | URL del servidor | `--url` → `OPENFACTURA_URL` → perfil guardado → `https://app.openfactura.co` | | Clave de API | `OPENFACTURA_API_KEY` → perfil guardado | | Perfil | `--profile` → `OPENFACTURA_PROFILE` → `default` | ## Comandos Todos aceptan `--json` (el JSON de la API en stdout), `--url`, `--profile` y `--help`. ```bash openfactura whoami # actor, permisos, ambiente openfactura emisor # datos fiscales, establecimientos, credenciales cargadas openfactura clientes list --q "contribuyente" openfactura clientes add --file cliente.json openfactura productos list --q licencia openfactura productos add --codigo LIC-001 --descripcion "Licencia anual" --precio 100 --tipo-item 2 openfactura catalogos CAT-013 --departamento 06 openfactura dte validar factura.json openfactura dte emitir factura.json [--idempotency-key ] openfactura dte list --tipo 01 --estado procesado --desde 2026-11-01 --hasta 2026-11-30 openfactura dte get openfactura dte json -o dte.json openfactura dte pdf -o factura.pdf openfactura dte invalidar --tipo 2 --motivo "Operación rescindida" openfactura dte retorno retorno.json openfactura eventos operaciones-especiales simplificadas.json openfactura eventos list --tipo retorno openfactura eventos get|json|pdf openfactura contingencia transmitir openfactura certificacion estado # sólo en Open Factura Cloud openfactura certificacion ejecutar # emite las pruebas mínimas (Open Factura Cloud) ``` El archivo de `dte emitir` es el mismo cuerpo de `POST /api/v1/dte` (ve [Emitir documentos](https://docs.openfactura.co/guias/emitir/)). `-` lee de la entrada estándar. > **Nota** > > Con una clave `of_live_…`, `dte emitir`, `dte retorno`, `eventos operaciones-especiales`, > `dte invalidar` y `contingencia transmitir` piden confirmación. Usa `--yes` para omitirla; sin una > terminal interactiva, `--yes` es obligatorio. ## En scripts ```bash codigo=$(openfactura dte emitir factura.json --json | jq -r .codigoGeneracion) openfactura dte pdf "$codigo" -o "facturas/$codigo.pdf" ``` Los errores van a stderr como `Error [codigo]: mensaje`; con `--json`, stderr recibe el JSON de error de la API. | Salida | Significado | | ------ | ----------------------------------------------------------------- | | `0` | Todo bien | | `1` | Error de la API o de validación (incluye errores de red) | | `2` | Uso incorrecto: opción inválida, archivo ilegible, falta `--yes` | | `3` | Sin autenticar: falta la clave, o es inválida, revocada o expiró | | `4` | Límite de peticiones excedido | La CLI también incluye un [servidor MCP por stdio](https://docs.openfactura.co/herramientas/mcp/): `openfactura mcp`. --- # Habilidades para agentes > Instala las habilidades de Open Factura (skills.sh) para que Claude Code, Cursor, Codex y otros agentes facturen con cuidado. Las habilidades son instrucciones empaquetadas (MIT) que enseñan a un agente de programación o asistente cómo usar Open Factura: qué preguntar, qué validar y cuándo pedir confirmación. Se distribuyen por [skills.sh](https://skills.sh/xaviigna/openfactura). ```bash npx skills add xaviigna/openfactura # las cuatro npx skills add xaviigna/openfactura --skill openfactura-facturar # sólo una ``` | Habilidad | Qué enseña | | ---------------------- | ------------------------------------------------------------------------------------------- | | `openfactura-conectar` | Conectar el MCP o la CLI, elegir una clave `of_test_`/`of_live_` y permisos mínimos | | `openfactura-facturar` | Validar, confirmar y emitir; qué tipo de DTE corresponde; IVA en los precios; retornos | | `openfactura-errores` | Qué hacer con cada código de error, contingencia, emisión incierta e invalidación con plazos | | `openfactura-webhooks` | Configurar webhooks, implementar el endpoint que los recibe y verificar la firma | ## Reglas que aplican los agentes - Validar antes de emitir y corregir cada problema. - Confirmar contigo el resumen (tipo, receptor, líneas, total y ambiente) antes de emitir. - No calcular impuestos: mostrar el total que devuelve Open Factura. - Una `idempotencyKey` por intento, reutilizada en los reintentos. - No adivinar datos fiscales: buscarlos en clientes guardados o preguntarlos. - Decidir por el `code` del error, nunca por el texto del mensaje. Las habilidades funcionan junto con el [servidor MCP](https://docs.openfactura.co/herramientas/mcp/) o la [CLI](https://docs.openfactura.co/herramientas/cli/). --- # SDK @openfactura/dte > SDK en TypeScript (MIT) para construir, validar, firmar y transmitir DTE directamente al MH, sin Java ni Docker. `@openfactura/dte` es el motor de Open Factura, publicado aparte con licencia MIT para integrarlo en cualquier sistema. Construye, valida, firma y transmite DTE según la Normativa 2.0 y el Manual Técnico v2 del MH. La firma usa WebCrypto: corre en Node 20+, Cloudflare Workers, Deno y Bun. > **Nota** > > Si usas la API de Open Factura no necesitas el SDK: el campo `datos` de `POST /dte` es exactamente la > entrada de sus constructores. Usa el SDK si quieres firmar y transmitir con tus propias credenciales, > sin pasar por Open Factura. ```ts import { ClienteMH, construirFactura, emitirDte } from '@openfactura/dte' import { generarPdfDte } from '@openfactura/dte/pdf' const cliente = new ClienteMH({ ambiente: '00', usuario: NIT, password: PASSWORD_API }) const dte = construirFactura( { ambiente: '00', emisor, correlativo: 1 }, { items: [{ descripcion: 'Café 340 g', cantidad: 2, precioUni: 9.5 }] }, // precios con IVA ) const { archivo } = await emitirDte(cliente, dte, { certificado: crtXml, password: CLAVE_PRIVADA }) archivo.selloRecibido // sello de recepción del MH await generarPdfDte(archivo) // representación gráfica con QR ``` ## Qué incluye | Área | Detalle | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Constructores | Factura (01), Crédito Fiscal (03), Remisión (04), Notas (05/06), Retención (07), Liquidación (08), Doc. Contable de Liquidación (09), Exportación (11), Sujeto Excluido (14), Donación (15); eventos de invalidación, contingencia, retorno (18) y operaciones especiales (17) | | Validación | Los 15 JSON Schemas oficiales y los catálogos CAT-001…CAT-033, con un validador sin `eval` apto para Workers | | Firma | JWS RS512 compatible con el firmador oficial; lee el `.crt` del MH | | Transmisión | Autenticación, recepción individual y por lotes, consultas, invalidación, contingencia y la política de reintentos del Manual Técnico | | Extras | Número de control, código de generación, URL y QR de consulta, plazos, PDF y un MH simulado (`@openfactura/dte/mock`) para desarrollo y CI | ## Reglas de cálculo - Cuerpo a 8 decimales y resumen a 2, redondeo medio hacia arriba. - Factura: precios **con** IVA. Crédito Fiscal, notas y remisión: precios **sin** IVA. - Descuentos por ítem y globales, retención y percepción de IVA del 1 %, retención de renta del 10 % en sujeto excluido. - NIT, DUI y NRC se envían sin guiones; toda dirección lleva `distrito` (CAT-008). Código y pruebas: [`packages/dte`](https://github.com/xaviigna/openfactura/tree/main/packages/dte). --- # Ser emisor ante Hacienda > El recorrido para emitir DTE con valor legal en El Salvador, de la solicitud de pruebas a la autorización en producción. > **Precaución** > > Este es un resumen para desarrolladores, no asesoría legal ni tributaria. Fue elaborado a partir de > los manuales del Ministerio de Hacienda; ante cualquier diferencia prevalecen los documentos oficiales > publicados en [factura.gob.sv](https://factura.gob.sv). Open Factura se integra con el **Sistema de Transmisión** del MH: tu software (en este caso, Open Factura) envía los documentos por API. La alternativa, el Sistema de Facturación, es el portal gratuito de Hacienda para quienes emiten 500 documentos mensuales o menos en promedio. ## El recorrido 1. **Requisitos previos.** Usuario de los servicios en línea de la DGII (NIT y contraseña) y la lista de documentos que vas a emitir: todos los que tu operación requiere. Los eventos de invalidación y contingencia son obligatorios en el Sistema de Transmisión. 2. **Solicitud de ingreso al ambiente de pruebas.** En factura.gob.sv → Ingresar → Solicitud de Ingreso, elige **Sistema de Transmisión** y registra un contacto. Desde que ingresas tienes **2 meses** para completar las pruebas, un solo plazo para todos los documentos. 3. **Acreditamiento en pruebas.** En el Sitio Emisores DTE (ambiente para pruebas) personaliza las claves del certificado y la **contraseña de API**, y descarga el certificado `.crt`. El usuario de la API es tu NIT. Cárgalos en Open Factura: **Emisor → Credenciales del MH**, ambiente `00`. 4. **Pruebas mínimas.** Transmite con éxito (con sello) la cantidad mínima de cada documento que vayas a emitir, más 5 eventos de invalidación y 5 de contingencia (y de retorno y operaciones especiales, si los usarás). El Sitio Emisores DTE muestra el avance. Open Factura emite por ti las de Factura, Crédito Fiscal, invalidación y contingencia: ve [Certificación](https://docs.openfactura.co/guias/certificacion/). 5. **Autorización.** En el Sitio Emisores DTE → Solicitar Autorización, elige los documentos que ya cumplieron las pruebas y presenta la solicitud. La autorización llega por correo. 6. **Acreditamiento en producción.** Repite el acreditamiento en el ambiente productivo: el certificado y la contraseña de API de producción son distintos. Cárgalos en Open Factura para el ambiente `01` y cambia a producción (ve [Ambientes](https://docs.openfactura.co/ambientes/)). Tienes hasta un mes desde el día siguiente a la autorización para empezar a emitir. ## Pruebas mínimas por documento | Documento | Mínimo | Documento | Mínimo | | ---------------------------- | ------ | ------------------------------- | ------ | | Factura (01) | 90 | Comprobante de Retención (07) | 50 | | Crédito Fiscal (03) | 75 | Factura de Exportación (11) | 90 | | Nota de Remisión (04) | 50 | Sujeto Excluido (14) | 25 | | Nota de Crédito (05) | 50 | Invalidación | 5 | | Nota de Débito (06) | 25 | Contingencia | 5 | | Retorno | 5 | Operaciones especiales | 5 | Sólo cuentan los documentos que obtuvieron sello de recepción, y sólo hay que probar los tipos que solicitarás. ## Qué cambia en producción | Elemento | Pruebas | Producción | | -------------------------------- | -------------- | ---------------------------- | | Ambiente | `00` | `01` | | Certificado y contraseña de API | Los de pruebas | Los del ambiente productivo | | Clave de Open Factura | `of_test_…` | `of_live_…` | | Leyenda "sin valor legal" en el PDF | Sí | No | El resumen completo, con las fuentes de cada paso, está en [`docs/ser-emisor.md`](https://github.com/xaviigna/openfactura/blob/main/docs/ser-emisor.md). --- # Certificación ante Hacienda > Emite en un clic las pruebas mínimas que exige el Ministerio de Hacienda para autorizarte como emisor de DTE. > **Disponible en Open Factura Cloud** > > La certificación automática es parte de [Open Factura Cloud](https://openfactura.co). Si te > autoalojas, emite las pruebas mínimas por tu cuenta con la API o la CLI y lleva el conteo en el > Sitio Emisores DTE de Hacienda. Para autorizarte como emisor, Hacienda exige que transmitas con éxito un mínimo de documentos en el ambiente de pruebas, dentro de los 2 meses de tu plazo. Open Factura los emite por ti: | Prueba | Mínimo | | ----------------------------- | ------ | | Factura (01) | 90 | | Comprobante de Crédito Fiscal (03) | 75 | | Evento de invalidación | 5 | | Evento de contingencia | 5 | Son las obligatorias para todo emisor del Sistema de Transmisión. Si vas a emitir otros documentos, al terminar pulsa **Certificar también los demás documentos**: emite además las pruebas de Nota de Remisión (50), Nota de Crédito (50), Nota de Débito (25), Comprobante de Retención (50), Comprobante de Liquidación (75), Documento Contable de Liquidación (50), Factura de Exportación (90), Sujeto Excluido (25), Donación (25) y los eventos de retorno (5) y operaciones especiales (5). Así puedes solicitar la autorización de todo de una vez. ## Antes de empezar - Las **credenciales de pruebas** de Hacienda cargadas (certificado y contraseña de API). - Un **cliente contribuyente** (con NIT y NRC) que reciba los Créditos Fiscales de prueba. - El **responsable** del emisor: Hacienda lo exige en los eventos de invalidación y contingencia. La página te lo pide si falta. ## Cómo funciona 1. **Ensayo.** Emite una Factura, un Crédito Fiscal, una invalidación y un ciclo de contingencia. Si algo está mal, aparece aquí y no en el documento 60. El panel se pausa al terminar el ensayo para que compruebes el contador del Sitio Emisores DTE. 2. **Volumen.** Emite el resto: hasta 90 Facturas y 75 Créditos Fiscales, y luego las invalidaciones y las contingencias que falten. Una Factura invalidada sigue contando, igual que en el contador de Hacienda. 3. **Si Hacienda rechaza una prueba, se detiene** y te muestra el código y las observaciones del MH. No gasta más números de control. Corrige la causa y reanuda: continúa donde quedó. 4. **Al completar**, entra al Sitio Emisores DTE, comprueba el contador y abre **Solicitar Autorización**. Los Créditos Fiscales de prueba necesitan como receptor a **otro contribuyente**: Hacienda rechaza los que van dirigidos a tu propia empresa (`[receptor.nit] VALOR NO VALIDO`). Crea en **Clientes** uno con NIT, NRC, actividad económica y dirección, y elígelo antes de iniciar. #### Panel Abre **Certificación** y pulsa **Iniciar**. Puedes pausar y reanudar cuando quieras. #### CLI ```bash openfactura certificacion ejecutar # lo obligatorio openfactura certificacion ejecutar --completo # todos los documentos y eventos openfactura certificacion estado ``` `ejecutar` sale con 0 al completar y con 1 si Hacienda rechazó una prueba o dejó de responder. #### API ```bash curl -X POST https://app.openfactura.co/api/v1/certificacion/iniciar \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" \ -H "Content-Type: application/json" -d '{}' # Repite mientras "estado" sea "en_curso": curl -X POST https://app.openfactura.co/api/v1/certificacion/avanzar \ -H "Authorization: Bearer $OPENFACTURA_API_KEY" ``` La clave necesita `dte:read`, `dte:emit` y `dte:invalidate`. #### MCP Pide a tu agente: «certifícame ante Hacienda». Usa `certificacion_avanzar` hasta completar y te muestra el rechazo si lo hay. > **Nota** > > El avance que ves cuenta lo que Open Factura transmitió con sello desde que iniciaste. **El contador > oficial es el del Sitio Emisores DTE.** Si Hacienda te abre un plazo nuevo, usa «Empezar desde cero». > **Precaución** > > Con las credenciales del simulador todo funciona igual, pero es un **simulacro**: nada llega a > Hacienda ni cuenta para tu autorización. --- # Autoalojamiento > Ejecuta la API de Open Factura en tu equipo con el simulador del MH o despliégala en tu propia cuenta de Cloudflare. Open Factura es de código abierto ([AGPL-3.0](https://github.com/xaviigna/openfactura/blob/main/LICENSE) para el servidor; MIT para el SDK, la CLI y las habilidades). Corre como un solo Worker de Cloudflare con una base D1: API REST, MCP, webhooks y conciliación. > **Sin panel** > > Lo autoalojado no trae panel web: se administra con la API, la CLI o tu agente. El panel, el alta > guiada y la certificación automática son parte de [Open Factura Cloud](https://openfactura.co). ## En tu equipo Requisitos: [Bun](https://bun.sh) 1.2+ y Node 20+. ```bash git clone https://github.com/xaviigna/openfactura && cd openfactura bun install cp server/.dev.vars.example server/.dev.vars # completa los secretos (openssl rand -base64 32/48) echo "MH_SIMULADO=1" >> server/.dev.vars # simulador del MH: emite sin credenciales de Hacienda echo "WEBHOOKS_LOCAL=1" >> server/.dev.vars # webhooks a http://localhost (sólo desarrollo) bun run build cd server && bun run db:migrate:local && cd .. bun run dev # API y MCP en :8788 ``` Crea tu organización y una clave de pruebas, y conecta el simulador: ```bash export OPENFACTURA_URL=http://localhost:8788 export OPENFACTURA_ADMIN_TOKEN= npx @openfactura/cli alta "Mi Empresa" # imprime el id de la organización curl -X PUT $OPENFACTURA_URL/api/v1/emisor -H "Authorization: Bearer $OPENFACTURA_ADMIN_TOKEN" \ -H "X-Organizacion: " -H 'Content-Type: application/json' -d @emisor.json curl -X POST $OPENFACTURA_URL/api/v1/emisor/credenciales-prueba \ -H "Authorization: Bearer $OPENFACTURA_ADMIN_TOKEN" -H "X-Organizacion: " npx @openfactura/cli dte emitir factura.json # {"tipoDte":"01","datos":{"items":[…]}} ``` En los ejemplos de esta documentación cambia `https://app.openfactura.co` (y `https://openfactura.co/mcp`) por `http://localhost:8788` (y `http://localhost:8788/mcp`). ## En tu cuenta de Cloudflare > **Precaución** > > El plan gratuito de Workers limita la CPU a 10 ms por petición. Firmar (RSA) y generar el PDF superan > ese límite: usa el plan de pago de Workers. 1. **Base de datos.** ```bash cd server bunx wrangler d1 create openfactura ``` Copia el `database_id` a `server/wrangler.jsonc` y aplica las migraciones con `bun run db:migrate:remote`. 2. **Configuración.** En `server/wrangler.jsonc` cambia `vars.APP_URL` por tu URL pública. Luego los secretos: ```bash openssl rand -base64 32 | bunx wrangler secret put BETTER_AUTH_SECRET openssl rand -base64 32 | bunx wrangler secret put CRED_KEK_V1 openssl rand -base64 48 | tee admin-token.txt | bunx wrangler secret put ADMIN_TOKEN ``` `ADMIN_TOKEN` es tu credencial de operador: crea organizaciones y administra el emisor, sus credenciales del MH y las claves de API. Guárdalo como una contraseña. `CRED_KEK_V1` cifra las credenciales del MH de todos los emisores. **Guarda una copia fuera de Cloudflare.** No definas `WEBHOOKS_LOCAL` en producción. 3. **Desplegar.** ```bash cd .. && bun run deploy ``` 4. **Primer uso.** `npx @openfactura/cli alta "Mi Empresa" --url https://TU-DOMINIO`, y con `ADMIN_TOKEN` + `X-Organizacion`: `PUT /api/v1/emisor`, `PUT /api/v1/emisor/credenciales/00` (contraseña de API, certificado `.crt` y su contraseña) y `POST /api/v1/emisor/probar`. Paso a paso en [`docs/self-hosting.md`](https://github.com/xaviigna/openfactura/blob/main/docs/self-hosting.md). ## Variables | Nombre | Tipo | Descripción | | ------------------------- | ------- | -------------------------------------------------------------------- | | `APP_URL` | var | URL pública del servicio | | `BETTER_AUTH_SECRET` | secreto | Firma de sesiones | | `ADMIN_TOKEN` | secreto | Credencial del operador (32+ caracteres); sin ella no hay administración | | `CRED_KEK_V1` (`V2`, …) | secreto | Llave maestra AES-256, 32 bytes en base64 | | `LIMITE_CLAVE_POR_MINUTO` | var | Peticiones por minuto por clave de API (120) | | `TRUSTED_ORIGINS` | var | Orígenes adicionales de confianza, separados por coma | | `MH_BASE_URL` | var | Sustituye la URL del MH (sólo para pruebas) | | `MH_SIMULADO` | var | `1` permite `POST /emisor/credenciales-prueba` (simulador del MH, ambiente `00`) | | `WEBHOOKS_LOCAL` | var | `1` permite webhooks a `http://localhost` (sólo desarrollo) | ## Rotar la llave maestra 1. `bunx wrangler secret put CRED_KEK_V2` con una llave nueva. Lo nuevo se cifra con V2; lo anterior se sigue descifrando con V1. 2. Vuelve a guardar las credenciales de cada emisor. 3. Cuando ningún valor empiece por `v1.`, elimina `CRED_KEK_V1`. El documento OpenAPI de tu instalación queda en `https://TU-DOMINIO/api/v1/openapi.json`. --- # Errores > El formato común de errores y qué hacer con cada código. Todo error tiene la misma forma: ```json { "error": { "code": "dte_invalido", "message": "…", "details": {} } } ``` Decide siempre por `code`, nunca por el texto de `message`. Los códigos son contrato público y no cambian de nombre. ## Códigos | HTTP | `code` | Qué pasó | Qué hacer | | ---- | ------------------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | 400 | `datos_invalidos` | Cuerpo o parámetros inválidos; `details` = `[{ ruta, mensaje }]` | Corrige cada ruta y repite. | | 400 | `dte_invalido` | El DTE no cumple el esquema del MH; `details.problemas` = `[{ ruta, mensaje, regla }]`. No se emitió nada | Corrige y vuelve a validar. | | 401 | `no_autenticado` | Falta sesión, o la clave es inválida, fue revocada o expiró | Revisa la clave o crea otra. | | 402 | `plan_requerido` | El plan de la cuenta no incluye la operación (sólo en la nube) | Mejora tu plan; no se consumió número de control. | | 403 | `sin_permiso` | Falta el permiso `details.permiso` | Usa una clave con ese permiso. | | 403 | `origen_no_permitido` | Petición con cookie desde otro origen | Usa el panel o una clave de API. | | 404 | `no_encontrado` | El recurso no existe en tu organización | Revisa el id o el código. | | 409 | `sin_organizacion`, `sin_organizacion_activa` | La sesión no tiene organización activa | Crea o activa una organización. | | 409 | `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta` | Falta configuración | Complétala en el panel (**Emisor**). | | 409 | `nit_bloqueado` | El NIT del emisor no puede cambiarse después de emitir documentos | Crea otra organización para otro NIT. | | 409 | `estado_invalido` | El documento no está en un estado que permita la operación | Consulta el documento. | | 409 | `ambiente_distinto` | El documento o evento es de otro ambiente | Usa la clave del ambiente correcto. | | 409 | `retorno_excedido` | El retorno supera lo que queda del documento | `details.disponible` dice cuánto queda. | | 409 | `retorno_vigente` | El documento tiene un retorno vigente | Invalida antes el retorno (`details.codigoGeneracion`). | | 409 | `idempotencia_distinta` | Esa `Idempotency-Key` ya se usó con otro cuerpo | Otra clave para otro documento; el mismo cuerpo para el mismo. | | 409 | `idempotencia_en_curso` | El primer intento sigue en proceso | Espera y repite con la misma clave. | | 409 | `cliente_repetido`, `producto_repetido`, `codigo_repetido` | Ya existe un registro con ese documento o código | Actualiza el existente. | | 409 | `sin_archivo` | El documento sigue pendiente y aún no tiene archivo | Consulta de nuevo en unos minutos. | | 409 | `simulador_inactivo` | Credenciales de prueba pedidas a un servidor sin simulador | Carga credenciales reales del MH. | | 422 | `dte_rechazado` | El MH rechazó el documento; el número de control queda como rechazado | Muestra `details.mh.observaciones`, corrige y emite uno nuevo con otra `Idempotency-Key`. | | 422 | `invalidacion_rechazada` | El MH rechazó la invalidación (a menudo, fuera de plazo) | Revisa `details.mh` y los [plazos](https://docs.openfactura.co/guias/invalidacion/). | | 422 | `contingencia_rechazada` | El MH rechazó el evento de contingencia | Revisa `details.mh`. | | 422 | `evento_rechazado` | El MH rechazó un retorno u operaciones especiales | Corrige y reenvía con otra `Idempotency-Key`. | | 429 | `limite_excedido` | Demasiadas peticiones | Espera `Retry-After` segundos. | | 429 | `limite_plan` | Se alcanzó el límite del plan, p. ej. DTE del mes (sólo en la nube) | Mejora tu plan o espera al siguiente periodo; no se consumió número de control. | | 500 | `error_interno` | Error inesperado del servidor | Reintenta; si persiste, repórtalo. | | 502 | `mh_credenciales_rechazadas` | El MH no aceptó la contraseña de API o el certificado | Revisa **Emisor → Credenciales del MH**. | | 502 | `mh_no_disponible` | No se pudo hablar con el MH; no se consumió número de control | Reintenta con la misma `Idempotency-Key`. | | 502 | `emision_fallida` | No se pudo firmar; el documento quedó `rechazado` sin transmitirse | Revisa el certificado y emite de nuevo. | | 502 | `emision_incierta` | No se pudo confirmar el resultado; el documento queda `pendiente` | **No emitas otro.** Consulta `details.codigoGeneracion`; se concilia solo con el MH. | En `details.mh` el MH informa `{ codigo, mensaje, observaciones[] }`. > **Nota** > > La lista completa de códigos también está en el esquema `Error` de la > [referencia de la API](https://docs.openfactura.co/api/). --- # API de Open Factura > Facturación electrónica de El Salvador (DTE, Normativa 2.0). API REST de Open Factura. Todo es JSON. Autentícate con una clave de API (`Authorization: Bearer of_test_…` para el ambiente de pruebas `00`, `of_live_…` para producción `01`) o con la cookie de sesión del panel. Errores: `{ "error": { "code", "message", "details" } }`; decide siempre por `code`. Límite: 120 peticiones por minuto por clave, con encabezados `RateLimit-*`. URL base: `https://app.openfactura.co/api/v1` ## Sesión Identidad del actor y tablero. - [Documento OpenAPI 3.1 de esta API](https://docs.openfactura.co/api/operations/openapi_obtener.md): `GET /openapi.json` - [Quién hace la petición](https://docs.openfactura.co/api/operations/yo_obtener.md): `GET /yo` - [Resumen del tablero](https://docs.openfactura.co/api/operations/resumen_obtener.md): `GET /resumen` - [Uso por día](https://docs.openfactura.co/api/operations/uso_obtener.md): `GET /uso` ## Emisor Datos fiscales, establecimientos, puntos de venta y credenciales del MH. - [Datos del emisor](https://docs.openfactura.co/api/operations/emisor_obtener.md): `GET /emisor` - [Crear o actualizar el emisor](https://docs.openfactura.co/api/operations/emisor_guardar.md): `PUT /emisor` - [Guardar credenciales del MH](https://docs.openfactura.co/api/operations/emisor_credenciales_guardar.md): `PUT /emisor/credenciales/{ambiente}` - [Generar credenciales de prueba (simulador)](https://docs.openfactura.co/api/operations/emisor_credenciales_prueba.md): `POST /emisor/credenciales-prueba` - [Probar la conexión con el MH](https://docs.openfactura.co/api/operations/emisor_probar.md): `POST /emisor/probar` - [Listar establecimientos](https://docs.openfactura.co/api/operations/establecimientos_listar.md): `GET /establecimientos` - [Crear un establecimiento](https://docs.openfactura.co/api/operations/establecimiento_crear.md): `POST /establecimientos` - [Actualizar un establecimiento](https://docs.openfactura.co/api/operations/establecimiento_actualizar.md): `PUT /establecimientos/{id}` - [Crear un punto de venta](https://docs.openfactura.co/api/operations/punto_venta_crear.md): `POST /puntos-venta` ## Clientes Receptores guardados. - [Buscar clientes](https://docs.openfactura.co/api/operations/clientes_listar.md): `GET /clientes` - [Crear un cliente](https://docs.openfactura.co/api/operations/cliente_crear.md): `POST /clientes` - [Obtener un cliente](https://docs.openfactura.co/api/operations/cliente_obtener.md): `GET /clientes/{id}` - [Actualizar un cliente](https://docs.openfactura.co/api/operations/cliente_actualizar.md): `PUT /clientes/{id}` - [Eliminar un cliente](https://docs.openfactura.co/api/operations/cliente_eliminar.md): `DELETE /clientes/{id}` ## Productos Catálogo de productos y servicios (precios sin IVA). - [Buscar productos](https://docs.openfactura.co/api/operations/productos_listar.md): `GET /productos` - [Crear un producto](https://docs.openfactura.co/api/operations/producto_crear.md): `POST /productos` - [Obtener un producto](https://docs.openfactura.co/api/operations/producto_obtener.md): `GET /productos/{id}` - [Actualizar un producto](https://docs.openfactura.co/api/operations/producto_actualizar.md): `PUT /productos/{id}` - [Eliminar un producto](https://docs.openfactura.co/api/operations/producto_eliminar.md): `DELETE /productos/{id}` ## DTE Emisión, consulta, invalidación y contingencia de documentos. - [Listar documentos](https://docs.openfactura.co/api/operations/dte_listar.md): `GET /dte` - [Emitir un DTE](https://docs.openfactura.co/api/operations/dte_emitir.md): `POST /dte` - [Validar sin emitir](https://docs.openfactura.co/api/operations/dte_validar.md): `POST /dte/validar` - [Obtener un documento](https://docs.openfactura.co/api/operations/dte_obtener.md): `GET /dte/{codigo}` - [Archivo DTE oficial (JSON)](https://docs.openfactura.co/api/operations/dte_json.md): `GET /dte/{codigo}/json` - [Representación gráfica (PDF)](https://docs.openfactura.co/api/operations/dte_pdf.md): `GET /dte/{codigo}/pdf` - [Invalidar un documento o un evento](https://docs.openfactura.co/api/operations/dte_invalidar.md): `POST /dte/{codigo}/invalidar` - [Transmitir documentos en contingencia](https://docs.openfactura.co/api/operations/contingencia_transmitir.md): `POST /contingencia/transmitir` ## Eventos Evento de Retorno (18) y Operaciones Especiales (17). - [Evento de Retorno (18)](https://docs.openfactura.co/api/operations/dte_retorno.md): `POST /dte/{codigo}/retorno` - [Evento de Operaciones Especiales (17)](https://docs.openfactura.co/api/operations/operaciones_especiales_informar.md): `POST /eventos/operaciones-especiales` - [Listar eventos](https://docs.openfactura.co/api/operations/eventos_listar.md): `GET /eventos` - [Obtener un evento](https://docs.openfactura.co/api/operations/evento_obtener.md): `GET /eventos/{codigo}` - [Archivo del evento (JSON)](https://docs.openfactura.co/api/operations/evento_json.md): `GET /eventos/{codigo}/json` - [Representación gráfica del evento (PDF)](https://docs.openfactura.co/api/operations/evento_pdf.md): `GET /eventos/{codigo}/pdf` ## Catálogos Catálogos oficiales del MH (CAT-xxx). - [Listar catálogos del MH](https://docs.openfactura.co/api/operations/catalogos_listar.md): `GET /catalogos` - [Consultar un catálogo](https://docs.openfactura.co/api/operations/catalogo_buscar.md): `GET /catalogos/{id}` ## Claves de API Claves `of_test_…` y `of_live_…`. Sólo desde una sesión del panel. - [Listar claves de API](https://docs.openfactura.co/api/operations/claves_listar.md): `GET /claves` - [Crear una clave de API](https://docs.openfactura.co/api/operations/clave_crear.md): `POST /claves` - [Revocar una clave](https://docs.openfactura.co/api/operations/clave_revocar.md): `DELETE /claves/{id}` ## Webhooks Notificaciones firmadas (HMAC-SHA256) cuando cambia el estado de un documento o evento. - [Listar webhooks](https://docs.openfactura.co/api/operations/webhooks_listar.md): `GET /webhooks` - [Crear un webhook](https://docs.openfactura.co/api/operations/webhook_crear.md): `POST /webhooks` - [Eliminar un webhook y sus entregas](https://docs.openfactura.co/api/operations/webhook_eliminar.md): `DELETE /webhooks/{id}` - [Cambiar URL, descripción, eventos o activarlo/desactivarlo](https://docs.openfactura.co/api/operations/webhook_actualizar.md): `PATCH /webhooks/{id}` - [Generar un secreto nuevo](https://docs.openfactura.co/api/operations/webhook_rotar_secreto.md): `POST /webhooks/{id}/rotar-secreto` - [Enviar un evento `webhook.prueba` ahora](https://docs.openfactura.co/api/operations/webhook_probar.md): `POST /webhooks/{id}/probar` - [Últimas 50 entregas de un webhook](https://docs.openfactura.co/api/operations/webhook_entregas.md): `GET /webhooks/{id}/entregas` - [Reenviar una entrega ahora](https://docs.openfactura.co/api/operations/webhook_entrega_reenviar.md): `POST /webhooks/entregas/{id}/reenviar` ## Auditoría Bitácora de acciones de la organización. - [Bitácora de auditoría](https://docs.openfactura.co/api/operations/auditoria_listar.md): `GET /auditoria` ## Registros Peticiones hechas con claves de API (sólo metadatos; 30 días). - [Registros de peticiones de la API](https://docs.openfactura.co/api/operations/registros_listar.md): `GET /registros` --- # Sesión > Identidad del actor y tablero. - [Documento OpenAPI 3.1 de esta API](https://docs.openfactura.co/api/operations/openapi_obtener.md): `GET /openapi.json` - [Quién hace la petición](https://docs.openfactura.co/api/operations/yo_obtener.md): `GET /yo` - [Resumen del tablero](https://docs.openfactura.co/api/operations/resumen_obtener.md): `GET /resumen` - [Uso por día](https://docs.openfactura.co/api/operations/uso_obtener.md): `GET /uso` --- # Emisor > Datos fiscales, establecimientos, puntos de venta y credenciales del MH. - [Datos del emisor](https://docs.openfactura.co/api/operations/emisor_obtener.md): `GET /emisor` - [Crear o actualizar el emisor](https://docs.openfactura.co/api/operations/emisor_guardar.md): `PUT /emisor` - [Guardar credenciales del MH](https://docs.openfactura.co/api/operations/emisor_credenciales_guardar.md): `PUT /emisor/credenciales/{ambiente}` - [Generar credenciales de prueba (simulador)](https://docs.openfactura.co/api/operations/emisor_credenciales_prueba.md): `POST /emisor/credenciales-prueba` - [Probar la conexión con el MH](https://docs.openfactura.co/api/operations/emisor_probar.md): `POST /emisor/probar` - [Listar establecimientos](https://docs.openfactura.co/api/operations/establecimientos_listar.md): `GET /establecimientos` - [Crear un establecimiento](https://docs.openfactura.co/api/operations/establecimiento_crear.md): `POST /establecimientos` - [Actualizar un establecimiento](https://docs.openfactura.co/api/operations/establecimiento_actualizar.md): `PUT /establecimientos/{id}` - [Crear un punto de venta](https://docs.openfactura.co/api/operations/punto_venta_crear.md): `POST /puntos-venta` --- # Clientes > Receptores guardados. - [Buscar clientes](https://docs.openfactura.co/api/operations/clientes_listar.md): `GET /clientes` - [Crear un cliente](https://docs.openfactura.co/api/operations/cliente_crear.md): `POST /clientes` - [Obtener un cliente](https://docs.openfactura.co/api/operations/cliente_obtener.md): `GET /clientes/{id}` - [Actualizar un cliente](https://docs.openfactura.co/api/operations/cliente_actualizar.md): `PUT /clientes/{id}` - [Eliminar un cliente](https://docs.openfactura.co/api/operations/cliente_eliminar.md): `DELETE /clientes/{id}` --- # Productos > Catálogo de productos y servicios (precios sin IVA). - [Buscar productos](https://docs.openfactura.co/api/operations/productos_listar.md): `GET /productos` - [Crear un producto](https://docs.openfactura.co/api/operations/producto_crear.md): `POST /productos` - [Obtener un producto](https://docs.openfactura.co/api/operations/producto_obtener.md): `GET /productos/{id}` - [Actualizar un producto](https://docs.openfactura.co/api/operations/producto_actualizar.md): `PUT /productos/{id}` - [Eliminar un producto](https://docs.openfactura.co/api/operations/producto_eliminar.md): `DELETE /productos/{id}` --- # DTE > Emisión, consulta, invalidación y contingencia de documentos. - [Listar documentos](https://docs.openfactura.co/api/operations/dte_listar.md): `GET /dte` - [Emitir un DTE](https://docs.openfactura.co/api/operations/dte_emitir.md): `POST /dte` - [Validar sin emitir](https://docs.openfactura.co/api/operations/dte_validar.md): `POST /dte/validar` - [Obtener un documento](https://docs.openfactura.co/api/operations/dte_obtener.md): `GET /dte/{codigo}` - [Archivo DTE oficial (JSON)](https://docs.openfactura.co/api/operations/dte_json.md): `GET /dte/{codigo}/json` - [Representación gráfica (PDF)](https://docs.openfactura.co/api/operations/dte_pdf.md): `GET /dte/{codigo}/pdf` - [Invalidar un documento o un evento](https://docs.openfactura.co/api/operations/dte_invalidar.md): `POST /dte/{codigo}/invalidar` - [Transmitir documentos en contingencia](https://docs.openfactura.co/api/operations/contingencia_transmitir.md): `POST /contingencia/transmitir` --- # Eventos > Evento de Retorno (18) y Operaciones Especiales (17). - [Evento de Retorno (18)](https://docs.openfactura.co/api/operations/dte_retorno.md): `POST /dte/{codigo}/retorno` - [Evento de Operaciones Especiales (17)](https://docs.openfactura.co/api/operations/operaciones_especiales_informar.md): `POST /eventos/operaciones-especiales` - [Listar eventos](https://docs.openfactura.co/api/operations/eventos_listar.md): `GET /eventos` - [Obtener un evento](https://docs.openfactura.co/api/operations/evento_obtener.md): `GET /eventos/{codigo}` - [Archivo del evento (JSON)](https://docs.openfactura.co/api/operations/evento_json.md): `GET /eventos/{codigo}/json` - [Representación gráfica del evento (PDF)](https://docs.openfactura.co/api/operations/evento_pdf.md): `GET /eventos/{codigo}/pdf` --- # Catálogos > Catálogos oficiales del MH (CAT-xxx). - [Listar catálogos del MH](https://docs.openfactura.co/api/operations/catalogos_listar.md): `GET /catalogos` - [Consultar un catálogo](https://docs.openfactura.co/api/operations/catalogo_buscar.md): `GET /catalogos/{id}` --- # Claves de API > Claves `of_test_…` y `of_live_…`. Sólo desde una sesión del panel. - [Listar claves de API](https://docs.openfactura.co/api/operations/claves_listar.md): `GET /claves` - [Crear una clave de API](https://docs.openfactura.co/api/operations/clave_crear.md): `POST /claves` - [Revocar una clave](https://docs.openfactura.co/api/operations/clave_revocar.md): `DELETE /claves/{id}` --- # Webhooks > Notificaciones firmadas (HMAC-SHA256) cuando cambia el estado de un documento o evento. - [Listar webhooks](https://docs.openfactura.co/api/operations/webhooks_listar.md): `GET /webhooks` - [Crear un webhook](https://docs.openfactura.co/api/operations/webhook_crear.md): `POST /webhooks` - [Eliminar un webhook y sus entregas](https://docs.openfactura.co/api/operations/webhook_eliminar.md): `DELETE /webhooks/{id}` - [Cambiar URL, descripción, eventos o activarlo/desactivarlo](https://docs.openfactura.co/api/operations/webhook_actualizar.md): `PATCH /webhooks/{id}` - [Generar un secreto nuevo](https://docs.openfactura.co/api/operations/webhook_rotar_secreto.md): `POST /webhooks/{id}/rotar-secreto` - [Enviar un evento `webhook.prueba` ahora](https://docs.openfactura.co/api/operations/webhook_probar.md): `POST /webhooks/{id}/probar` - [Últimas 50 entregas de un webhook](https://docs.openfactura.co/api/operations/webhook_entregas.md): `GET /webhooks/{id}/entregas` - [Reenviar una entrega ahora](https://docs.openfactura.co/api/operations/webhook_entrega_reenviar.md): `POST /webhooks/entregas/{id}/reenviar` --- # Auditoría > Bitácora de acciones de la organización. - [Bitácora de auditoría](https://docs.openfactura.co/api/operations/auditoria_listar.md): `GET /auditoria` --- # Registros > Peticiones hechas con claves de API (sólo metadatos; 30 días). - [Registros de peticiones de la API](https://docs.openfactura.co/api/operations/registros_listar.md): `GET /registros` --- # Documento OpenAPI 3.1 de esta API `GET https://app.openfactura.co/api/v1/openapi.json` ## Respuestas - **200**: Este documento. --- # Quién hace la petición `GET https://app.openfactura.co/api/v1/yo` Usuario o clave, organización, permisos y ambiente activo del MH. ## Respuestas - **200**: Actor autenticado. Esquema: `Yo`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Yo ```json { "type": "object", "properties": { "usuario": { "type": [ "object", "null" ], "properties": { "id": { "type": "string" }, "nombre": { "type": "string" }, "correo": { "type": "string" } }, "description": "Nulo cuando autentica una clave de API." }, "rol": { "type": [ "string", "null" ] }, "actor": { "type": "string", "enum": [ "usuario", "clave" ] }, "organizationId": { "type": "string" }, "permisos": { "type": "array", "items": { "type": "string", "enum": [ "dte:read", "dte:emit", "dte:invalidate", "clientes:read", "clientes:write", "productos:read", "productos:write", "emisor:read", "emisor:write", "claves:manage", "webhooks:manage", "auditoria:read", "registros:read" ] } }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "simulador": { "type": "boolean", "description": "El servidor usa el simulador del MH." }, "extensiones": { "type": "array", "description": "Enlaces del panel que agregan las extensiones instaladas, filtrados por permisos. Vacío en una instalación estándar.", "items": { "type": "object", "properties": { "titulo": { "type": "string" }, "ruta": { "type": "string" }, "icono": { "type": [ "string", "null" ] }, "grupo": { "type": "string", "enum": [ "desarrolladores", "administracion", "mas" ] } }, "required": [ "titulo", "ruta" ] } }, "url": { "type": "string", "description": "URL pública del servidor (`APP_URL`)." }, "mcp": { "type": "string", "description": "URL pública del servidor MCP remoto (`URL_MCP` o `/mcp`)." }, "cuenta": { "type": [ "object", "null" ], "description": "Plan y uso de la cuenta, cuando una extensión los aporta. Nulo en una instalación propia.", "properties": { "plan": { "type": "object", "properties": { "id": { "type": "string" }, "nombre": { "type": "string" } }, "required": [ "id", "nombre" ] }, "uso": { "type": "array", "items": { "type": "object", "properties": { "etiqueta": { "type": "string" }, "usado": { "type": "number" }, "limite": { "type": [ "number", "null" ] } }, "required": [ "etiqueta", "usado", "limite" ] } }, "mejorar": { "type": "object", "properties": { "titulo": { "type": "string" }, "detalle": { "type": "string" }, "ruta": { "type": "string" } }, "required": [ "titulo", "detalle", "ruta" ] } }, "required": [ "plan" ] } } } ``` --- # Resumen del tablero `GET https://app.openfactura.co/api/v1/resumen` Permiso: `dte:read`. ## Respuestas - **200**: Totales del mes y documentos recientes. Esquema: `Resumen`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Resumen ```json { "type": "object", "properties": { "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "mes": { "type": "string" }, "porEstado": { "type": "object", "additionalProperties": { "type": "integer" } }, "procesadosMes": { "type": "integer" }, "totalMes": { "type": "number" }, "recientes": { "type": "array", "items": { "$ref": "#/components/schemas/Documento" } } } } ``` ### Documento ```json { "type": "object", "properties": { "id": { "type": "string" }, "codigoGeneracion": { "type": "string", "description": "UUID en mayúsculas." }, "numeroControl": { "type": "string", "examples": [ "DTE-01-M001P001-000000000000001" ] }, "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "pendiente", "procesado", "rechazado", "contingencia", "invalidado" ] }, "fechaEmision": { "type": "string", "format": "date" }, "horaEmision": { "type": "string", "examples": [ "01:38:12" ] }, "receptorNombre": { "type": [ "string", "null" ] }, "receptorDocumento": { "type": [ "string", "null" ] }, "total": { "type": "number" }, "selloRecibido": { "type": [ "string", "null" ] }, "origen": { "type": "string", "examples": [ "panel", "api", "mcp" ] }, "error": { "type": [ "string", "null" ] }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." }, "urlConsulta": { "type": [ "string", "null" ], "description": "Consulta pública en el portal del MH." } }, "required": [ "id", "codigoGeneracion", "numeroControl", "tipoDte", "ambiente", "estado", "total" ] } ``` --- # Uso por día `GET https://app.openfactura.co/api/v1/uso` Documentos emitidos, procesados y rechazados y monto procesado por día en el ambiente activo. Sin fechas: los últimos 30 días (hora de El Salvador). Rango máximo: 366 días. Sólo incluye los días con movimiento. Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `desde` | query | no | string | | | `hasta` | query | no | string | | ## Respuestas - **200**: Totales del rango y detalle por día. Esquema: `Uso`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Uso ```json { "type": "object", "properties": { "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "desde": { "type": "string" }, "hasta": { "type": "string" }, "totales": { "type": "object", "properties": { "emitidos": { "type": "integer" }, "procesados": { "type": "integer" }, "rechazados": { "type": "integer" }, "monto": { "type": "number" } } }, "dias": { "type": "array", "items": { "type": "object", "properties": { "fecha": { "type": "string" }, "emitidos": { "type": "integer" }, "procesados": { "type": "integer" }, "rechazados": { "type": "integer" }, "monto": { "type": "number" } } } } } } ``` --- # Datos del emisor `GET https://app.openfactura.co/api/v1/emisor` Datos fiscales, establecimientos con sus puntos de venta y el estado de las credenciales del MH por ambiente (sólo banderas; los secretos nunca se devuelven). Permiso: `emisor:read`. ## Respuestas - **200**: Emisor. Esquema: `EmisorVista`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EmisorVista ```json { "type": "object", "properties": { "emisor": { "type": [ "object", "null" ], "description": "Datos fiscales; nulo si no está configurado." }, "establecimientos": { "type": "array", "items": { "$ref": "#/components/schemas/EstablecimientoVista" } }, "credenciales": { "type": "object", "properties": { "00": { "$ref": "#/components/schemas/EstadoCredenciales" }, "01": { "$ref": "#/components/schemas/EstadoCredenciales" } } }, "ambienteActivo": { "type": "string", "enum": [ "00", "01" ] } } } ``` ### EstablecimientoVista ```json { "allOf": [ { "$ref": "#/components/schemas/Establecimiento" }, { "type": "object", "properties": { "id": { "type": "string" }, "puntosVenta": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` ### Establecimiento ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipo": { "default": "01", "type": "string", "enum": [ "01", "02", "04", "07" ] }, "codMh": { "type": "string", "pattern": "^[MBSP]\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 10 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] } }, "required": [ "nombre", "codMh" ] } ``` ### EstadoCredenciales ```json { "type": "object", "properties": { "apiPassword": { "type": "boolean" }, "certificado": { "type": "boolean" }, "certificadoNit": { "type": [ "string", "null" ] }, "simuladas": { "type": "boolean", "description": "Generadas para el simulador del MH: los documentos no llegan a Hacienda." }, "actualizado": { "type": [ "integer", "null" ] } } } ``` --- # Crear o actualizar el emisor `PUT https://app.openfactura.co/api/v1/emisor` La primera vez crea el establecimiento `M001` y el punto de venta `P001`. Si se omite `ambiente` se conserva el actual. Permiso: `emisor:write`. ## Cuerpo (application/json, requerido) `Emisor` ## Respuestas - **200**: Emisor guardado. Esquema: `EmisorVista`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Emisor ```json { "type": "object", "properties": { "nit": { "type": "string" }, "nrc": { "type": "string" }, "nombre": { "type": "string", "minLength": 1, "maxLength": 250 }, "nombreComercial": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "codActividad": { "type": "string", "minLength": 1, "maxLength": 6 }, "descActividad": { "type": "string", "minLength": 1, "maxLength": 150 }, "departamento": { "type": "string", "minLength": 1, "maxLength": 2 }, "municipio": { "type": "string", "minLength": 1, "maxLength": 2 }, "distrito": { "type": "string", "minLength": 1, "maxLength": 2 }, "complemento": { "type": "string", "minLength": 1, "maxLength": 200 }, "telefono": { "type": "string", "minLength": 8, "maxLength": 30 }, "correo": { "type": "string", "maxLength": 100, "format": "email", "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "responsableNombre": { "anyOf": [ { "type": "string", "maxLength": 100 }, { "type": "null" } ] }, "responsableTipoDoc": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "responsableNumDoc": { "anyOf": [ { "type": "string", "maxLength": 20 }, { "type": "null" } ] } }, "required": [ "nit", "nrc", "nombre", "codActividad", "descActividad", "departamento", "municipio", "distrito", "complemento", "telefono", "correo" ] } ``` ### EmisorVista ```json { "type": "object", "properties": { "emisor": { "type": [ "object", "null" ], "description": "Datos fiscales; nulo si no está configurado." }, "establecimientos": { "type": "array", "items": { "$ref": "#/components/schemas/EstablecimientoVista" } }, "credenciales": { "type": "object", "properties": { "00": { "$ref": "#/components/schemas/EstadoCredenciales" }, "01": { "$ref": "#/components/schemas/EstadoCredenciales" } } }, "ambienteActivo": { "type": "string", "enum": [ "00", "01" ] } } } ``` ### EstablecimientoVista ```json { "allOf": [ { "$ref": "#/components/schemas/Establecimiento" }, { "type": "object", "properties": { "id": { "type": "string" }, "puntosVenta": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` ### Establecimiento ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipo": { "default": "01", "type": "string", "enum": [ "01", "02", "04", "07" ] }, "codMh": { "type": "string", "pattern": "^[MBSP]\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 10 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] } }, "required": [ "nombre", "codMh" ] } ``` ### EstadoCredenciales ```json { "type": "object", "properties": { "apiPassword": { "type": "boolean" }, "certificado": { "type": "boolean" }, "certificadoNit": { "type": [ "string", "null" ] }, "simuladas": { "type": "boolean", "description": "Generadas para el simulador del MH: los documentos no llegan a Hacienda." }, "actualizado": { "type": [ "integer", "null" ] } } } ``` --- # Guardar credenciales del MH `PUT https://app.openfactura.co/api/v1/emisor/credenciales/{ambiente}` `certificado` es el contenido XML del `.crt` de Hacienda y va siempre con su contraseña. Se verifica que la contraseña abra la llave y que el NIT del certificado sea el del emisor. Los secretos se cifran y nunca se devuelven. Permiso: `emisor:write`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `ambiente` | path | sí | string | `00` pruebas, `01` producción. | ## Cuerpo (application/json, requerido) `Credenciales` ## Respuestas - **200**: Estado de las credenciales de ese ambiente. Esquema: `EstadoCredenciales`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Credenciales ```json { "type": "object", "properties": { "apiPassword": { "type": "string", "minLength": 1, "maxLength": 200 }, "certificado": { "type": "string", "minLength": 1, "maxLength": 200000 }, "certificadoPassword": { "type": "string", "minLength": 1, "maxLength": 200 } } } ``` ### EstadoCredenciales ```json { "type": "object", "properties": { "apiPassword": { "type": "boolean" }, "certificado": { "type": "boolean" }, "certificadoNit": { "type": [ "string", "null" ] }, "simuladas": { "type": "boolean", "description": "Generadas para el simulador del MH: los documentos no llegan a Hacienda." }, "actualizado": { "type": [ "integer", "null" ] } } } ``` --- # Generar credenciales de prueba (simulador) `POST https://app.openfactura.co/api/v1/emisor/credenciales-prueba` Sólo en servidores con `MH_SIMULADO=1`: genera un certificado y una contraseña de prueba para el ambiente `00`, atendidos por el simulador del MH. Sólo estas credenciales usan el simulador: las que sube el usuario van a Hacienda. Sin el simulador responde `409 simulador_inactivo`. Permiso: `emisor:write`. ## Respuestas - **200**: Estado de las credenciales del ambiente 00. Esquema: `EstadoCredenciales`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EstadoCredenciales ```json { "type": "object", "properties": { "apiPassword": { "type": "boolean" }, "certificado": { "type": "boolean" }, "certificadoNit": { "type": [ "string", "null" ] }, "simuladas": { "type": "boolean", "description": "Generadas para el simulador del MH: los documentos no llegan a Hacienda." }, "actualizado": { "type": [ "integer", "null" ] } } } ``` --- # Probar la conexión con el MH `POST https://app.openfactura.co/api/v1/emisor/probar` Permiso: `emisor:write`. ## Respuestas - **200**: Resultado de autenticarse contra el MH. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Listar establecimientos `GET https://app.openfactura.co/api/v1/establecimientos` Permiso: `emisor:read`. ## Respuestas - **200**: Establecimientos con sus puntos de venta. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EstablecimientoVista ```json { "allOf": [ { "$ref": "#/components/schemas/Establecimiento" }, { "type": "object", "properties": { "id": { "type": "string" }, "puntosVenta": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` ### Establecimiento ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipo": { "default": "01", "type": "string", "enum": [ "01", "02", "04", "07" ] }, "codMh": { "type": "string", "pattern": "^[MBSP]\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 10 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] } }, "required": [ "nombre", "codMh" ] } ``` --- # Crear un establecimiento `POST https://app.openfactura.co/api/v1/establecimientos` Permiso: `emisor:write`. ## Cuerpo (application/json, requerido) `Establecimiento` ## Respuestas - **201**: Lista actualizada de establecimientos. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Establecimiento ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipo": { "default": "01", "type": "string", "enum": [ "01", "02", "04", "07" ] }, "codMh": { "type": "string", "pattern": "^[MBSP]\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 10 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] } }, "required": [ "nombre", "codMh" ] } ``` ### EstablecimientoVista ```json { "allOf": [ { "$ref": "#/components/schemas/Establecimiento" }, { "type": "object", "properties": { "id": { "type": "string" }, "puntosVenta": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` --- # Actualizar un establecimiento `PUT https://app.openfactura.co/api/v1/establecimientos/{id}` Permiso: `emisor:write`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Cuerpo (application/json, requerido) `Establecimiento` ## Respuestas - **200**: Lista actualizada de establecimientos. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Establecimiento ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipo": { "default": "01", "type": "string", "enum": [ "01", "02", "04", "07" ] }, "codMh": { "type": "string", "pattern": "^[MBSP]\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 10 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] } }, "required": [ "nombre", "codMh" ] } ``` ### EstablecimientoVista ```json { "allOf": [ { "$ref": "#/components/schemas/Establecimiento" }, { "type": "object", "properties": { "id": { "type": "string" }, "puntosVenta": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` --- # Crear un punto de venta `POST https://app.openfactura.co/api/v1/puntos-venta` Permiso: `emisor:write`. ## Cuerpo (application/json, requerido) `PuntoVenta` ## Respuestas - **201**: Lista actualizada de establecimientos. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### PuntoVenta ```json { "type": "object", "properties": { "establecimientoId": { "type": "string", "minLength": 1 }, "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "codMh": { "type": "string", "pattern": "^P\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 15 }, { "type": "null" } ] } }, "required": [ "establecimientoId", "nombre", "codMh" ] } ``` ### EstablecimientoVista ```json { "allOf": [ { "$ref": "#/components/schemas/Establecimiento" }, { "type": "object", "properties": { "id": { "type": "string" }, "puntosVenta": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` ### Establecimiento ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipo": { "default": "01", "type": "string", "enum": [ "01", "02", "04", "07" ] }, "codMh": { "type": "string", "pattern": "^[MBSP]\\d{3}$" }, "codInterno": { "anyOf": [ { "type": "string", "maxLength": 10 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] } }, "required": [ "nombre", "codMh" ] } ``` --- # Buscar clientes `GET https://app.openfactura.co/api/v1/clientes` `q` busca por nombre, nombre comercial o número de documento. Hasta 100 por defecto. Permiso: `clientes:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `q` | query | no | string | | | `limite` | query | no | integer | | ## Respuestas - **200**: Clientes. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Cliente ```json { "allOf": [ { "$ref": "#/components/schemas/ClienteEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` ### ClienteEntrada ```json { "type": "object", "properties": { "tipoDocumento": { "type": "string", "enum": [ "13", "36", "37", "03", "02" ] }, "numDocumento": { "type": "string", "minLength": 1, "maxLength": 20 }, "nrc": { "anyOf": [ { "type": "string", "maxLength": 8 }, { "type": "null" } ] }, "nombre": { "type": "string", "minLength": 1, "maxLength": 250 }, "nombreComercial": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "codActividad": { "anyOf": [ { "type": "string", "maxLength": 6 }, { "type": "null" } ] }, "descActividad": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] }, "correo": { "anyOf": [ { "anyOf": [ { "type": "string", "maxLength": 100, "format": "email", "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" }, { "type": "string", "const": "" } ] }, { "type": "null" } ] } }, "required": [ "tipoDocumento", "numDocumento", "nombre" ] } ``` --- # Crear un cliente `POST https://app.openfactura.co/api/v1/clientes` Permiso: `clientes:write`. ## Cuerpo (application/json, requerido) `ClienteEntrada` ## Respuestas - **201**: Cliente creado. Esquema: `Cliente`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### ClienteEntrada ```json { "type": "object", "properties": { "tipoDocumento": { "type": "string", "enum": [ "13", "36", "37", "03", "02" ] }, "numDocumento": { "type": "string", "minLength": 1, "maxLength": 20 }, "nrc": { "anyOf": [ { "type": "string", "maxLength": 8 }, { "type": "null" } ] }, "nombre": { "type": "string", "minLength": 1, "maxLength": 250 }, "nombreComercial": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "codActividad": { "anyOf": [ { "type": "string", "maxLength": 6 }, { "type": "null" } ] }, "descActividad": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] }, "correo": { "anyOf": [ { "anyOf": [ { "type": "string", "maxLength": 100, "format": "email", "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" }, { "type": "string", "const": "" } ] }, { "type": "null" } ] } }, "required": [ "tipoDocumento", "numDocumento", "nombre" ] } ``` ### Cliente ```json { "allOf": [ { "$ref": "#/components/schemas/ClienteEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` --- # Obtener un cliente `GET https://app.openfactura.co/api/v1/clientes/{id}` Permiso: `clientes:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Cliente. Esquema: `Cliente`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Cliente ```json { "allOf": [ { "$ref": "#/components/schemas/ClienteEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` ### ClienteEntrada ```json { "type": "object", "properties": { "tipoDocumento": { "type": "string", "enum": [ "13", "36", "37", "03", "02" ] }, "numDocumento": { "type": "string", "minLength": 1, "maxLength": 20 }, "nrc": { "anyOf": [ { "type": "string", "maxLength": 8 }, { "type": "null" } ] }, "nombre": { "type": "string", "minLength": 1, "maxLength": 250 }, "nombreComercial": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "codActividad": { "anyOf": [ { "type": "string", "maxLength": 6 }, { "type": "null" } ] }, "descActividad": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] }, "correo": { "anyOf": [ { "anyOf": [ { "type": "string", "maxLength": 100, "format": "email", "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" }, { "type": "string", "const": "" } ] }, { "type": "null" } ] } }, "required": [ "tipoDocumento", "numDocumento", "nombre" ] } ``` --- # Actualizar un cliente `PUT https://app.openfactura.co/api/v1/clientes/{id}` Permiso: `clientes:write`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Cuerpo (application/json, requerido) `ClienteEntrada` ## Respuestas - **200**: Cliente actualizado. Esquema: `Cliente`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### ClienteEntrada ```json { "type": "object", "properties": { "tipoDocumento": { "type": "string", "enum": [ "13", "36", "37", "03", "02" ] }, "numDocumento": { "type": "string", "minLength": 1, "maxLength": 20 }, "nrc": { "anyOf": [ { "type": "string", "maxLength": 8 }, { "type": "null" } ] }, "nombre": { "type": "string", "minLength": 1, "maxLength": 250 }, "nombreComercial": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "codActividad": { "anyOf": [ { "type": "string", "maxLength": 6 }, { "type": "null" } ] }, "descActividad": { "anyOf": [ { "type": "string", "maxLength": 150 }, { "type": "null" } ] }, "departamento": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "municipio": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "distrito": { "anyOf": [ { "type": "string", "maxLength": 2 }, { "type": "null" } ] }, "complemento": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "telefono": { "anyOf": [ { "type": "string", "maxLength": 30 }, { "type": "null" } ] }, "correo": { "anyOf": [ { "anyOf": [ { "type": "string", "maxLength": 100, "format": "email", "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" }, { "type": "string", "const": "" } ] }, { "type": "null" } ] } }, "required": [ "tipoDocumento", "numDocumento", "nombre" ] } ``` ### Cliente ```json { "allOf": [ { "$ref": "#/components/schemas/ClienteEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` --- # Eliminar un cliente `DELETE https://app.openfactura.co/api/v1/clientes/{id}` Permiso: `clientes:write`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Eliminado. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Buscar productos `GET https://app.openfactura.co/api/v1/productos` `q` busca por descripción o código. Hasta 100 por defecto. Permiso: `productos:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `q` | query | no | string | | | `limite` | query | no | integer | | ## Respuestas - **200**: Productos. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Producto ```json { "allOf": [ { "$ref": "#/components/schemas/ProductoEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` ### ProductoEntrada ```json { "type": "object", "properties": { "codigo": { "type": "string", "minLength": 1, "maxLength": 25 }, "descripcion": { "type": "string", "minLength": 1, "maxLength": 1000 }, "precioUnitario": { "type": "number", "minimum": 0, "maximum": 99999999999 }, "tipoItem": { "default": 1, "anyOf": [ { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 4 } ] }, "uniMedida": { "default": 59, "type": "integer", "minimum": 1, "maximum": 99 }, "tipoVenta": { "default": "gravada", "type": "string", "enum": [ "gravada", "exenta", "noSujeta" ] } }, "required": [ "codigo", "descripcion", "precioUnitario" ] } ``` --- # Crear un producto `POST https://app.openfactura.co/api/v1/productos` `precioUnitario` va sin IVA. Permiso: `productos:write`. ## Cuerpo (application/json, requerido) `ProductoEntrada` ## Respuestas - **201**: Producto creado. Esquema: `Producto`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### ProductoEntrada ```json { "type": "object", "properties": { "codigo": { "type": "string", "minLength": 1, "maxLength": 25 }, "descripcion": { "type": "string", "minLength": 1, "maxLength": 1000 }, "precioUnitario": { "type": "number", "minimum": 0, "maximum": 99999999999 }, "tipoItem": { "default": 1, "anyOf": [ { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 4 } ] }, "uniMedida": { "default": 59, "type": "integer", "minimum": 1, "maximum": 99 }, "tipoVenta": { "default": "gravada", "type": "string", "enum": [ "gravada", "exenta", "noSujeta" ] } }, "required": [ "codigo", "descripcion", "precioUnitario" ] } ``` ### Producto ```json { "allOf": [ { "$ref": "#/components/schemas/ProductoEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` --- # Obtener un producto `GET https://app.openfactura.co/api/v1/productos/{id}` Permiso: `productos:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Producto. Esquema: `Producto`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Producto ```json { "allOf": [ { "$ref": "#/components/schemas/ProductoEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` ### ProductoEntrada ```json { "type": "object", "properties": { "codigo": { "type": "string", "minLength": 1, "maxLength": 25 }, "descripcion": { "type": "string", "minLength": 1, "maxLength": 1000 }, "precioUnitario": { "type": "number", "minimum": 0, "maximum": 99999999999 }, "tipoItem": { "default": 1, "anyOf": [ { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 4 } ] }, "uniMedida": { "default": 59, "type": "integer", "minimum": 1, "maximum": 99 }, "tipoVenta": { "default": "gravada", "type": "string", "enum": [ "gravada", "exenta", "noSujeta" ] } }, "required": [ "codigo", "descripcion", "precioUnitario" ] } ``` --- # Actualizar un producto `PUT https://app.openfactura.co/api/v1/productos/{id}` Permiso: `productos:write`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Cuerpo (application/json, requerido) `ProductoEntrada` ## Respuestas - **200**: Producto actualizado. Esquema: `Producto`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### ProductoEntrada ```json { "type": "object", "properties": { "codigo": { "type": "string", "minLength": 1, "maxLength": 25 }, "descripcion": { "type": "string", "minLength": 1, "maxLength": 1000 }, "precioUnitario": { "type": "number", "minimum": 0, "maximum": 99999999999 }, "tipoItem": { "default": 1, "anyOf": [ { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 4 } ] }, "uniMedida": { "default": 59, "type": "integer", "minimum": 1, "maximum": 99 }, "tipoVenta": { "default": "gravada", "type": "string", "enum": [ "gravada", "exenta", "noSujeta" ] } }, "required": [ "codigo", "descripcion", "precioUnitario" ] } ``` ### Producto ```json { "allOf": [ { "$ref": "#/components/schemas/ProductoEntrada" }, { "type": "object", "properties": { "id": { "type": "string" }, "organizationId": { "type": "string" }, "creado": { "type": "string" } } } ] } ``` --- # Eliminar un producto `DELETE https://app.openfactura.co/api/v1/productos/{id}` Permiso: `productos:write`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Eliminado. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Listar documentos `GET https://app.openfactura.co/api/v1/dte` Documentos del ambiente activo. `q` busca por receptor, número de control o código de generación; `desde`/`hasta` en formato AAAA-MM-DD. Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `tipo` | query | no | string | | | `estado` | query | no | string | | | `q` | query | no | string | | | `desde` | query | no | string | | | `hasta` | query | no | string | | | `pagina` | query | no | integer | | | `porPagina` | query | no | integer | | ## Respuestas - **200**: Página de documentos. Esquema: `ListaDocumentos`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### ListaDocumentos ```json { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/Documento" } }, "total": { "type": "integer" }, "pagina": { "type": "integer" }, "porPagina": { "type": "integer" }, "ambiente": { "type": "string", "enum": [ "00", "01" ] } }, "required": [ "items", "total", "pagina", "porPagina", "ambiente" ] } ``` ### Documento ```json { "type": "object", "properties": { "id": { "type": "string" }, "codigoGeneracion": { "type": "string", "description": "UUID en mayúsculas." }, "numeroControl": { "type": "string", "examples": [ "DTE-01-M001P001-000000000000001" ] }, "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "pendiente", "procesado", "rechazado", "contingencia", "invalidado" ] }, "fechaEmision": { "type": "string", "format": "date" }, "horaEmision": { "type": "string", "examples": [ "01:38:12" ] }, "receptorNombre": { "type": [ "string", "null" ] }, "receptorDocumento": { "type": [ "string", "null" ] }, "total": { "type": "number" }, "selloRecibido": { "type": [ "string", "null" ] }, "origen": { "type": "string", "examples": [ "panel", "api", "mcp" ] }, "error": { "type": [ "string", "null" ] }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." }, "urlConsulta": { "type": [ "string", "null" ], "description": "Consulta pública en el portal del MH." } }, "required": [ "id", "codigoGeneracion", "numeroControl", "tipoDte", "ambiente", "estado", "total" ] } ``` --- # Emitir un DTE `POST https://app.openfactura.co/api/v1/dte` Construye, valida, firma y transmite el documento al MH. `datos` es la entrada del constructor del SDK `@openfactura/dte` para ese tipo. En Factura (01) `precioUni` incluye IVA; en Crédito Fiscal, notas y remisión va sin IVA. Si se envía `clienteId` y falta `datos.receptor`, el receptor se toma del cliente guardado. El Comprobante de Liquidación (08), el Documento Contable de Liquidación (09) y el Comprobante de Donación (15) no admiten contingencia: con el MH caído responden `mh_no_disponible`. Permiso: `dte:emit`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | string | Hasta 200 caracteres. Repetir con la misma clave y el mismo cuerpo devuelve la respuesta original con `Idempotent-Replayed: true`, sin emitir dos veces; con otro cuerpo: `409 idempotencia_distinta`. Se conservan 24 horas, por ambiente. | ## Cuerpo (application/json, requerido) `Emision` ## Respuestas - **201**: Procesado por el MH, con `selloRecibido`. Esquema: `Documento`. - **202**: Contingencia: el MH no respondió; el DTE quedó firmado y se transmite después. Esquema: `Documento`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **402**: El plan de la cuenta no incluye la operación. Sólo lo devuelven distribuciones con planes (una instalación propia nunca). Códigos: `plan_requerido`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **422**: El MH rechazó el documento o el evento; `details.mh` = `{ codigo, mensaje, observaciones[] }`. Códigos: `dte_rechazado`, `invalidacion_rechazada`, `contingencia_rechazada`, `evento_rechazado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. - **502**: Problema hablando con el MH o al firmar. Con `emision_incierta` consulta `details.codigoGeneracion` antes de reintentar. Códigos: `mh_credenciales_rechazadas`, `mh_no_disponible`, `emision_fallida`, `emision_incierta`. Esquema: `Error`. ## Esquemas ### Emision ```json { "type": "object", "properties": { "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "datos": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, "clienteId": { "type": "string" }, "puntoVentaId": { "type": "string" } }, "required": [ "tipoDte", "datos" ] } ``` ### Documento ```json { "type": "object", "properties": { "id": { "type": "string" }, "codigoGeneracion": { "type": "string", "description": "UUID en mayúsculas." }, "numeroControl": { "type": "string", "examples": [ "DTE-01-M001P001-000000000000001" ] }, "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "pendiente", "procesado", "rechazado", "contingencia", "invalidado" ] }, "fechaEmision": { "type": "string", "format": "date" }, "horaEmision": { "type": "string", "examples": [ "01:38:12" ] }, "receptorNombre": { "type": [ "string", "null" ] }, "receptorDocumento": { "type": [ "string", "null" ] }, "total": { "type": "number" }, "selloRecibido": { "type": [ "string", "null" ] }, "origen": { "type": "string", "examples": [ "panel", "api", "mcp" ] }, "error": { "type": [ "string", "null" ] }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." }, "urlConsulta": { "type": [ "string", "null" ], "description": "Consulta pública en el portal del MH." } }, "required": [ "id", "codigoGeneracion", "numeroControl", "tipoDte", "ambiente", "estado", "total" ] } ``` --- # Validar sin emitir `POST https://app.openfactura.co/api/v1/dte/validar` Construye el DTE y lo valida contra el esquema del MH. No consume número de control, no firma y no transmite. Permiso: `dte:emit`. ## Cuerpo (application/json, requerido) `Emision` ## Respuestas - **200**: DTE construido, problemas y total. Esquema: `Validacion`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Emision ```json { "type": "object", "properties": { "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "datos": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, "clienteId": { "type": "string" }, "puntoVentaId": { "type": "string" } }, "required": [ "tipoDte", "datos" ] } ``` ### Validacion ```json { "type": "object", "properties": { "dte": { "type": "object", "additionalProperties": true }, "problemas": { "type": "array", "items": { "type": "object", "properties": { "ruta": { "type": "string" }, "mensaje": { "type": "string" }, "regla": { "type": "string" } } } }, "total": { "type": "number" }, "receptorNombre": { "type": [ "string", "null" ] } }, "required": [ "dte", "problemas", "total" ] } ``` --- # Obtener un documento `GET https://app.openfactura.co/api/v1/dte/{codigo}` Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Respuestas - **200**: Documento con su DTE, la respuesta del MH y sus eventos. Esquema: `DocumentoDetalle`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### DocumentoDetalle ```json { "allOf": [ { "$ref": "#/components/schemas/Documento" }, { "type": "object", "properties": { "dte": { "type": "object", "additionalProperties": true, "description": "Estructura del DTE tal como se firmó." }, "respuestaMh": { "type": [ "object", "null" ], "description": "Última respuesta del MH." }, "eventos": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` ### Documento ```json { "type": "object", "properties": { "id": { "type": "string" }, "codigoGeneracion": { "type": "string", "description": "UUID en mayúsculas." }, "numeroControl": { "type": "string", "examples": [ "DTE-01-M001P001-000000000000001" ] }, "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "pendiente", "procesado", "rechazado", "contingencia", "invalidado" ] }, "fechaEmision": { "type": "string", "format": "date" }, "horaEmision": { "type": "string", "examples": [ "01:38:12" ] }, "receptorNombre": { "type": [ "string", "null" ] }, "receptorDocumento": { "type": [ "string", "null" ] }, "total": { "type": "number" }, "selloRecibido": { "type": [ "string", "null" ] }, "origen": { "type": "string", "examples": [ "panel", "api", "mcp" ] }, "error": { "type": [ "string", "null" ] }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." }, "urlConsulta": { "type": [ "string", "null" ], "description": "Consulta pública en el portal del MH." } }, "required": [ "id", "codigoGeneracion", "numeroControl", "tipoDte", "ambiente", "estado", "total" ] } ``` --- # Archivo DTE oficial (JSON) `GET https://app.openfactura.co/api/v1/dte/{codigo}/json` Estructura del DTE más `firmaElectronica` y `selloRecibido`, para entregar al receptor. Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Respuestas - **200**: Archivo DTE. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Representación gráfica (PDF) `GET https://app.openfactura.co/api/v1/dte/{codigo}/pdf` Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Respuestas - **200**: PDF del documento. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Invalidar un documento o un evento `POST https://app.openfactura.co/api/v1/dte/{codigo}/invalidar` Irreversible. `tipoAnulacion`: 1 error en la información (requiere `codigoGeneracionReemplazo`), 2 rescindir la operación, 3 otro (requiere motivo y reemplazo). `:codigo` también puede ser un evento de retorno u operaciones especiales: entonces responde `{ invalidado, tipo, codigoGeneracion }`. Un DTE con un retorno vigente responde `409 retorno_vigente`. Permiso: `dte:invalidate`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Cuerpo (application/json, requerido) `Invalidacion` ## Respuestas - **200**: Documento invalidado, o el evento invalidado. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **422**: El MH rechazó el documento o el evento; `details.mh` = `{ codigo, mensaje, observaciones[] }`. Códigos: `dte_rechazado`, `invalidacion_rechazada`, `contingencia_rechazada`, `evento_rechazado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. - **502**: Problema hablando con el MH o al firmar. Con `emision_incierta` consulta `details.codigoGeneracion` antes de reintentar. Códigos: `mh_credenciales_rechazadas`, `mh_no_disponible`, `emision_fallida`, `emision_incierta`. Esquema: `Error`. ## Esquemas ### Invalidacion ```json { "type": "object", "properties": { "tipoAnulacion": { "anyOf": [ { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 } ] }, "motivoAnulacion": { "anyOf": [ { "type": "string", "maxLength": 250 }, { "type": "null" } ] }, "codigoGeneracionReemplazo": { "anyOf": [ { "type": "string", "maxLength": 36 }, { "type": "null" } ] }, "solicitante": { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipoDocumento": { "type": "string", "minLength": 2, "maxLength": 2 }, "numDocumento": { "type": "string", "minLength": 1, "maxLength": 20 } }, "required": [ "nombre", "tipoDocumento", "numDocumento" ] }, "responsable": { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 100 }, "tipoDocumento": { "type": "string", "minLength": 2, "maxLength": 2 }, "numDocumento": { "type": "string", "minLength": 1, "maxLength": 20 } }, "required": [ "nombre", "tipoDocumento", "numDocumento" ] } }, "required": [ "tipoAnulacion" ] } ``` ### DocumentoDetalle ```json { "allOf": [ { "$ref": "#/components/schemas/Documento" }, { "type": "object", "properties": { "dte": { "type": "object", "additionalProperties": true, "description": "Estructura del DTE tal como se firmó." }, "respuestaMh": { "type": [ "object", "null" ], "description": "Última respuesta del MH." }, "eventos": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } ] } ``` ### Documento ```json { "type": "object", "properties": { "id": { "type": "string" }, "codigoGeneracion": { "type": "string", "description": "UUID en mayúsculas." }, "numeroControl": { "type": "string", "examples": [ "DTE-01-M001P001-000000000000001" ] }, "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "pendiente", "procesado", "rechazado", "contingencia", "invalidado" ] }, "fechaEmision": { "type": "string", "format": "date" }, "horaEmision": { "type": "string", "examples": [ "01:38:12" ] }, "receptorNombre": { "type": [ "string", "null" ] }, "receptorDocumento": { "type": [ "string", "null" ] }, "total": { "type": "number" }, "selloRecibido": { "type": [ "string", "null" ] }, "origen": { "type": "string", "examples": [ "panel", "api", "mcp" ] }, "error": { "type": [ "string", "null" ] }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." }, "urlConsulta": { "type": [ "string", "null" ], "description": "Consulta pública en el portal del MH." } }, "required": [ "id", "codigoGeneracion", "numeroControl", "tipoDte", "ambiente", "estado", "total" ] } ``` --- # Evento de Retorno (18) `POST https://app.openfactura.co/api/v1/dte/{codigo}/retorno` Devolución de bienes, reembolsos o envases sobre una Factura (01), Factura de Exportación (11) o Sujeto Excluido (14) procesados, sin invalidar el documento. `precioUni` como en el documento original (en Factura, con IVA). Plazo: 3 meses desde la fecha del DTE. La suma de los retornos no puede superar el documento (`409 retorno_excedido` con `details.disponible`). Permiso: `dte:emit`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | | `Idempotency-Key` | header | no | string | Hasta 200 caracteres. Repetir con la misma clave y el mismo cuerpo devuelve la respuesta original con `Idempotent-Replayed: true`, sin emitir dos veces; con otro cuerpo: `409 idempotencia_distinta`. Se conservan 24 horas, por ambiente. | ## Cuerpo (application/json, requerido) `Retorno` ## Respuestas - **201**: Evento procesado. Esquema: `Evento`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **422**: El MH rechazó el documento o el evento; `details.mh` = `{ codigo, mensaje, observaciones[] }`. Códigos: `dte_rechazado`, `invalidacion_rechazada`, `contingencia_rechazada`, `evento_rechazado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. - **502**: Problema hablando con el MH o al firmar. Con `emision_incierta` consulta `details.codigoGeneracion` antes de reintentar. Códigos: `mh_credenciales_rechazadas`, `mh_no_disponible`, `emision_fallida`, `emision_incierta`. Esquema: `Error`. ## Esquemas ### Retorno ```json { "type": "object", "properties": { "items": { "minItems": 1, "maxItems": 2000, "type": "array", "items": { "type": "object", "properties": { "descripcion": { "type": "string", "minLength": 1, "maxLength": 1000 }, "cantidad": { "type": "number", "exclusiveMinimum": 0 }, "precioUni": { "type": "number", "minimum": 0 }, "tipoItem": { "anyOf": [ { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 4 } ] }, "codigo": { "anyOf": [ { "type": "string", "maxLength": 25 }, { "type": "null" } ] }, "uniMedida": { "type": "integer", "minimum": 1, "maximum": 99 }, "descuento": { "type": "number", "minimum": 0 }, "tipoVenta": { "type": "string", "enum": [ "gravada", "exenta", "noSujeta" ] }, "seguro": { "type": "number", "minimum": 0 }, "flete": { "type": "number", "minimum": 0 } }, "required": [ "descripcion", "cantidad", "precioUni" ] } } }, "required": [ "items" ] } ``` ### Evento ```json { "type": "object", "properties": { "id": { "type": "string" }, "tipo": { "type": "string", "enum": [ "retorno", "operaciones_especiales" ] }, "tipoEvento": { "type": "string", "enum": [ "18", "17" ] }, "codigoGeneracion": { "type": "string" }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "procesado", "rechazado", "invalidado" ] }, "selloRecibido": { "type": [ "string", "null" ] }, "documentoId": { "type": [ "string", "null" ], "description": "Documento de un retorno." }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." } }, "required": [ "id", "tipo", "codigoGeneracion", "ambiente", "estado" ] } ``` --- # Transmitir documentos en contingencia `POST https://app.openfactura.co/api/v1/contingencia/transmitir` Envía el evento de contingencia con los documentos en estado `contingencia` del ambiente activo (hasta 40 por llamada) y luego transmite cada uno. Si `restantes` es mayor que 0, vuelve a llamar. Requiere el responsable configurado en el emisor. Permiso: `dte:invalidate`. ## Respuestas - **200**: Resultado del lote. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **422**: El MH rechazó el documento o el evento; `details.mh` = `{ codigo, mensaje, observaciones[] }`. Códigos: `dte_rechazado`, `invalidacion_rechazada`, `contingencia_rechazada`, `evento_rechazado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. - **502**: Problema hablando con el MH o al firmar. Con `emision_incierta` consulta `details.codigoGeneracion` antes de reintentar. Códigos: `mh_credenciales_rechazadas`, `mh_no_disponible`, `emision_fallida`, `emision_incierta`. Esquema: `Error`. ## Esquemas ### Documento ```json { "type": "object", "properties": { "id": { "type": "string" }, "codigoGeneracion": { "type": "string", "description": "UUID en mayúsculas." }, "numeroControl": { "type": "string", "examples": [ "DTE-01-M001P001-000000000000001" ] }, "tipoDte": { "type": "string", "enum": [ "11", "14", "15", "01", "03", "04", "05", "06", "07", "08", "09" ] }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "pendiente", "procesado", "rechazado", "contingencia", "invalidado" ] }, "fechaEmision": { "type": "string", "format": "date" }, "horaEmision": { "type": "string", "examples": [ "01:38:12" ] }, "receptorNombre": { "type": [ "string", "null" ] }, "receptorDocumento": { "type": [ "string", "null" ] }, "total": { "type": "number" }, "selloRecibido": { "type": [ "string", "null" ] }, "origen": { "type": "string", "examples": [ "panel", "api", "mcp" ] }, "error": { "type": [ "string", "null" ] }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." }, "urlConsulta": { "type": [ "string", "null" ], "description": "Consulta pública en el portal del MH." } }, "required": [ "id", "codigoGeneracion", "numeroControl", "tipoDte", "ambiente", "estado", "total" ] } ``` --- # Evento de Operaciones Especiales (17) `POST https://app.openfactura.co/api/v1/eventos/operaciones-especiales` Informa facturas simplificadas (`tipoDocumento: "02"`, siempre por rangos `docDel`–`docAl`) o comprobantes de control interno (`"97"`, uno a uno o por rangos; no se mezclan). Precios sin IVA. `fecha` define el periodo (por defecto hoy, nunca futura). Permiso: `dte:emit`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | string | Hasta 200 caracteres. Repetir con la misma clave y el mismo cuerpo devuelve la respuesta original con `Idempotent-Replayed: true`, sin emitir dos veces; con otro cuerpo: `409 idempotencia_distinta`. Se conservan 24 horas, por ambiente. | ## Cuerpo (application/json, requerido) `OperacionesEspeciales` ## Respuestas - **201**: Evento procesado. Esquema: `Evento`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **422**: El MH rechazó el documento o el evento; `details.mh` = `{ codigo, mensaje, observaciones[] }`. Códigos: `dte_rechazado`, `invalidacion_rechazada`, `contingencia_rechazada`, `evento_rechazado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. - **502**: Problema hablando con el MH o al firmar. Con `emision_incierta` consulta `details.codigoGeneracion` antes de reintentar. Códigos: `mh_credenciales_rechazadas`, `mh_no_disponible`, `emision_fallida`, `emision_incierta`. Esquema: `Error`. ## Esquemas ### OperacionesEspeciales ```json { "type": "object", "properties": { "tipoDocumento": { "type": "string", "enum": [ "97", "02" ] }, "fecha": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "items": { "minItems": 1, "maxItems": 2000, "type": "array", "items": { "type": "object", "properties": { "descripcion": { "type": "string", "minLength": 1, "maxLength": 1500 }, "precioUni": { "type": "number", "minimum": 0 }, "cantidad": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "numDocumento": { "anyOf": [ { "type": "string", "maxLength": 36 }, { "type": "null" } ] }, "docDel": { "type": "string", "minLength": 1, "maxLength": 36 }, "docAl": { "type": "string", "minLength": 1, "maxLength": 36 }, "fechaEmision": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "tipoVenta": { "type": "string", "enum": [ "gravada", "exenta", "noSujeta" ] }, "codigoGeneracionRef": { "type": "string", "minLength": 36, "maxLength": 36 } }, "required": [ "descripcion", "precioUni" ] } } }, "required": [ "tipoDocumento", "items" ] } ``` ### Evento ```json { "type": "object", "properties": { "id": { "type": "string" }, "tipo": { "type": "string", "enum": [ "retorno", "operaciones_especiales" ] }, "tipoEvento": { "type": "string", "enum": [ "18", "17" ] }, "codigoGeneracion": { "type": "string" }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "procesado", "rechazado", "invalidado" ] }, "selloRecibido": { "type": [ "string", "null" ] }, "documentoId": { "type": [ "string", "null" ], "description": "Documento de un retorno." }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." } }, "required": [ "id", "tipo", "codigoGeneracion", "ambiente", "estado" ] } ``` --- # Listar eventos `GET https://app.openfactura.co/api/v1/eventos` Los 50 eventos de retorno u operaciones especiales más recientes del ambiente activo. Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `tipo` | query | no | string | | ## Respuestas - **200**: Eventos. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Evento ```json { "type": "object", "properties": { "id": { "type": "string" }, "tipo": { "type": "string", "enum": [ "retorno", "operaciones_especiales" ] }, "tipoEvento": { "type": "string", "enum": [ "18", "17" ] }, "codigoGeneracion": { "type": "string" }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "procesado", "rechazado", "invalidado" ] }, "selloRecibido": { "type": [ "string", "null" ] }, "documentoId": { "type": [ "string", "null" ], "description": "Documento de un retorno." }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." } }, "required": [ "id", "tipo", "codigoGeneracion", "ambiente", "estado" ] } ``` --- # Obtener un evento `GET https://app.openfactura.co/api/v1/eventos/{codigo}` Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Respuestas - **200**: Evento con su JSON y la respuesta del MH. Esquema: `EventoDetalle`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EventoDetalle ```json { "allOf": [ { "$ref": "#/components/schemas/Evento" }, { "type": "object", "properties": { "dte": { "type": "object", "additionalProperties": true }, "respuestaMh": { "type": [ "object", "null" ] } } } ] } ``` ### Evento ```json { "type": "object", "properties": { "id": { "type": "string" }, "tipo": { "type": "string", "enum": [ "retorno", "operaciones_especiales" ] }, "tipoEvento": { "type": "string", "enum": [ "18", "17" ] }, "codigoGeneracion": { "type": "string" }, "ambiente": { "type": "string", "enum": [ "00", "01" ] }, "estado": { "type": "string", "enum": [ "procesado", "rechazado", "invalidado" ] }, "selloRecibido": { "type": [ "string", "null" ] }, "documentoId": { "type": [ "string", "null" ], "description": "Documento de un retorno." }, "creado": { "type": "integer", "description": "Milisegundos desde 1970." } }, "required": [ "id", "tipo", "codigoGeneracion", "ambiente", "estado" ] } ``` --- # Archivo del evento (JSON) `GET https://app.openfactura.co/api/v1/eventos/{codigo}/json` Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Respuestas - **200**: Archivo del evento firmado, con sello. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Representación gráfica del evento (PDF) `GET https://app.openfactura.co/api/v1/eventos/{codigo}/pdf` Permiso: `dte:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `codigo` | path | sí | string | Código de generación (UUID) o id interno. | ## Respuestas - **200**: PDF del evento. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Listar catálogos del MH `GET https://app.openfactura.co/api/v1/catalogos` ## Respuestas - **200**: Catálogos disponibles. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Consultar un catálogo `GET https://app.openfactura.co/api/v1/catalogos/{id}` `CAT-012` departamentos, `CAT-013` municipios (filtra con `departamento`), `CAT-008` distritos, `CAT-019` actividades económicas (50 por defecto; usa `q`), `CAT-014` unidades, `CAT-017` formas de pago, `CAT-022` documentos. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | | `q` | query | no | string | | | `departamento` | query | no | string | | | `limite` | query | no | integer | | ## Respuestas - **200**: Valores del catálogo. Esquema: `Catalogo`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Catalogo ```json { "type": "object", "properties": { "id": { "type": "string" }, "nombre": { "type": "string" }, "total": { "type": "integer" }, "valores": { "type": "array", "items": { "type": "object", "properties": { "codigo": { "type": "string" }, "valor": { "type": "string" }, "departamento": { "type": "string" } }, "required": [ "codigo", "valor" ] } } } } ``` --- # Listar claves de API `GET https://app.openfactura.co/api/v1/claves` Permiso: `claves:manage`. ## Respuestas - **200**: Claves (nunca la clave completa). - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Clave ```json { "type": "object", "properties": { "id": { "type": "string" }, "nombre": { "type": "string" }, "prefijo": { "type": "string", "description": "Primeros caracteres de la clave." }, "modo": { "type": "string", "enum": [ "test", "live" ] }, "scopes": { "type": "array", "items": { "type": "string" } }, "expira": { "type": [ "integer", "null" ], "description": "Milisegundos desde 1970; nulo si no expira." }, "ultimoUso": { "type": [ "integer", "null" ] }, "revocada": { "type": [ "integer", "null" ], "description": "Cuándo se revocó; nulo si sigue vigente." }, "creado": { "type": "integer" }, "estado": { "type": "string", "enum": [ "activa", "revocada", "expirada" ] } } } ``` --- # Crear una clave de API `POST https://app.openfactura.co/api/v1/claves` La clave completa (`clave`) se devuelve sólo en esta respuesta. Una clave nunca puede llevar `emisor:write`, `claves:manage` ni `auditoria:read`. Permiso: `claves:manage`. ## Cuerpo (application/json, requerido) `ClaveApiEntrada` ## Respuestas - **201**: Clave creada. Esquema: `ClaveCreada`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### ClaveApiEntrada ```json { "type": "object", "properties": { "nombre": { "type": "string", "minLength": 1, "maxLength": 80 }, "modo": { "type": "string", "enum": [ "test", "live" ] }, "scopes": { "minItems": 1, "type": "array", "items": { "type": "string", "enum": [ "dte:read", "dte:emit", "dte:invalidate", "clientes:read", "clientes:write", "productos:read", "productos:write", "emisor:read", "webhooks:manage", "registros:read" ] } }, "expiraEnDias": { "anyOf": [ { "type": "integer", "minimum": 1, "maximum": 3650 }, { "type": "null" } ] } }, "required": [ "nombre", "modo", "scopes" ] } ``` ### ClaveCreada ```json { "allOf": [ { "$ref": "#/components/schemas/Clave" }, { "type": "object", "properties": { "clave": { "type": "string", "description": "La clave completa. Sólo esta vez." } }, "required": [ "clave" ] } ] } ``` ### Clave ```json { "type": "object", "properties": { "id": { "type": "string" }, "nombre": { "type": "string" }, "prefijo": { "type": "string", "description": "Primeros caracteres de la clave." }, "modo": { "type": "string", "enum": [ "test", "live" ] }, "scopes": { "type": "array", "items": { "type": "string" } }, "expira": { "type": [ "integer", "null" ], "description": "Milisegundos desde 1970; nulo si no expira." }, "ultimoUso": { "type": [ "integer", "null" ] }, "revocada": { "type": [ "integer", "null" ], "description": "Cuándo se revocó; nulo si sigue vigente." }, "creado": { "type": "integer" }, "estado": { "type": "string", "enum": [ "activa", "revocada", "expirada" ] } } } ``` --- # Revocar una clave `DELETE https://app.openfactura.co/api/v1/claves/{id}` Permiso: `claves:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Revocada de inmediato. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Listar webhooks `GET https://app.openfactura.co/api/v1/webhooks` Permiso: `webhooks:manage`. ## Respuestas - **200**: Webhooks de la organización (nunca el secreto). - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### Webhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "descripcion": { "type": [ "string", "null" ] }, "eventos": { "type": "array", "items": { "type": "string", "enum": [ "dte.procesado", "dte.rechazado", "dte.contingencia", "dte.invalidado", "retorno.procesado", "operaciones_especiales.procesado" ] } }, "activo": { "type": "boolean" }, "creado": { "type": "integer" }, "actualizado": { "type": [ "integer", "null" ] } }, "required": [ "id", "url", "eventos", "activo", "creado" ] } ``` --- # Crear un webhook `POST https://app.openfactura.co/api/v1/webhooks` Sólo `https://` y nunca destinos privados o locales. Máximo 10 por organización. El secreto (`whsec_…`) se devuelve sólo en esta respuesta. Permiso: `webhooks:manage`. ## Cuerpo (application/json, requerido) `WebhookEntrada` ## Respuestas - **201**: Webhook creado. Esquema: `WebhookConSecreto`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **409**: Falta configurar algo o hay un conflicto. Códigos: `sin_organizacion`, `sin_organizacion_activa`, `nit_bloqueado`, `emisor_no_configurado`, `credenciales_incompletas`, `sin_punto_venta`, `estado_invalido`, `ambiente_distinto`, `retorno_excedido`, `retorno_vigente`, `idempotencia_distinta`, `idempotencia_en_curso`, `cliente_repetido`, `producto_repetido`, `codigo_repetido`, `sin_archivo`, `limite_webhooks`, `simulador_inactivo`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### WebhookEntrada ```json { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2000 }, "descripcion": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "eventos": { "minItems": 1, "type": "array", "items": { "type": "string", "enum": [ "dte.procesado", "dte.rechazado", "dte.contingencia", "dte.invalidado", "retorno.procesado", "operaciones_especiales.procesado" ] } }, "activo": { "default": true, "type": "boolean" } }, "required": [ "url", "eventos" ] } ``` ### WebhookConSecreto ```json { "allOf": [ { "$ref": "#/components/schemas/Webhook" }, { "type": "object", "properties": { "secreto": { "type": "string", "description": "whsec_…: clave de la firma HMAC. Sólo esta vez." } }, "required": [ "secreto" ] } ] } ``` ### Webhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "descripcion": { "type": [ "string", "null" ] }, "eventos": { "type": "array", "items": { "type": "string", "enum": [ "dte.procesado", "dte.rechazado", "dte.contingencia", "dte.invalidado", "retorno.procesado", "operaciones_especiales.procesado" ] } }, "activo": { "type": "boolean" }, "creado": { "type": "integer" }, "actualizado": { "type": [ "integer", "null" ] } }, "required": [ "id", "url", "eventos", "activo", "creado" ] } ``` --- # Eliminar un webhook y sus entregas `DELETE https://app.openfactura.co/api/v1/webhooks/{id}` Permiso: `webhooks:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Eliminado. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. --- # Cambiar URL, descripción, eventos o activarlo/desactivarlo `PATCH https://app.openfactura.co/api/v1/webhooks/{id}` Permiso: `webhooks:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Cuerpo (application/json, requerido) `WebhookCambios` ## Respuestas - **200**: Webhook actualizado. Esquema: `Webhook`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### WebhookCambios ```json { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2000 }, "descripcion": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] }, "eventos": { "minItems": 1, "type": "array", "items": { "type": "string", "enum": [ "dte.procesado", "dte.rechazado", "dte.contingencia", "dte.invalidado", "retorno.procesado", "operaciones_especiales.procesado" ] } }, "activo": { "type": "boolean" } } } ``` ### Webhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "descripcion": { "type": [ "string", "null" ] }, "eventos": { "type": "array", "items": { "type": "string", "enum": [ "dte.procesado", "dte.rechazado", "dte.contingencia", "dte.invalidado", "retorno.procesado", "operaciones_especiales.procesado" ] } }, "activo": { "type": "boolean" }, "creado": { "type": "integer" }, "actualizado": { "type": [ "integer", "null" ] } }, "required": [ "id", "url", "eventos", "activo", "creado" ] } ``` --- # Generar un secreto nuevo `POST https://app.openfactura.co/api/v1/webhooks/{id}/rotar-secreto` El secreto anterior deja de valer de inmediato. El nuevo se devuelve sólo aquí. Permiso: `webhooks:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Webhook con el secreto nuevo. Esquema: `WebhookConSecreto`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### WebhookConSecreto ```json { "allOf": [ { "$ref": "#/components/schemas/Webhook" }, { "type": "object", "properties": { "secreto": { "type": "string", "description": "whsec_…: clave de la firma HMAC. Sólo esta vez." } }, "required": [ "secreto" ] } ] } ``` ### Webhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "descripcion": { "type": [ "string", "null" ] }, "eventos": { "type": "array", "items": { "type": "string", "enum": [ "dte.procesado", "dte.rechazado", "dte.contingencia", "dte.invalidado", "retorno.procesado", "operaciones_especiales.procesado" ] } }, "activo": { "type": "boolean" }, "creado": { "type": "integer" }, "actualizado": { "type": [ "integer", "null" ] } }, "required": [ "id", "url", "eventos", "activo", "creado" ] } ``` --- # Enviar un evento `webhook.prueba` ahora `POST https://app.openfactura.co/api/v1/webhooks/{id}/probar` Permiso: `webhooks:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Resultado del envío (no se reintenta). - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EntregaWebhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "webhookId": { "type": "string" }, "evento": { "type": "string" }, "estado": { "type": "string", "enum": [ "pendiente", "entregado", "fallido" ] }, "intentos": { "type": "integer" }, "ultimoStatus": { "type": [ "integer", "null" ] }, "ultimoError": { "type": [ "string", "null" ] }, "proximoIntento": { "type": [ "integer", "null" ] }, "creado": { "type": "integer" }, "entregado": { "type": [ "integer", "null" ] } }, "required": [ "id", "webhookId", "evento", "estado", "intentos", "creado" ] } ``` --- # Últimas 50 entregas de un webhook `GET https://app.openfactura.co/api/v1/webhooks/{id}/entregas` Permiso: `webhooks:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Entregas, de la más reciente a la más antigua (sin el cuerpo). - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EntregaWebhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "webhookId": { "type": "string" }, "evento": { "type": "string" }, "estado": { "type": "string", "enum": [ "pendiente", "entregado", "fallido" ] }, "intentos": { "type": "integer" }, "ultimoStatus": { "type": [ "integer", "null" ] }, "ultimoError": { "type": [ "string", "null" ] }, "proximoIntento": { "type": [ "integer", "null" ] }, "creado": { "type": "integer" }, "entregado": { "type": [ "integer", "null" ] } }, "required": [ "id", "webhookId", "evento", "estado", "intentos", "creado" ] } ``` --- # Reenviar una entrega ahora `POST https://app.openfactura.co/api/v1/webhooks/entregas/{id}/reenviar` Mismo cuerpo, firma nueva; reinicia el calendario de reintentos. Permiso: `webhooks:manage`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `id` | path | sí | string | Identificador del recurso. | ## Respuestas - **200**: Entrega tras el envío. Esquema: `EntregaWebhook`. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **404**: El recurso no existe en esta organización. Códigos: `no_encontrado`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### EntregaWebhook ```json { "type": "object", "properties": { "id": { "type": "string" }, "webhookId": { "type": "string" }, "evento": { "type": "string" }, "estado": { "type": "string", "enum": [ "pendiente", "entregado", "fallido" ] }, "intentos": { "type": "integer" }, "ultimoStatus": { "type": [ "integer", "null" ] }, "ultimoError": { "type": [ "string", "null" ] }, "proximoIntento": { "type": [ "integer", "null" ] }, "creado": { "type": "integer" }, "entregado": { "type": [ "integer", "null" ] } }, "required": [ "id", "webhookId", "evento", "estado", "intentos", "creado" ] } ``` --- # Bitácora de auditoría `GET https://app.openfactura.co/api/v1/auditoria` Paginación hacia atrás con `antesDe` (marca de tiempo en milisegundos). Permiso: `auditoria:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `antesDe` | query | no | integer | | | `limite` | query | no | integer | | ## Respuestas - **200**: Registros, del más reciente al más antiguo. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### RegistroAuditoria ```json { "type": "object", "properties": { "id": { "type": "string" }, "actorTipo": { "type": "string" }, "actorId": { "type": "string" }, "accion": { "type": "string" }, "recurso": { "type": "string" }, "recursoId": { "type": [ "string", "null" ] }, "ip": { "type": [ "string", "null" ] }, "detalle": { "type": [ "object", "null" ] }, "creado": { "type": [ "string", "integer" ] } } } ``` --- # Registros de peticiones de la API `GET https://app.openfactura.co/api/v1/registros` Peticiones hechas con claves de API (REST y MCP) en los últimos 30 días: método, patrón de ruta, estado HTTP, duración y clave. Nunca guarda cuerpos, encabezados ni parámetros. Paginación hacia atrás con `antesDe` (marca de tiempo en milisegundos). Permiso: `registros:read`. ## Parámetros | Nombre | En | Requerido | Tipo | Descripción | | --- | --- | --- | --- | --- | | `antesDe` | query | no | integer | | | `limite` | query | no | integer | | | `estado` | query | no | string | | | `origen` | query | no | string | | | `claveId` | query | no | string | | ## Respuestas - **200**: Peticiones, de la más reciente a la más antigua. - **400**: Cuerpo o parámetros inválidos (`details` = `[{ ruta, mensaje }]`), o el DTE no cumple el esquema del MH (`details.problemas`). Códigos: `datos_invalidos`, `dte_invalido`. Esquema: `Error`. - **401**: Falta sesión, o la clave de API no es válida, fue revocada o expiró. Códigos: `no_autenticado`. Esquema: `Error`. - **403**: Falta un permiso (`details.permiso`) o el origen de la petición con cookie no está permitido. Códigos: `sin_permiso`, `origen_no_permitido`. Esquema: `Error`. - **429**: Límite de peticiones excedido (`Retry-After` indica cuántos segundos esperar) o, en distribuciones con planes, límite del plan alcanzado (`limite_plan`). Códigos: `limite_excedido`, `limite_plan`. Esquema: `Error`. ## Esquemas ### RegistroApi ```json { "type": "object", "properties": { "id": { "type": "string" }, "creado": { "type": "integer", "description": "Marca de tiempo en milisegundos." }, "metodo": { "type": "string" }, "ruta": { "type": "string", "description": "Patrón de la ruta, p. ej. `/api/v1/dte/:codigo`." }, "estado": { "type": "integer", "description": "Estado HTTP de la respuesta." }, "duracionMs": { "type": "integer" }, "origen": { "type": "string", "enum": [ "api", "mcp" ] }, "clave": { "type": "object", "properties": { "id": { "type": "string" }, "prefijo": { "type": [ "string", "null" ] }, "nombre": { "type": [ "string", "null" ] } } } } } ``` --- # Facturar desde WhatsApp > Disponible en Open Factura Cloud. Emite y consulta facturas desde WhatsApp. Con **Open Factura Cloud** puedes facturar desde WhatsApp, sin abrir el panel: - Vincula tu teléfono a tu usuario desde el panel. - Emite facturas y comprobantes de crédito fiscal con menús guiados: cliente, productos y resumen validado por Hacienda antes de confirmar. - Recibe el PDF en la misma conversación y consulta tus últimos documentos. - El bot actúa con los mismos permisos de tu usuario, en el ambiente activo de tu empresa. Esta función es exclusiva de la versión administrada y no forma parte del código abierto. Conoce los planes en [openfactura.co](https://openfactura.co).