> ## Documentation Index
> Fetch the complete documentation index at: https://dev.acpay.do/llms.txt
> Use this file to discover all available pages before exploring further.

# Modificar un webhook

> 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.



## OpenAPI

````yaml /api-reference/ecf.json patch /v1/webhooks/{id}
openapi: 3.0.0
info:
  title: AC-PAY Facturación Electrónica API
  description: >

    API para emitir y consultar comprobantes fiscales electrónicos (e-CF) ante
    la DGII de República Dominicana.


    ## Autenticación

    Envía tu llave en cada solicitud: `Authorization: Bearer <token>`.

    Las llaves tienen el ambiente en el prefijo: `acpay_test_...` (pruebas, sin
    validez fiscal) o `acpay_live_...` (producción).

    Cada ambiente tiene su propia URL base; una llave de un ambiente es
    rechazada en el otro con `401`.


    ## Idempotencia

    Todas las operaciones `POST` (emitir, anular, reintentar, notas,
    comprobantes especiales, registrar y probar webhooks) exigen el encabezado
    `Idempotency-Key`. `PATCH` y `DELETE` no la usan: repetirlos deja el mismo
    resultado.

    Reintentar con la misma llave y la misma solicitud durante 24 h devuelve la
    respuesta original con `Idempotent-Replayed: true` sin volver a ejecutarla.

    Usar la misma llave con otra solicitud devuelve `422`; si la primera sigue
    en curso, `409`.

    No se guardan los `401`, `403`, `409`, `429`, los `5xx` ni los `400` de
    validación del cuerpo: después de cualquiera de ellos puedes reintentar con
    la misma llave.


    ## Errores

    Todas las respuestas de error tienen la forma `{ "error": { "status": 400,
    "message": "...", "details": ["..."] } }`.

    `details` aparece solo cuando hay errores por campo.


    ## Límites de uso

    Hay dos límites y ambos responden `429`:

    - **Solicitudes por minuto** de tu llave: el `429` trae `Retry-After`
    (segundos a esperar). Cuando están disponibles se incluyen los encabezados
    `X-RateLimit-*`.

    - **Comprobantes por mes** de cada tipo de e-CF (al emitir): el `429` no
    trae `Retry-After` y su mensaje dice cuándo se renueva. Consulta tu consumo
    con `GET /v1/limits` (sección **Límites**).


    ## Estados y webhooks

    El estado de cada e-CF ante la DGII se actualiza solo, en segundo plano: no
    necesitas consultar en bucle. Registra un webhook (sección **Webhooks**)
    para recibir los cambios de estado; las consultas `GET` siguen actualizando
    el estado al momento si lo necesitas.


    ## Trazabilidad

    Cada respuesta incluye `X-Request-Id` (puedes enviar el tuyo). Inclúyelo al
    reportar un problema.
  version: '1.0'
  contact: {}
servers:
  - url: https://e-cf.acpay.do/sandbox
    description: Sandbox (pruebas)
