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

# Webhook del comprobante

> Contrato de salida: el comprobante contable que Payana envía al terminar de registrar un documento, y cómo mapearlo a tu ERP.

Cuando un documento termina de causarse en Payana, se dispara el evento `document.accounted` contra el endpoint que registres. El payload trae el comprobante contable completo y listo para registrar: cada línea con su cuenta, los impuestos discriminados, las retenciones calculadas, la distribución por centro de costo y el cruce contra la orden de compra, la entrada de almacén o el anticipo cuando corresponde.

Este es el único momento en que Payana escribe hacia tu lado. Tu sistema es el que decide cómo se traduce a un asiento del ERP.

## Configuración

El endpoint receptor y su secret se configuran del lado de Payana; coordinalo con tu equipo de implementación. Necesitás definir:

| Parámetro | Descripción |
| - | - |
| URL | Endpoint HTTPS que recibe el `POST` |
| Evento | `document.accounted` |
| Secret | Clave con la que Payana firma el payload |
| Reintentos | Cantidad de reintentos ante fallo. Por defecto `3` |
| Timeout | Segundos de espera de tu respuesta. `120` para este evento |
| Headers | Headers adicionales, si tu endpoint requiere autenticación propia |

<Warning>
  Este webhook usa un timeout de **120 segundos**, más alto que el de los demás eventos, porque tu endpoint tiene que escribir en el ERP antes de responder. Son los dos minutos que medimos como suficientes para registrar un comprobante en la mayoría de los ERPs. Si el tuyo tarda más, avisale a tu equipo de implementación para ajustarlo antes de salir a producción: si Payana corta por timeout una escritura que en realidad estaba en curso, el reintento puede duplicarte el comprobante.
</Warning>

## Entrega y respuesta

La entrega es **síncrona**: Payana espera a que registres el comprobante y le respondas con el resultado. No es un webhook de notificación que se confirma y se procesa después.

```http theme={null}
POST https://tu-erp.com/hooks/payana
Content-Type: application/json
X-Payana-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Payana-Event: document.accounted
X-Payana-Timestamp: 2026-09-08T14:32:11Z
```

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant P as Payana
    participant E as Tu ERP

    P->>E: POST document.accounted
    E->>E: Registra el comprobante
    E-->>P: Responde consecutivo o error
    P->>P: Si hubo error lo muestra al cliente y reintenta
```

### Respuesta correcta

Respondé **`200`** con el identificador y el consecutivo bajo el que quedó registrado el comprobante. Payana lo guarda y se lo muestra al usuario, que así puede encontrar el documento en tu ERP.

```json theme={null}
{
  "erp_invoice_id": "INV-88213",
  "invoice_reference": "FC-2026-0912"
}
```

| Campo | Tipo | Requerido | Descripción |
| - | - | - | - |
| `erp_invoice_id` | string | Sí | El ID del documento creado en tu ERP |
| `invoice_reference` | string | No | El consecutivo bajo el que quedó causado. Mandá `null` si tu ERP todavía no se lo asignó: el comprobante existe igual y Payana no lo toma como error |

### Respuesta con error

Si no pudiste registrarlo, respondé **`422`** con un código y un mensaje. Ese mensaje se le muestra al cliente en Payana, así que escribilo para que una persona de contabilidad entienda qué pasó y qué hacer.

```json theme={null}
{
  "error": {
    "code": "PERIODO_CERRADO",
    "message": "El período contable de septiembre 2026 está cerrado en el ERP."
  }
}
```

Payana marca la causación como fallida, le muestra el error al cliente y habilita el reintento. Un `5xx`, un cuerpo ilegible o exceder el timeout se tratan como fallo genérico y disparan los reintentos automáticos.

### Idempotencia

Procesá el webhook de forma **idempotente**: usá `data.reference` como llave de deduplicación. Es imprescindible acá, porque un timeout hace que Payana reintente un comprobante que tal vez ya registraste. Si te llega un `reference` que ya procesaste, no lo registres de nuevo: respondé `200` con el `erp_invoice_id` y el consecutivo que le habías asignado.

### Verificación de la firma

`X-Payana-Signature` es el HMAC SHA256 del body crudo, con tu secret como clave.

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secretKey) {
  const expected = crypto
    .createHmac('sha256', secretKey)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

Verificá siempre la firma antes de procesar, y usá una comparación de tiempo constante (`timingSafeEqual`) en lugar de `===`.

## Directa o con cruces

Una factura puede causarse de dos formas, y no se registran igual. Casi ningún ERP las recibe por el mismo endpoint —en Dynamics FO, por ejemplo, una va a `VendInvoiceJournal` y la otra a `VendorInvoice`—, así que tu integración tiene que distinguirlas antes de escribir.

**El discriminador es el campo `source` de las líneas del asiento.**

| Forma | `source` de las líneas | Qué significa |
| - | - | - |
| **Directa** | `document` · `agent` · `front` | La factura se imputa contra cuentas contables. No hay documento previo que cruzar: es un asiento armado desde cero |
| **Con cruces** | `purchase_order` · `goods_receipt` | Al menos una línea viene de una orden de compra o de una entrada de almacén. Cada línea apunta a la línea que cruza y hereda sus imputaciones |

Para decidir, recorré las líneas: si **alguna** tiene `source` igual a `purchase_order` o `goods_receipt`, es una causación con cruces.

```javascript theme={null}
const CROSSED = ['purchase_order', 'goods_receipt'];

