# Consultar un comprobante especial Source: https://ac-core.mintlify.app/api-reference/comprobantes-especiales/consultar-un-comprobante-especial /api-reference/ecf.json get /v1/special-documents/{id} Devuelve el comprobante y su estado ante la DGII. Si está `pending`, la consulta actualiza el estado. # Descargar el XML firmado de un comprobante especial Source: https://ac-core.mintlify.app/api-reference/comprobantes-especiales/descargar-el-xml-firmado-de-un-comprobante-especial /api-reference/ecf.json get /v1/special-documents/{id}/xml XML del comprobante tal como se envió a la DGII, como adjunto `.xml`. # Emitir un comprobante especial Source: https://ac-core.mintlify.app/api-reference/comprobantes-especiales/emitir-un-comprobante-especial /api-reference/ecf.json post /v1/special-documents Emite un e-CF 41 (compras a proveedores que no emiten comprobantes), 43 (gastos menores), 46 (exportaciones) o 47 (pagos al exterior), lo firma y lo envía a la DGII. La respuesta suele llegar con `status: pending`; el estado se actualiza solo: recíbelo con un webhook o consúltalo con `GET /v1/special-documents/{id}`. **Límite mensual:** si alcanzaste el límite mensual de este tipo de e-CF responde `429` **sin** `Retry-After` (el `429` por exceso de solicitudes sí lo trae); consulta tu consumo con `GET /v1/limits`. Consultar, descargar y anular siguen funcionando, y reintentar un documento ya emitido no vuelve a contar. Ese `429` no se guarda: cuando se amplíe el límite puedes reintentar con la misma `Idempotency-Key`. # Reintentar el envío de un comprobante especial Source: https://ac-core.mintlify.app/api-reference/comprobantes-especiales/reintentar-el-envío-de-un-comprobante-especial /api-reference/ecf.json post /v1/special-documents/{id}/retry Reenvía a la DGII, con el mismo e-NCF, un comprobante cuyo envío falló (`status: error`). Uno `rejected` no se reintenta: corrige los datos y emite otro. No lleva cuerpo. # Estado del servicio Source: https://ac-core.mintlify.app/api-reference/estado/estado-del-servicio /api-reference/ecf.json get /v1/health No requiere autenticación. # Anular un e-CF Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/anular-un-e-cf /api-reference/ecf.json post /v1/invoices/{id}/void Anula ante la DGII un e-CF aceptado. No lleva cuerpo. Devuelve el documento con `status: cancelled`. # Consultar un e-CF Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/consultar-un-e-cf /api-reference/ecf.json get /v1/invoices/{id} Devuelve el documento y su estado ante la DGII. Si está `pending`, la consulta actualiza el estado. # Descargar el XML firmado Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/descargar-el-xml-firmado /api-reference/ecf.json get /v1/invoices/{id}/xml XML del e-CF tal como se envió a la DGII, como adjunto `.xml`. # Descargar la representación impresa (PDF) Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/descargar-la-representación-impresa-pdf /api-reference/ecf.json get /v1/invoices/{id}/pdf Archivo `.pdf` como adjunto. # Emitir un e-CF de venta Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/emitir-un-e-cf-de-venta /api-reference/ecf.json post /v1/invoices Emite una factura electrónica (31, 32, 44 o 45), la firma y la envía a la DGII. La respuesta suele llegar con `status: pending`; el estado se actualiza solo hasta `accepted`, `accepted_with_observations` o `rejected`: recíbelo con un webhook o consúltalo con `GET /v1/invoices/{id}`. **Límite mensual:** si alcanzaste el límite mensual de este tipo de e-CF responde `429` **sin** `Retry-After` (el `429` por exceso de solicitudes sí lo trae); consulta tu consumo con `GET /v1/limits`. Consultar, descargar y anular siguen funcionando, y reintentar un documento ya emitido no vuelve a contar. Ese `429` no se guarda: cuando se amplíe el límite puedes reintentar con la misma `Idempotency-Key`. # Listar e-CF de venta Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/listar-e-cf-de-venta /api-reference/ecf.json get /v1/invoices Devuelve, paginados, los e-CF de venta de tu comercio. Filtra por estado, tipo, rango de fecha de emisión (inclusive) o comprador. # Reintentar el envío de un e-CF Source: https://ac-core.mintlify.app/api-reference/facturas-electrónicas-e-cf/reintentar-el-envío-de-un-e-cf /api-reference/ecf.json post /v1/invoices/{id}/retry Reenvía a la DGII, con el mismo e-NCF, un e-CF cuyo envío falló (`status: error`). Úsalo en vez de emitir uno nuevo para no dejar huecos en tu secuencia. Un e-CF `rejected` no se reintenta: corrige los datos y emite otro. No lleva cuerpo. # Consultar los límites mensuales de emisión Source: https://ac-core.mintlify.app/api-reference/límites/consultar-los-límites-mensuales-de-emisión /api-reference/ecf.json get /v1/limits Límite mensual de cada tipo de e-CF, lo emitido en el mes en curso y lo que queda disponible. Solo aparecen los tipos con límite; un tipo que no aparece no tiene límite. En el ambiente de pruebas `enforced` es `false`: los límites no se aplican. # Consultar una nota Source: https://ac-core.mintlify.app/api-reference/notas-de-crédito-y-débito/consultar-una-nota /api-reference/ecf.json get /v1/notes/{id} Devuelve la nota y su estado ante la DGII. Si está `pending`, la consulta actualiza el estado. # Descargar el XML firmado de una nota Source: https://ac-core.mintlify.app/api-reference/notas-de-crédito-y-débito/descargar-el-xml-firmado-de-una-nota /api-reference/ecf.json get /v1/notes/{id}/xml XML de la nota tal como se envió a la DGII, como adjunto `.xml`. # Emitir una nota de crédito (E34) Source: https://ac-core.mintlify.app/api-reference/notas-de-crédito-y-débito/emitir-una-nota-de-crédito-e34 /api-reference/ecf.json post /v1/invoices/{id}/credit-notes Emite una nota de crédito que modifica la factura `{id}` (descuentos, devoluciones o anulación). Se emite por líneas (`items`) o por monto (`amount`), no con ambos. La respuesta suele llegar con `status: pending`; el estado se actualiza solo: recíbelo con un webhook o consúltalo con `GET /v1/notes/{id}`. **Límite mensual:** si alcanzaste el límite mensual de este tipo de e-CF responde `429` **sin** `Retry-After` (el `429` por exceso de solicitudes sí lo trae); consulta tu consumo con `GET /v1/limits`. Consultar, descargar y anular siguen funcionando, y reintentar un documento ya emitido no vuelve a contar. Ese `429` no se guarda: cuando se amplíe el límite puedes reintentar con la misma `Idempotency-Key`. # Emitir una nota de débito (E33) Source: https://ac-core.mintlify.app/api-reference/notas-de-crédito-y-débito/emitir-una-nota-de-débito-e33 /api-reference/ecf.json post /v1/invoices/{id}/debit-notes Emite una nota de débito que modifica la factura `{id}` (cargos o intereses no facturados). Se emite por líneas (`items`) o por monto (`amount`), no con ambos. La respuesta suele llegar con `status: pending`; el estado se actualiza solo: recíbelo con un webhook o consúltalo con `GET /v1/notes/{id}`. **Límite mensual:** si alcanzaste el límite mensual de este tipo de e-CF responde `429` **sin** `Retry-After` (el `429` por exceso de solicitudes sí lo trae); consulta tu consumo con `GET /v1/limits`. Consultar, descargar y anular siguen funcionando, y reintentar un documento ya emitido no vuelve a contar. Ese `429` no se guarda: cuando se amplíe el límite puedes reintentar con la misma `Idempotency-Key`. # Listar las notas de una factura Source: https://ac-core.mintlify.app/api-reference/notas-de-crédito-y-débito/listar-las-notas-de-una-factura /api-reference/ecf.json get /v1/invoices/{id}/notes Notas de crédito y débito que modifican la factura `{id}`, de la más reciente a la más antigua. # Reintentar el envío de una nota Source: https://ac-core.mintlify.app/api-reference/notas-de-crédito-y-débito/reintentar-el-envío-de-una-nota /api-reference/ecf.json post /v1/notes/{id}/retry Reenvía a la DGII, con el mismo e-NCF, una nota cuyo envío falló (`status: error`). Una nota `rejected` no se reintenta: corrige los datos y emite otra. No lleva cuerpo. # Listar las secuencias de e-NCF Source: https://ac-core.mintlify.app/api-reference/secuencias/listar-las-secuencias-de-e-ncf /api-reference/ecf.json get /v1/sequences Rangos de e-NCF autorizados a tu comercio por tipo de e-CF, con el próximo número, los disponibles y su vencimiento. Revisa `usable` antes de emitir un tipo. # Borrar un webhook Source: https://ac-core.mintlify.app/api-reference/webhooks/borrar-un-webhook /api-reference/ecf.json delete /v1/webhooks/{id} Borra el webhook: deja de recibir eventos, incluidos los reintentos pendientes. Responde `204` sin cuerpo. Para dejar de recibir eventos temporalmente, usa `PATCH` con `active: false`. # Consultar un webhook Source: https://ac-core.mintlify.app/api-reference/webhooks/consultar-un-webhook /api-reference/ecf.json get /v1/webhooks/{id} Devuelve el webhook (sin su secreto) y sus entregas más recientes, con el estado y los intentos de cada una. # Enviar un evento de prueba Source: https://ac-core.mintlify.app/api-reference/webhooks/enviar-un-evento-de-prueba /api-reference/ecf.json post /v1/webhooks/{id}/test Envía ahora a la URL del webhook un evento `test` (`data: { "message": "..." }`), firmado igual que los reales, y devuelve si tu endpoint respondió `2xx`. Úsalo para probar tu verificación de firma. No lleva cuerpo. # Listar los webhooks Source: https://ac-core.mintlify.app/api-reference/webhooks/listar-los-webhooks /api-reference/ecf.json get /v1/webhooks Webhooks registrados por tu comercio (sin su secreto). # Modificar un webhook Source: https://ac-core.mintlify.app/api-reference/webhooks/modificar-un-webhook /api-reference/ecf.json patch /v1/webhooks/{id} Cambia la URL, los eventos o si está activo (`active: false` lo pausa sin borrarlo). Solo se modifican los campos enviados; envía al menos uno. No exige `Idempotency-Key`: repetir la misma solicitud deja el webhook igual. El secreto no cambia. # Registrar un webhook Source: https://ac-core.mintlify.app/api-reference/webhooks/registrar-un-webhook /api-reference/ecf.json post /v1/webhooks Registra una URL HTTPS que recibirá los eventos indicados. La respuesta incluye `secret`, con el que verificas la firma `X-Acpay-Signature` de cada evento: **solo se devuelve aquí**, guárdalo de inmediato. Ver "Cómo recibir y verificar los eventos" en esta sección. # Errores y límites Source: https://ac-core.mintlify.app/guias/errores-y-limites Forma de los errores, códigos, reintentos y límites mensuales. ## Forma de los errores Todas las respuestas de error tienen la misma forma: ```json theme={null} { "error": { "status": 400, "message": "La solicitud no es válida.", "details": [ "items.0.quantity: debe ser un número entero.", "dueDate: es obligatoria cuando paymentType es credit." ] } } ``` `details` aparece solo cuando hay errores por campo. Cada respuesta, también las de error, trae `X-Request-Id`: inclúyelo cuando nos reportes un problema. Puedes enviar el tuyo en la solicitud. ## Códigos | Código | Significado | ¿Reintentar? | | - | - | - | | `400` | Solicitud inválida: cuerpo, parámetros o `Idempotency-Key`. | No sin corregirla. Después de corregirla, con la misma `Idempotency-Key`. | | `401` | Llave ausente, con formato inválido, de otro ambiente, vencida o revocada. | No. Revisa la llave y la URL base. | | `403` | La llave no tiene permiso para esta operación, o el comercio está inactivo. | No. | | `404` | El documento no existe o no es de tu comercio. | No. | | `409` | Una solicitud con la misma `Idempotency-Key` sigue en curso, o el estado del documento no permite la operación (por ejemplo, anular uno que no está aceptado). | Si es por la `Idempotency-Key`, sí: con la misma llave, en unos segundos. Si es por el estado, no. | | `422` | La `Idempotency-Key` ya se usó con otra solicitud. | No: usa una llave nueva. | | `429` | Límite de solicitudes por minuto o límite mensual de comprobantes. | Ver abajo. | | `5xx` (`500`, `502`, `503`, `504`) | Error inesperado o falta de disponibilidad del servicio de facturación. No se sabe si la solicitud llegó. | Sí, con la misma `Idempotency-Key`: ningún `5xx` se guarda y el reintento no emite un segundo comprobante. | ## Los dos `429` * **Solicitudes por minuto de tu llave:** el `429` trae `Retry-After` (segundos a esperar) y, cuando están disponibles, los encabezados `X-RateLimit-*`. Espera y reintenta. * **Comprobantes por mes de cada tipo de e-CF** (solo al emitir): el `429` **no** trae `Retry-After` y su mensaje dice cuándo se renueva. No se resuelve reintentando: espera la renovación o pide ampliarlo. ```json theme={null} { "error": { "status": 429, "message": "Alcanzaste el límite mensual de comprobantes 31 (500). Se renueva el 1 de octubre; para ampliarlo contacta a AC-PAY." } } ``` Ningún `429` se guarda como respuesta idempotente: cuando puedas volver a emitir, reintenta con la misma `Idempotency-Key`. ## Límites mensuales `GET /v1/limits` muestra, por tipo de e-CF, tu límite, lo emitido y lo disponible en el mes. * Solo aparecen los tipos con límite; un tipo que no aparece no tiene límite. * El mes se cuenta en hora de República Dominicana y se renueva al inicio del mes siguiente (`resetsAt`). * Te avisamos por correo al llegar al 80 % y al 100 % del límite de cada tipo, una vez por mes. * Consultar, descargar y anular documentos sigue funcionando con el límite alcanzado, y reintentar un documento ya emitido no vuelve a contar. * **En el sandbox los límites no se aplican** (`enforced: false`). # Estados de un e-CF Source: https://ac-core.mintlify.app/guias/estados Qué significa cada estado de un e-CF ante la DGII y qué hacer en cada caso. Cada comprobante (factura, nota o comprobante especial) tiene un `status` que refleja su situación ante la DGII. AC-PAY lo actualiza solo, en segundo plano: no necesitas consultar en bucle. | Estado | Qué significa | Qué hacer | | - | - | - | | `pending` | Firmado y enviado; la DGII todavía no responde. | Esperar el webhook, o consultar el documento más tarde. | | `accepted` | La DGII lo aceptó. Es válido. | Entregarlo al cliente (PDF o XML). | | `accepted_with_observations` | Aceptado, con observaciones en `statusMessage`. Es válido. | Revisar las observaciones para los próximos comprobantes. | | `rejected` | La DGII lo rechazó. El motivo está en `statusMessage`. | Corregir los datos y emitir un comprobante nuevo (con otra `Idempotency-Key`). | | `cancelled` | Anulado. | Nada: ya no tiene efecto. | | `error` | No se pudo procesar el envío. | Reintentarlo con `POST .../{id}/retry`: se reenvía con el mismo e-NCF. | ## El recorrido normal 1. `POST /v1/invoices` responde `201` con `status: pending`. 2. La DGII responde y el estado cambia a `accepted`, `accepted_with_observations` o `rejected`. 3. Si registraste un webhook, recibes `ecf.accepted` o `ecf.rejected` con el nuevo estado. ## Rechazado o con error: no es lo mismo * **`rejected`** es una decisión de la DGII sobre el contenido. Repetir el mismo comprobante no lo arregla: corrige los datos y emite uno nuevo. * **`error`** es un fallo del envío, no del contenido. `POST /v1/invoices/{id}/retry` (o `/v1/notes/{id}/retry`, `/v1/special-documents/{id}/retry`) lo reenvía con el mismo e-NCF, sin cuerpo. ## Anular y corregir * **Anular** un e-CF aceptado: `POST /v1/invoices/{id}/void`, sin cuerpo. Recibes `ecf.cancelled` por webhook. * **Corregir** montos o textos de una factura ya aceptada: emite una nota de crédito (`POST /v1/invoices/{id}/credit-notes`, E34) o de débito (`POST /v1/invoices/{id}/debit-notes`, E33). Cada nota tiene su propio estado y su propio recorrido. `modificationCode` de las notas: `1` anulación total, `2` corrige textos, `3` corrige montos, `4` reemplazo de un comprobante en contingencia. Se emiten por líneas (`items`) o por monto (`amount` con ITBIS y `itbis` opcional), nunca con ambos. ## Tipos de e-CF | Endpoint | Tipos | | - | - | | `POST /v1/invoices` | `31` crédito fiscal, `32` consumo, `44` regímenes especiales, `45` gubernamental | | `POST /v1/invoices/{id}/credit-notes` | `34` nota de crédito | | `POST /v1/invoices/{id}/debit-notes` | `33` nota de débito | | `POST /v1/special-documents` | `41` compras, `43` gastos menores, `46` exportaciones, `47` pagos al exterior | # Idempotencia Source: https://ac-core.mintlify.app/guias/idempotencia Cómo reintentar una solicitud sin emitir dos comprobantes. Una emisión es una operación fiscal: si la repites por error, quedan dos comprobantes. El encabezado `Idempotency-Key` hace que repetir una solicitud sea seguro. ## Cómo funciona * **Todos los `POST`** la exigen: emitir, anular, reintentar, notas, comprobantes especiales, registrar y probar webhooks. `PATCH` y `DELETE` no la usan: repetirlos deja el mismo resultado. * La generas tú, una por operación: un UUID sirve (de 1 a 255 caracteres ASCII imprimibles). * Si repites la **misma solicitud con la misma llave** en las siguientes 24 horas, recibes la respuesta original con `Idempotent-Replayed: true`, sin que se ejecute de nuevo. | Situación | Respuesta | | - | - | | Primera vez | El resultado normal (por ejemplo `201`). | | Misma llave y misma solicitud, ya terminada | La respuesta original, con `Idempotent-Replayed: true`. | | Misma llave y misma solicitud, la primera aún en curso | `409`: espera y reintenta. | | Misma llave con otra solicitud | `422`: usa una llave nueva para una operación distinta. | ## Qué se guarda y qué no No se guardan los `401`, `403`, `409`, `429` ni los `5xx`, ni los `400` por errores de validación del cuerpo. Después de cualquiera de ellos puedes reintentar con la **misma** llave; por ejemplo, después de corregir el cuerpo o de esperar el `Retry-After`. Si una emisión vence (`503`) después de llegar a AC-PAY, el reintento con la misma `Idempotency-Key` no emite otro comprobante: devuelve el ya emitido. Antes de repetir una respuesta guardada se comprueba que tu llave de API siga vigente: con una llave revocada recibes `401`. ## Cuánto dura y qué cuenta como la misma solicitud * **Ámbito:** la llave es de tu comercio. La misma llave en otro comercio no choca con la tuya. * **Misma solicitud** quiere decir mismo método, misma ruta y mismo cuerpo. El cuerpo se compara como JSON con las claves ordenadas: el orden de las propiedades y los espacios no importan; los valores, sí. * **Las 24 horas** se cuentan desde que la primera solicitud terminó. En ese plazo recibes la respuesta original tal cual, incluidos los errores `4xx` que se guardan. * **Emisiones y registro de webhooks, 30 días:** al emitir una factura, una nota o un comprobante especial, y al registrar un webhook, AC-PAY asocia la llave al recurso creado y la conserva 30 días. Si repites la solicitud con la misma llave después de las 24 horas, pero dentro de esos 30 días, recibes el mismo recurso con `Idempotent-Replayed: true`, no uno nuevo. ### Si perdiste la respuesta Repite la solicitud con la misma llave y el mismo cuerpo, aunque hayan pasado horas o días (hasta 30 en las emisiones): recibes el documento ya emitido, o lo emite si la primera nunca llegó. Por eso la llave tiene que vivir en tu base de datos junto al documento, no solo en memoria. Si después de reintentar sigues recibiendo `409`, la primera solicitud no terminó de registrarse. No cambies de llave, porque podrías emitir dos veces: reporta el `X-Request-Id` a AC-PAY. ## Cómo usarla bien 1. **Genera la llave antes del primer intento y guárdala junto a tu documento** (por ejemplo, en la fila de la factura en tu base de datos). 2. **Reintenta siempre con la misma llave** cuando no sabes si la primera solicitud llegó: un timeout, un corte de red o cualquier `5xx` (`500`, `502`, `503`, `504`). 3. **Usa una llave nueva** solo para una operación nueva: otra factura, o la misma factura corregida después de un `rejected`. ```js theme={null} // Reintento seguro: la misma llave en cada intento. async function issueWithRetry(invoice, idempotencyKey, attempts = 3) { for (let attempt = 1; attempt <= attempts; attempt += 1) { const response = await fetch(`${BASE_URL}/invoices`, { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}`, 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json', }, body: JSON.stringify(invoice), }); // 409: la primera solicitud con esta llave sigue en curso. 5xx: no se // sabe si llegó. En los dos casos se repite con la misma llave. const retryable = response.status === 409 || response.status >= 500; if (!retryable || attempt === attempts) { return response; } await new Promise((resolve) => setTimeout(resolve, attempt * 2000)); } } ``` Para un `429`, respeta `Retry-After` si viene. Si no trae `Retry-After`, es el límite mensual y no se resuelve reintentando: ver [Errores y límites](/guias/errores-y-limites). # Inicio rápido Source: https://ac-core.mintlify.app/guias/inicio-rapido Tu primera factura electrónica en el sandbox, en cinco pasos. El sandbox envía los comprobantes al ambiente de pruebas de la DGII: no tienen validez fiscal y no cuentan para tus límites mensuales. | Ambiente | URL base | Llave | | - | - | - | | Sandbox (pruebas) | `https://e-cf.acpay.do/sandbox/v1` | `acpay_test_...` | | Producción | `https://e-cf.acpay.do/v1` | `acpay_live_...` | > [!NOTE] > > Una llave de un ambiente se rechaza en el otro con `401`. Mientras integras, usa solo la llave de prueba y la URL del sandbox. **1. Consigue tu llave de prueba** Pide a AC-PAY una llave `acpay_test_...` para tu comercio. Guárdala como un secreto de tu servidor (por ejemplo, en una variable de entorno), nunca en el código ni en una aplicación web o móvil. ```bash theme={null} export ACPAY_TEST_KEY="acpay_test_..." ``` Envíala en cada solicitud: `Authorization: Bearer $ACPAY_TEST_KEY`. **2. Revisa tus secuencias** Para emitir necesitas una secuencia de e-NCF vigente del tipo que vas a usar (`usable: true`). ```bash theme={null} curl https://e-cf.acpay.do/sandbox/v1/sequences \ -H "Authorization: Bearer $ACPAY_TEST_KEY" ``` **3. Emite una factura** Cada `POST` lleva un `Idempotency-Key` único que generas tú (un UUID). Si la red falla, repites la solicitud con la misma llave y no se emite un segundo comprobante: ver [Idempotencia](/guias/idempotencia). ```bash cURL theme={null} curl -X POST https://e-cf.acpay.do/sandbox/v1/invoices \ -H "Authorization: Bearer $ACPAY_TEST_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "ecfType": 32, "paymentType": "cash", "items": [ { "description": "Almuerzo ejecutivo", "quantity": 3, "unitPrice": 590, "priceIncludesItbis": true } ] }' ``` ```javascript Node.js theme={null} import { randomUUID } from 'node:crypto'; const BASE_URL = 'https://e-cf.acpay.do/sandbox/v1'; // Genera la llave una vez por factura y guárdala con ella: si reintentas, // reutilízala para no emitir dos veces. const idempotencyKey = randomUUID(); const response = await fetch(`${BASE_URL}/invoices`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ACPAY_TEST_KEY}`, 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ ecfType: 32, paymentType: 'cash', items: [ { description: 'Almuerzo ejecutivo', quantity: 3, unitPrice: 590, priceIncludesItbis: true }, ], }), }); const body = await response.json(); if (!response.ok) { // Siempre { error: { status, message, details? } } throw new Error(`${response.status}: ${body.error.message}`); } // body.id: guárdalo para consultar el estado. ``` ```python Python theme={null} import os import uuid import requests BASE_URL = "https://e-cf.acpay.do/sandbox/v1" # Genera la llave una vez por factura y guárdala con ella: si reintentas, # reutilízala para no emitir dos veces. idempotency_key = str(uuid.uuid4()) response = requests.post( f"{BASE_URL}/invoices", headers={ "Authorization": f"Bearer {os.environ['ACPAY_TEST_KEY']}", "Idempotency-Key": idempotency_key, }, json={ "ecfType": 32, "paymentType": "cash", "items": [ {"description": "Almuerzo ejecutivo", "quantity": 3, "unitPrice": 590, "priceIncludesItbis": True} ], }, timeout=60, ) body = response.json() if not response.ok: # Siempre {"error": {"status", "message", "details"?}} raise RuntimeError(f"{response.status_code}: {body['error']['message']}") # body["id"]: guárdalo para consultar el estado. ``` La respuesta (`201`) trae el `id` del documento y casi siempre `status: pending`: ya se firmó y se envió a la DGII, que todavía no responde. Guarda el `id`. **4. Espera el resultado** El estado se actualiza solo, en segundo plano. Tienes dos formas de enterarte: * **Webhook (recomendado):** registra una URL HTTPS de tu servidor y recibes `ecf.accepted` o `ecf.rejected`. Ver [Webhooks](/guias/webhooks). * **Consulta:** `GET /v1/invoices/{id}` devuelve el estado actual (y lo actualiza al momento si sigue `pending`). ```bash theme={null} curl https://e-cf.acpay.do/sandbox/v1/invoices/ \ -H "Authorization: Bearer $ACPAY_TEST_KEY" ``` `accepted` o `accepted_with_observations` es un comprobante válido. `rejected` trae el motivo en `statusMessage`. Ver [Estados de un e-CF](/guias/estados). **5. Descarga el PDF y el XML** ```bash theme={null} curl -OJ https://e-cf.acpay.do/sandbox/v1/invoices//pdf -H "Authorization: Bearer $ACPAY_TEST_KEY" curl -OJ https://e-cf.acpay.do/sandbox/v1/invoices//xml -H "Authorization: Bearer $ACPAY_TEST_KEY" ``` ## Pruébalo sin escribir código En la pestaña **Referencia de la API**, cada endpoint tiene un panel para probarlo: pega tu llave de prueba, completa los campos y envía la solicitud al sandbox. Las solicitudes van directo de tu navegador al sandbox de AC-PAY, sin pasar por terceros. > [!WARNING] > > En el panel de pruebas usa solo tu llave `acpay_test_...`. Nunca pegues una llave de producción en un sitio web. # Integrar con un asistente de IA Source: https://ac-core.mintlify.app/guias/integrar-con-ia Toda la documentación en Markdown para que tu asistente de código (Claude, ChatGPT, Cursor, Copilot) integre el API sin adivinar. Esta documentación está disponible en Markdown para que la lea un asistente de IA. Dale la documentación completa en vez de pedirle que adivine: así usa las rutas, campos y reglas reales del API. ## Archivos para tu asistente | Archivo | Contenido | Úsalo para | | - | - | - | | [`/llms.txt`](https://dev.acpay.do/llms.txt) | Índice de todas las páginas, con enlaces a su versión en Markdown. | Que el asistente elija qué leer. | | [`/llms-full.txt`](https://dev.acpay.do/llms-full.txt) | Toda la documentación en un solo archivo de texto. | Pegarla completa en el contexto. | | Cualquier página + `.md` | La página en Markdown. Por ejemplo, [`/guias/inicio-rapido.md`](https://dev.acpay.do/guias/inicio-rapido.md). | Darle solo lo que necesita. | | Especificación OpenAPI | El contrato completo del API. Descárgala desde el menú de cualquier página de la referencia. | Generar un cliente o validar tipos. | En cada página, el menú de la esquina superior te deja copiarla en Markdown o abrirla directamente en ChatGPT, Claude o Cursor. ## Instrucción sugerida Copia esto en tu asistente y ajusta el lenguaje y el framework: ```text theme={null} Integra el API de facturación electrónica (e-CF) de AC-PAY en este proyecto. Lee primero la documentación completa: https://dev.acpay.do/llms-full.txt Requisitos: - Lenguaje y framework: . - Usa el sandbox: https://e-cf.acpay.do/sandbox/v1, con la llave en la variable de entorno ACPAY_TEST_KEY (nunca escrita en el código). - Cada POST lleva Idempotency-Key: genérala una vez por documento, guárdala con él y reutilízala en los reintentos. - Reintenta solo lo que la documentación dice que se puede reintentar (409 por Idempotency-Key, cualquier 5xx, 429 con Retry-After), con la misma Idempotency-Key. - Recibe el estado por webhook: verifica la firma HMAC sobre el cuerpo crudo, rechaza entregas de más de 5 minutos y deduplica por deliveryId. - Maneja los errores con la forma { error: { status, message, details } }. - Escribe pruebas para la emisión, el reintento idempotente y la verificación del webhook. ``` ## Cuida tu llave > [!WARNING] > > Nunca le des a un asistente de IA tu llave `acpay_live_...`, ni la pegues en un chat. Si el asistente necesita probar contra el API, usa la llave de prueba desde una variable de entorno de tu máquina. Revisa el código que genere antes de usarlo: que no escriba la llave en el código ni en los logs, y que no emita un comprobante nuevo en cada reintento. # Pasar a producción Source: https://ac-core.mintlify.app/guias/pasar-a-produccion Lista de verificación antes de cambiar a tu llave de producción. Cuando tu integración funcione en el sandbox, revisa esta lista antes de cambiar a producción. ## Lista de verificación 1. **Emites y recibes el resultado:** facturas de los tipos que vas a usar llegan a `accepted` y manejas `rejected` mostrando `statusMessage`. 2. **Idempotencia:** guardas la `Idempotency-Key` con cada documento y la reutilizas en los reintentos. Probaste un timeout: el reintento no duplica el comprobante. 3. **Webhooks:** verificas la firma sobre el cuerpo crudo, rechazas entregas de más de 5 minutos, deduplicas por `deliveryId` y respondes `2xx` en menos de 10 segundos. 4. **Errores:** distingues los dos `429` (con y sin `Retry-After`) y reintentas solo lo que se puede reintentar. Ver [Errores y límites](/guias/errores-y-limites). 5. **Notas y anulaciones:** si las vas a usar, probaste una nota de crédito y una anulación. 6. **Trazabilidad:** registras el `X-Request-Id` de cada respuesta en tus logs (nunca la llave). ## El cambio 1. Pide tu llave `acpay_live_...`. Guárdala en el gestor de secretos de tu servidor de producción, separada de la de pruebas. 2. Cambia la URL base de `https://e-cf.acpay.do/sandbox/v1` a `https://e-cf.acpay.do/v1`. 3. Registra tus webhooks también en producción: los del sandbox no se copian, y cada uno tiene su propio `secret`. Las llaves de prueba no funcionan en producción, ni al revés (`401`). Los documentos del sandbox no tienen validez fiscal y no pasan a producción. ## Cuida tu llave * Solo en tu servidor. Nunca en una aplicación web o móvil, en un repositorio ni en un mensaje. * Si crees que se expuso, pide revocarla de inmediato y usa una nueva. * No la pegues en sitios web. El panel de pruebas de este portal solo llama al sandbox: úsalo únicamente con tu llave de prueba. # Webhooks Source: https://ac-core.mintlify.app/guias/webhooks Recibe los cambios de estado de tus e-CF y verifica cada entrega. Registra una URL HTTPS de tu servidor y te avisamos cuando cambia el estado de un e-CF ante la DGII, sin que tengas que consultar. ## Registrar un webhook ```bash theme={null} curl -X POST https://e-cf.acpay.do/sandbox/v1/webhooks \ -H "Authorization: Bearer $ACPAY_TEST_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "url": "https://erp.example.com/webhooks/ecf", "events": ["ecf.accepted", "ecf.rejected", "ecf.cancelled"] }' ``` * `url`: HTTPS, hasta 2048 caracteres. Solo entregamos a direcciones públicas en el puerto 443 u 8443, y no seguimos redirecciones. * `events`: al menos uno de `ecf.accepted`, `ecf.rejected` y `ecf.cancelled`, sin repetir. > **El `secret` solo se devuelve en esta respuesta.** Guárdalo de inmediato en tu gestor de secretos: con él verificas cada entrega. Si lo pierdes, borra el webhook y crea otro. Para probar tu endpoint: `POST /v1/webhooks/{id}/test` envía un evento `test` y te dice si respondió `2xx`. Para pausarlo sin borrarlo: `PATCH /v1/webhooks/{id}` con `{ "active": false }`. `GET /v1/webhooks/{id}` muestra las entregas recientes con su estado (`delivered`, `pending` o `failed`) y sus intentos. **Mientras desarrollas en tu computadora**, tu servidor local no es alcanzable desde internet. Expón tu puerto con un túnel HTTPS (por ejemplo, el de tu proveedor de nube o una herramienta de túneles) y registra esa URL en el sandbox. No lo dejes abierto más de lo necesario. ## Qué recibes Un `POST` con cuerpo JSON: ```json theme={null} { "event": "ecf.accepted", "data": { "documentType": "invoice", "id": "3f1c2b9e-8a4d-4f6e-9c1a-2b7d5e8f0a13", "encf": "E310000000001", "ecfType": 31, "status": "accepted", "statusMessage": null, "invoiceId": null, "occurredAt": "2026-09-27T14:15:00.000Z" }, "timestamp": "2026-09-27T14:15:01.000Z", "deliveryId": "b3d4f5a6-1c2e-4f7a-8b9c-0d1e2f3a4b5c" } ``` | Campo | Contenido | | - | - | | `event` | `ecf.accepted`, `ecf.rejected`, `ecf.cancelled` o `test`. | | `data.documentType` | `invoice`, `credit_note`, `debit_note` o `special_document`. | | `data.status` / `data.statusMessage` | El nuevo estado y su detalle (por ejemplo, el motivo del rechazo). | | `data.invoiceId` | En las notas, la factura que modifican; `null` en el resto. | | `timestamp` | Cuándo se envió esta entrega. | | `deliveryId` | Id de la entrega; se repite en los reintentos. | | Encabezado | Contenido | | - | - | | `X-Acpay-Signature` | `sha256=`: HMAC-SHA256 del cuerpo **crudo** con el secreto del webhook. | | `X-Acpay-Event` | El mismo `event` del cuerpo. | | `X-Acpay-Delivery` | El mismo `deliveryId` del cuerpo. | Los eventos son livianos: para el detalle consulta el documento según `data.documentType`, con `GET /v1/invoices/{id}`, `GET /v1/notes/{id}` o `GET /v1/special-documents/{id}`. ## Verifica cada entrega Cualquiera puede enviar un `POST` a tu URL. Antes de procesar una entrega: 1. **Verifica la firma** sobre el cuerpo crudo, antes de parsear el JSON (re-serializarlo cambia los bytes y la firma), con una comparación en tiempo constante. Si no coincide, responde `401` y descártala. 2. **Rechaza entregas viejas:** si `timestamp` tiene más de 5 minutos. 3. **Deduplica por `deliveryId`:** un reintento trae el mismo `deliveryId`; si ya lo procesaste, responde `200` sin repetir el trabajo. ```js theme={null} const crypto = require('crypto'); const express = require('express'); const WEBHOOK_SECRET = process.env.ECF_WEBHOOK_SECRET; const MAX_AGE_MS = 5 * 60 * 1000; const app = express(); // express.raw: se necesita el cuerpo crudo para verificar la firma. app.post( '/webhooks/ecf', express.raw({ type: 'application/json' }), (req, res) => { const expected = Buffer.from( 'sha256=' + crypto .createHmac('sha256', WEBHOOK_SECRET) .update(req.body) .digest('hex'), ); const received = Buffer.from(req.get('X-Acpay-Signature') ?? ''); if ( received.length !== expected.length || !crypto.timingSafeEqual(received, expected) ) { return res.sendStatus(401); } const payload = JSON.parse(req.body.toString('utf8')); const age = Date.now() - Date.parse(payload.timestamp); if (!Number.isFinite(age) || Math.abs(age) > MAX_AGE_MS) { return res.sendStatus(401); } // Deduplica por payload.deliveryId y procesa en segundo plano. res.sendStatus(200); }, ); ``` ## Responde rápido Responde `2xx` en menos de 10 segundos; si tu proceso tarda más, encólalo y responde de inmediato. Cualquier otra respuesta, un error de conexión o pasar de 10 segundos cuentan como fallo. Después de cada fallo esperamos antes del siguiente intento: 1 minuto, luego 5 minutos, 30 minutos, 2 horas y 24 horas, hasta 6 intentos en total. Las esperas se suman: el último intento llega unas 26 horas y 36 minutos después del primero. Cada espera puede alargarse hasta un minuto. ## Garantías de entrega * **Cada intento se firma de nuevo**, con un `timestamp` nuevo, y conserva el mismo `deliveryId`. Por eso un reintento horas después pasa la regla de los 5 minutos. * **Un `deliveryId` por webhook y por evento.** Si dos webhooks tuyos reciben el mismo evento, cada uno tiene su propio `deliveryId`. * **Guarda los `deliveryId` procesados al menos 27 horas,** lo que dura el ciclo completo de reintentos. Para no procesar dos veces, marca el `deliveryId` y encola el trabajo en la misma transacción de tu base de datos. * **No garantizamos el orden.** Un reintento puede llegar después de un evento más reciente. Usa `data.occurredAt` o consulta el documento para saber su estado actual. * **Al menos una vez, no exactamente una vez:** una entrega que tu servidor procesó, pero cuya respuesta no nos llegó, se repite con el mismo `deliveryId`. * **El secreto** son 64 caracteres hexadecimales. Úsalo tal cual, como texto, como clave del HMAC (no lo decodifiques a bytes). Si reintentas el registro con la misma `Idempotency-Key` y el mismo cuerpo, la respuesta vuelve a traer el mismo secreto. * **Si se agotan los intentos,** la entrega queda como `failed` en `GET /v1/webhooks/{id}`. Para no perder estados, concilia periódicamente con `GET /v1/invoices`. # API de facturación electrónica Source: https://ac-core.mintlify.app/index Emite y consulta comprobantes fiscales electrónicos (e-CF) ante la DGII de República Dominicana con una sola API REST. Con el API de AC-PAY emites facturas, notas de crédito y débito y comprobantes especiales, y recibes su estado ante la DGII sin volverte proveedor certificado: lo eres a través de AC-PAY. > [card] [Inicio rápido](/guias/inicio-rapido) > > Tu primera factura en el sandbox, en cinco pasos. > [card] Referencia de la API > > En la pestaña **Referencia de la API**: cada endpoint con sus campos, ejemplos y un panel para probarlo con tu llave de prueba. > [card] [Webhooks](/guias/webhooks) > > Recibe los cambios de estado y verifica cada entrega. > [card] [Pasar a producción](/guias/pasar-a-produccion) > > La lista de verificación antes de usar tu llave live. ## Ambientes | Ambiente | URL base | Llave | Comprobantes | | - | - | - | - | | Sandbox (pruebas) | `https://e-cf.acpay.do/sandbox/v1` | `acpay_test_...` | Van al ambiente de pruebas de la DGII, sin validez fiscal. | | Producción | `https://e-cf.acpay.do/v1` | `acpay_live_...` | Van a la DGII, con validez fiscal. | Una llave de un ambiente se rechaza en el otro con `401`. ## Lo esencial * **Autenticación:** `Authorization: Bearer ` en cada solicitud. * **Idempotencia:** todo `POST` lleva `Idempotency-Key`. Repetir la misma solicitud con la misma llave no emite dos veces. Ver [Idempotencia](/guias/idempotencia). * **Estados:** el estado ante la DGII se actualiza solo; recíbelo por [webhook](/guias/webhooks) en vez de consultar en bucle. * **Errores:** siempre `{ "error": { "status", "message", "details" } }`. Ver [Errores y límites](/guias/errores-y-limites). * **Trazabilidad:** cada respuesta trae `X-Request-Id`. Inclúyelo al reportar un problema. # Novedades Source: https://ac-core.mintlify.app/novedades Cambios del API de facturación electrónica y de este portal. **Noviembre 2026** v1 ## Portal de desarrolladores * Guías de inicio rápido, estados, idempotencia, webhooks, errores y paso a producción. * Referencia completa del API con un panel para probar cada endpoint contra el sandbox. * Sandbox en `https://e-cf.acpay.do/sandbox/v1` con llaves `acpay_test_...`.