security: []
tags:
  - name: Facturas electrónicas (e-CF)
    description: ''
  - name: Notas de crédito y débito
    description: ''
  - name: Comprobantes especiales
    description: ''
  - name: Secuencias
    description: ''
  - name: Límites
    description: >

      Tu plan puede fijar un **límite mensual de comprobantes por tipo de e-CF**
      (p. ej. 500 e-CF 31 al mes). Consulta con `GET /v1/limits` cuánto llevas
      emitido y cuánto te queda.


      - 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 dueño del comercio y a los usuarios de su
      backoffice) al llegar al **80 %** y al **100 %** del límite de cada tipo,
      una vez por mes.

      - `remaining` nunca es negativo y `percentUsed` es un entero redondeado.

      - En el ambiente de pruebas los límites no se aplican (`enforced: false`).


      **Al alcanzar el límite**, emitir un e-CF de ese tipo (`POST
      /v1/invoices`, `POST /v1/invoices/{id}/credit-notes`, `POST
      /v1/invoices/{id}/debit-notes` y `POST /v1/special-documents`) responde
      `429` **sin** `Retry-After`, con un mensaje como:


      ```json

      { "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." } }

      ```


      No lo confundas con el `429` por exceso de solicitudes por minuto, que sí
      trae `Retry-After` (segundos a esperar). El `429` por límite mensual no se
      resuelve reintentando: espera la renovación o pide ampliarlo.


      - Consultar, descargar y anular documentos sigue funcionando con el límite
      alcanzado.

      - Reintentar un documento ya emitido (`/retry` o la misma
      `Idempotency-Key`) no vuelve a contar.

      - Ningún `429` se guarda como respuesta idempotente: cuando se amplíe el
      límite puedes reintentar con la misma `Idempotency-Key`.
  - name: Webhooks
    description: >

      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.


      ## Cómo recibir y verificar los eventos


      Enviamos un `POST` con cuerpo JSON a tu URL:


      ```json

      {
        "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"
      }

      ```


      - `event`: `ecf.accepted`, `ecf.rejected`, `ecf.cancelled` o `test`
      (entregas de `POST /v1/webhooks/{id}/test`, cuyo `data` es `{ "message":
      "..." }`).

      - `data.documentType`: `invoice`, `credit_note`, `debit_note` o
      `special_document`.

      - `data.status` / `data.statusMessage`: el nuevo estado y su detalle (p.
      ej. el motivo del rechazo).

      - `data.invoiceId`: en las notas, el e-CF de venta que modifican; `null`
      en el resto.

      - `data.occurredAt`: cuándo cambió el estado. `timestamp`: cuándo se envió
      esta entrega.


      Encabezados de cada entrega:


      | Encabezado | Contenido |

      | --- | --- |

      | `X-Acpay-Signature` | `sha256=<hex>`: 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. |


      **Verifica cada entrega antes de procesarla:**


      1. Calcula el HMAC-SHA256 del cuerpo tal como llegó (bytes crudos, antes
      de parsear el JSON) con tu secreto y compáralo con `X-Acpay-Signature` en
      tiempo constante. Si no coincide, responde `401` y descártala.

      2. Rechaza la entrega si `timestamp` tiene más de 5 minutos (protección
      contra repeticiones).

      3. Deduplica por `deliveryId`: un reintento trae el mismo `deliveryId`; si
      ya lo procesaste, responde `200` sin repetir el trabajo.


      El secreto solo se devuelve al crear el webhook (`POST /v1/webhooks`). Si
      lo pierdes, borra el webhook y crea otro.


      **Responde `2xx` en menos de 10 segundos** (procesa en segundo plano si
      tardas más). Si no respondes `2xx` a tiempo, reintentamos 1 min, 5 min, 30
      min, 2 h y 24 h después de cada fallo: hasta 6 intentos en total (el
      inicial y 5 reintentos). Puedes ver cada entrega y sus intentos en
      `recentDeliveries` de `GET /v1/webhooks/{id}`.


      **Los eventos son livianos:** para el detalle completo consulta el
      documento según `data.documentType`: `GET /v1/invoices/{id}` (`invoice`),
      `GET /v1/notes/{id}` (`credit_note` y `debit_note`) o `GET
      /v1/special-documents/{id}` (`special_document`).


      Ejemplo de verificación en Node.js (Express):


      ```js

      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);
      });

      ```
  - name: Estado
    description: ''