const lines = payload.data.accounting_entry.journal_entry.erp_journal_entry_lines;
const isCrossed = lines.some((line) => CROSSED.includes(line.source));
```

<Info>
  Una línea con `source: "goods_receipt"` también enruta al camino de cruces: la factura igual cruza la orden de compra —la que originó esa entrada—, y la entrada solo precisa por qué cantidades.
</Info>

En una causación con cruces, cada línea trae `purchase_order_line_external_id` o `goods_receipt_line_external_id`, y sus dimensiones —centro de costo, bodega— vienen heredadas del documento cruzado, no elegidas de nuevo. Registrala contra ese documento para que tu ERP descuente lo pendiente; si la registrás suelta, la orden va a quedar abierta por algo que ya se facturó.

## Estructura del payload

```json theme={null}
{
  "event": "document.accounted",
  "timestamp": "2026-09-08T14:32:11Z",
  "data": {
    "reference": "aaea7db4-647c-4d92-9f86-89e34a68e1aa",
    "document_number": "FC-2026-0912",
    "type": "invoice",
    "issue_date": "2026-09-01",
    "expiration_date": "2026-10-01",
    "amount": 142800000,
    "amount_currency": "COP",
    "status": "approved",
    "payment_status": "pending",
    "fiscal_reference": "e19d3e7867371a12fd53b67a56db087f",
    "fiscal_metadata": { "final_notes": "Sujeta a retención en la fuente." },
    "beneficiary": { },
    "accounting_entry": { }
  }
}
```

El documento y su causación viajan juntos: `data` describe la factura y `data.accounting_entry` el comprobante contable armado.

Las convenciones son las mismas que en la entrada: los importes van en **centavos, como entero**, las fechas en `YYYY-MM-DD`, las marcas de tiempo en ISO 8601, y las referencias a entidades del catálogo usan el `external_id` de tu ERP.

### Campos del documento

| Campo | Tipo | Descripción |
| - | - | - |
| `reference` | string | Identificador del documento en Payana. Usalo como llave de idempotencia |
| `document_number` | string | Número de la factura |
| `type` | string | Tipo de documento: `invoice`, `credit_note`, `debit_note` |
| `issue_date` | date | Fecha de emisión |
| `expiration_date` | datetime | Fecha de vencimiento |
| `amount` | integer | Total del documento, en centavos |
| `amount_currency` | string | Moneda, ISO 4217 |
| `status` | string | Estado de aprobación en Payana |
| `payment_status` | string | Estado de pago |
| `fiscal_reference` | string | CUFE (Colombia) o UUID fiscal (México) |
| `fiscal_metadata` | object | Datos tomados del XML fiscal, como las notas finales |
| `file_path` | string | URL del PDF del documento |
| `purchase_order` | string | Orden de compra referenciada en el documento |
| `beneficiary` | object | El proveedor, con `identifier_number` y `identifier_type` |
| `accounting_entry` | object | El comprobante contable. `null` si el documento no está causado |

<Info>
  El `beneficiary` viene de la entidad fiscal, así que `identifier_number` es el NIT o RFC. Es la forma de cruzar contra el proveedor de tu ERP — la misma llave con la que lo sincronizaste.
</Info>

### El comprobante: `accounting_entry`

```json theme={null}
{
  "reference": "CAUS-2026-0042",
  "accounted_at": "2026-09-08T14:32:00Z",
  "summary": { },
  "journal_entry": { },
  "advance_crossings": []
}
```

| Campo | Tipo | Descripción |
| - | - | - |
| `reference` | string | Identificador del comprobante en Payana |
| `accounted_at` | datetime | Momento en que se causó |
| `summary` | object | Totales del comprobante |
| `journal_entry` | object | El asiento: encabezado y líneas |
| `advance_crossings` | array | Anticipos cruzados contra esta factura |
| `registration_payload` | object | El input original con el que se causó. Trazabilidad; no hace falta leerlo para registrar |

### Encabezado del asiento: `journal_entry`

| Campo | Tipo | Descripción |
| - | - | - |
| `status` | string | `posted` o `cancelled`. Solo se notifican comprobantes `posted` |
| `document_number` | string | Número del documento |
| `document_type_external_id` | string | Tipo de documento (diario) con el `external_id` de tu ERP |
| `vendor_external_id` | string | Proveedor, con el `external_id` de tu ERP |
| `issue_date` | date | Fecha de emisión |
| `posting_date` | date | Fecha contable |
| `expiration_date` | date | Fecha de vencimiento |
| `cost_center_external_id` | string | Centro de costo del encabezado |
| `description` | string | Descripción o concepto |
| `erp_journal_entry_lines` | array | Líneas del asiento |
| `payment_lines` | array | Líneas de la contrapartida por pagar |
| `erp_journal_entry_taxes` | array | Impuestos consolidados a nivel comprobante |

<Info>
  Todas las referencias a entidades del catálogo vuelven con **el `external_id` que vos enviaste**, no con identificadores internos de Payana. La correspondencia la resuelve Payana en los dos sentidos, así que no necesitás mantener una tabla de mapeo propia.
</Info>

### Líneas del asiento: `erp_journal_entry_lines`

Cada línea es un movimiento contable. `debit_cents` y `credit_cents` son excluyentes: una línea mueve por debe o por haber.

| Campo | Tipo | Descripción |
| - | - | - |
| `source` | string | De dónde salió la línea, ver tabla abajo |
| `description` | string | Descripción de la línea |
| `book_account_external_id` | string | Cuenta contable a imputar |
| `debit_cents` | integer | Importe al debe, en centavos |
| `credit_cents` | integer | Importe al haber, en centavos |
| `quantity` | number | Cantidad |
| `unit_price_cents` | integer | Precio unitario, en centavos |
| `discount` | number | Descuento en importe |
| `discount_rate` | number | Descuento en porcentaje |
| `product_external_id` | string | Producto, si la línea lo tiene |
| `vendor_external_id` | string | Tercero de la línea, si difiere del encabezado |
| `cost_centers` | array | `{ cost_center_external_id, percentage }` |
| `warehouses` | array | `{ warehouse_external_id }` |
| `taxes` | array | `{ tax_external_id, base_cents, amount_cents, type }` |
| `purchase_order_line_external_id` | string | Línea de OC que originó esta línea |
| `goods_receipt_line_external_id` | string | Línea de entrada de almacén que la originó |
| `metadata` | object | Datos adicionales |

**Valores de `source`**

| Valor | Significado |
| - | - |
| `document` | Salió del XML de la factura |
| `agent` | La propuso el agente de causación |
| `front` | La cargó o corrigió una persona en Payana |
| `purchase_order` | Viene del cruce contra una orden de compra |
| `goods_receipt` | Viene del cruce contra una entrada de almacén |
| `payable_line` | Contrapartida por pagar. Llega en `payment_lines`, no acá |

<Info>
  `source` es informativo para tu registro, pero es lo que te permite auditar el origen de cada imputación: una línea con `source: "purchase_order"` y `purchase_order_line_external_id` te dice exactamente qué línea de qué OC se está facturando, y con eso podés descontar el pendiente en tu ERP.
</Info>

### Contrapartida: `payment_lines`

La cuenta por pagar generada. Cuando hay pago en cuotas, viene una línea por cuota.

| Campo | Tipo | Descripción |
| - | - | - |
| `book_account_external_id` | string | Cuenta por pagar |
| `book_account_code` | string | Código de la cuenta |
| `book_account_name` | string | Nombre de la cuenta |
| `amount_cents` | integer | Importe de la cuota, en centavos |
| `due_date` | date | Vencimiento de la cuota |
| `quota_number` | integer | Número de cuota, si el pago es fraccionado |
| `payment_method_external_id` | string | Método de pago |
| `cost_center_external_id` | string | Centro de costo de la contrapartida |

### Cruce de anticipos: `advance_crossings`

Aparece cuando la factura se descontó contra uno o más anticipos.

| Campo | Tipo | Descripción |
| - | - | - |
| `external_move_line_id` | string | Línea del movimiento del anticipo en tu ERP |
| `external_move_id` | string | Movimiento contable del anticipo |
| `external_payment_id` | string | Pago que originó el anticipo |
| `amount_cents` | integer | Importe cruzado contra esta factura, en centavos |
| `original_amount_cents` | integer | Importe original del anticipo |
| `outstanding_amount_cents` | integer | Saldo del anticipo después del cruce |
| `currency` | string | Moneda, ISO 4217 |
| `advance_date` | date | Fecha del anticipo |
| `status` | string | Estado del cruce |
| `reference` | string | Referencia visible |

Después de procesar un cruce, actualizá el saldo del anticipo en tu ERP y volvé a sincronizarlo con `outstanding_amount_cents`, para que Payana no lo vuelva a ofrecer por un saldo que ya se consumió.

## Ejemplo completo

Una causación **con cruces**: factura de un servicio contra orden de compra, con IVA del 19% y retención en la fuente del 11%. Se reconoce por el `source: "purchase_order"` de la línea y su `purchase_order_line_external_id`.

```json theme={null}
{
  "event": "document.accounted",
  "timestamp": "2026-09-08T14:32:11Z",
  "data": {
    "reference": "aaea7db4-647c-4d92-9f86-89e34a68e1aa",
    "document_number": "FC-2026-0912",
    "type": "invoice",
    "issue_date": "2026-09-01",
    "expiration_date": "2026-10-01",
    "amount": 142800000,
    "amount_currency": "COP",
    "status": "approved",
    "payment_status": "pending",
    "fiscal_reference": "e19d3e7867371a12fd53b67a56db087f",
    "beneficiary": {
      "name": "Suministros Andinos S.A.S.",
      "identifier_type": "NIT",
      "identifier_number": "900123456"
    },
    "accounting_entry": {
      "reference": "CAUS-2026-0042",
      "accounted_at": "2026-09-08T14:32:00Z",
      "summary": {
        "gross_total_cents": 120000000,
        "discount_cents": 0,
        "subtotal_cents": 120000000,
        "iva_total_cents": 22800000,
        "retention_total_cents": 13200000,
        "net_total_cents": 142800000
      },
      "journal_entry": {
        "status": "posted",
        "document_number": "FC-2026-0912",
        "document_type_external_id": "FC",
        "vendor_external_id": "PROV-00412",
        "issue_date": "2026-09-01",
        "posting_date": "2026-09-08",
        "expiration_date": "2026-10-01",
        "description": "Mantenimiento preventivo agosto",
        "erp_journal_entry_lines": [
          {
            "source": "purchase_order",
            "description": "Mantenimiento preventivo agosto",
            "book_account_external_id": "5135",
            "debit_cents": 120000000,
            "credit_cents": 0,
            "quantity": 10,
            "unit_price_cents": 12000000,
            "product_external_id": "PROD-114",
            "purchase_order_line_external_id": "OC-2026-0187-1",
            "cost_centers": [
              { "cost_center_external_id": "CC-001-02", "percentage": 100 }
            ],
            "taxes": [
              {
                "tax_external_id": "IVA-19",
                "base_cents": 120000000,
                "amount_cents": 22800000,
                "type": "iva"
              },
              {
                "tax_external_id": "RF-SERV-11",
                "base_cents": 120000000,
                "amount_cents": 13200000,
                "type": "retefuente"
              }
            ]
          }
        ],
        "payment_lines": [
          {
            "book_account_external_id": "2205",
            "book_account_code": "220501",
            "book_account_name": "Proveedores nacionales",
            "amount_cents": 129600000,
            "due_date": "2026-10-01",
            "quota_number": 1
          }
        ]
      },
      "advance_crossings": []
    }
  }
}
```

En este ejemplo, el asiento a registrar es:

| Cuenta | Debe | Haber |
| - | - | - |
| `5135` Servicios de mantenimiento | 1.200.000 | |
| `2408` IVA descontable | 228.000 | |
| `2365` Retención en la fuente | | 132.000 |
| `2205` Proveedores nacionales | | 1.296.000 |

La retención se resta de la cuenta por pagar: el proveedor cobra \$1.296.000, no el total de la factura. Las líneas de impuesto vienen discriminadas dentro de `taxes` y su cuenta contable es la que sincronizaste en `default_book_account_external_id` de cada impuesto.

## Próximos pasos

* [Catálogo de entidades](/api-reference/guias-de-integracion/catalogo-de-entidades) — el contrato de entrada.
* [Cómo funciona la integración](/api-reference/guias-de-integracion/registro-de-facturas-erp) — arquitectura y modelo de identidad.
* [Referencia del evento `document.accounted`](/api-reference/documents/webhooks/documento-causado) — el esquema en OpenAPI.


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