paths:
  /v1/webhooks/{id}:
    patch:
      tags:
        - Webhooks
      summary: Modificar un webhook
      description: >-
        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.
      operationId: webhooksUpdate
      parameters:
        - name: id
          required: true
          in: path
          description: Identificador del webhook (UUID).
          schema:
            type: string
            format: uuid
            example: 7c9e6679-7425-40de-944b-e07fc1f90ae7
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookDto'
            examples:
              pausar:
                summary: Pausar el webhook sin borrarlo
                value:
                  active: false
              cambiarUrlYEventos:
                summary: Cambiar la URL y los eventos
                value:
                  url: https://erp.distribuidoracaribe.com.do/webhooks/ecf-v2
                  events:
                    - ecf.accepted
                    - ecf.rejected
      responses:
        '200':
          description: Webhook modificado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
        '400':
          description: Solicitud inválida (cuerpo, parámetros o Idempotency-Key).
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                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.'
        '401':
          description: >-
            Token ausente, con formato inválido, de otro ambiente, vencido o
            revocado.
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                error:
                  status: 401
                  message: Esta llave pertenece a otro ambiente.
        '403':
          description: >-
            La llave no tiene permiso para esta operación o el comercio está
            inactivo.
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                error:
                  status: 403
                  message: La llave no tiene permiso para esta operación.
        '404':
          description: El documento o webhook no existe o no pertenece a tu comercio.
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                error:
                  status: 404
                  message: Documento no encontrado.
        '429':
          description: >-
            Se superó el límite de solicitudes de la llave. Respeta
            `Retry-After`.
          headers:
            Retry-After:
              description: Segundos a esperar antes de reintentar.
              schema:
                type: integer
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                error:
                  status: 429
                  message: Demasiadas solicitudes. Intenta de nuevo más tarde.
        '502':
          description: >-
            Error inesperado del servicio de facturación. Se puede reintentar
            con la misma Idempotency-Key.
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                error:
                  status: 502
                  message: >-
                    El servicio de facturación electrónica tuvo un error
                    inesperado. Intenta de nuevo más tarde.
        '503':
          description: >-
            Servicio no disponible temporalmente. Se puede reintentar con la
            misma Idempotency-Key.
          headers:
            X-Request-Id:
              description: >-
                Identificador de la solicitud (el tuyo si enviaste uno válido).
                Inclúyelo al reportar un problema.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelopeResponse'
              example:
                error:
                  status: 503
                  message: >-
                    El servicio de facturación electrónica no está disponible.
                    Intenta de nuevo en unos minutos.
      security:
        - bearer: []
components:
  schemas:
    UpdateWebhookDto:
      type: object
      properties:
        url:
          type: string
          description: Nueva URL HTTPS (hasta 2048 caracteres).
          example: https://erp.distribuidoracaribe.com.do/webhooks/ecf-v2
          format: uri
          maxLength: 2048
        events:
          type: array
          description: >-
            Nueva lista de eventos (al menos uno, sin repetir). Reemplaza la
            anterior.
          minItems: 1
          uniqueItems: true
          example:
            - ecf.accepted
          items:
            type: string
            enum:
              - ecf.accepted
              - ecf.rejected
              - ecf.cancelled
        active:
          type: boolean
          description: >-
            `false` pausa el webhook (no recibe eventos) sin borrarlo; `true` lo
            reactiva.
          example: false
    WebhookResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: 7c9e6679-7425-40de-944b-e07fc1f90ae7
        url:
          type: string
          format: uri
          example: https://erp.distribuidoracaribe.com.do/webhooks/ecf
        events:
          type: array
          description: Eventos suscritos.
          example:
            - ecf.accepted
            - ecf.rejected
          items:
            type: string
            enum:
              - ecf.accepted
              - ecf.rejected
              - ecf.cancelled
        active:
          type: boolean
          description: Si recibe eventos. Uno inactivo no recibe nada.
          example: true
        createdAt:
          type: string
          format: date-time
          example: '2026-09-27T14:00:00.000Z'
        updatedAt:
          type: string
          format: date-time
          example: '2026-09-27T14:00:00.000Z'
      required:
        - id
        - url
        - events
        - active
        - createdAt
        - updatedAt
    ErrorEnvelopeResponse:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ErrorEnvelopeBodyResponse'
      required:
        - error
    ErrorEnvelopeBodyResponse:
      type: object
      properties:
        status:
          type: number
          description: Código HTTP (igual al de la respuesta).
          example: 400
        message:
          type: string
          example: La solicitud no es válida.
        details:
          description: 'Detalle por campo, cuando aplica (`ruta.del.campo: mensaje`).'
          example:
            - 'items.0.quantity: debe ser un número entero.'
          type: array
          items:
            type: string
      required:
        - status
        - message
  securitySchemes:
    bearer:
      scheme: bearer
      bearerFormat: acpay_<test|live>_<secreto>
      type: http
      description: Llave de API de AC-PAY del ambiente de esta URL.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.