---
title: "Recibir notificaciones | Timbro"
description: "Configura un webhook para recibir avisos sobre tus pagos y facturas"
canonical: "https://timbro.do/docs/cobros/webhooks/"
---

Webhooks y errores

# Recibir notificaciones

Timbro puede avisar a tu servidor cuando cambia un pago o una factura. Ese aviso se llama webhook. Primero verifica su firma y guarda el aviso; después consulta el recurso para conocer su estado actual. Esta guía empieza con los avisos de pagos y, más abajo, muestra los avisos de facturas con Invoices, sin instalar Payments.

## 1. Registra la URL de tu servidor

- [TypeScript](https://timbro.do/docs/cobros/webhooks/#tab-panel-0-0)
- [Python](https://timbro.do/docs/cobros/webhooks/#tab-panel-0-1)

```ts
import {
  createTimbroPayments,
  type Payment,
  type VerifiedWebhookEvent,
  webhooks,
} from "@timbro/payments";

const timbro = createTimbroPayments({ apiKey: process.env.TIMBRO_PAYMENTS_SECRET_KEY! });
const endpoint = await timbro.webhookEndpoints.create({
  url: "https://merchant.example/webhooks/timbro",
  enabledEvents: ["payment.succeeded"],
  idempotencyKey: "orders-webhook-v1",
});
```

```python
import os
from collections.abc import Mapping

from timbro_payments import TimbroPayments, WebhookVerificationError, webhooks
from timbro_payments.types import Payment

timbro = TimbroPayments(api_key=os.environ["TIMBRO_PAYMENTS_SECRET_KEY"])
endpoint = timbro.webhook_endpoints.create(
    {"url": "https://merchant.example/webhooks/timbro", "enabledEvents": ["payment.succeeded"]},
    idempotency_key="orders-webhook-v1",
)
```

Usa una URL HTTPS y guarda el `secret` del endpoint en la variable `TIMBRO_WEBHOOK_SECRET` de tu servidor. Timbro muestra este secreto de firma del webhook una sola vez, al crearlo; lo usarás para comprobar que los avisos vienen de Timbro.

Si necesitas repetir el registro, usa la misma clave de idempotencia (`Idempotency-Key`): `idempotencyKey` en TypeScript o `idempotency_key` en Python. Debes registrar la URL y guardar su secreto por separado para Prueba y En vivo.

## 2. Verifica y guarda el aviso

La ruta `POST` verifica la firma y guarda el aviso antes de responder. `saveAndSchedule` (`save_and_schedule` en Python) es una función que debes implementar en **tu aplicación**: guarda el evento completo y la tarea que lo procesará en una misma transacción de base de datos. Así, si tu servidor se reinicia, puede continuar el trabajo.

Usa `event.id` para reconocer un aviso que ya recibiste. Si llega de nuevo, conserva la tarea pendiente sin crear otra.

- [TypeScript](https://timbro.do/docs/cobros/webhooks/#tab-panel-1-0)
- [Python](https://timbro.do/docs/cobros/webhooks/#tab-panel-1-1)

```ts
declare function saveAndSchedule(event: VerifiedWebhookEvent): Promise<void>;

export async function POST(request: Request) {
  const webhookSecret = process.env.TIMBRO_WEBHOOK_SECRET;
  if (!webhookSecret) return new Response(null, { status: 503 });
  let event: VerifiedWebhookEvent;
  try {
    event = webhooks.verify({
      body: new Uint8Array(await request.arrayBuffer()),
      headers: Object.fromEntries(request.headers),
      secret: webhookSecret,
    });
  } catch {
    return new Response(null, { status: 401 });
  }
  await saveAndSchedule(event);
  return new Response(null, { status: 204 });
}
```

Llama `receive_webhook` desde la ruta `POST` de tu aplicación con el cuerpo original, en `bytes`, y los encabezados de la solicitud. Responde con el código HTTP que devuelve.

```python
def save_and_schedule(event: webhooks.WebhookEvent) -> None:
    """Tu aplicación: guarda el evento y su tarea en una misma transacción."""
    raise NotImplementedError


def receive_webhook(body: bytes, headers: Mapping[str, str]) -> int:
    webhook_secret = os.environ.get("TIMBRO_WEBHOOK_SECRET")
    if not webhook_secret:
        return 503
    try:
        event = webhooks.verify(body=body, headers=headers, secret=webhook_secret)
    except WebhookVerificationError:
        return 401
    save_and_schedule(event)
    return 204
```

`webhooks.verify` solo necesita el secreto de firma del webhook, no una clave de API. Pasa a `verify` el cuerpo original de la solicitud, sin convertirlo primero a JSON. Responde con `2xx` solo cuando hayas guardado el evento y su tarea. Si no puedes guardarlos, responde con un error para que Timbro vuelva a enviarlo. Mantén el reloj de tu servidor sincronizado: la verificación de la firma admite 5 minutos de diferencia.

Un evento firmado cuyo tipo esta versión del SDK aún no conoce se entrega de todos modos como evento no reconocido: guárdalo y omítelo al procesar, con `webhooks.isKnownWebhookEvent` en Payments e `isRecognizedWebhookEvent` en Invoices, o `webhooks.is_known_event` en ambos paquetes de Python, en lugar de rechazarlo.

## 3. Consulta el pago y actualiza el pedido

La tarea que guardaste consulta el pago para conocer su estado actual. `applyPaymentOnce` (`apply_payment_once` en Python) también es una función de **tu aplicación**: comprueba que la referencia, la moneda y el monto coincidan con el pedido, y márcalo como pagado una sola vez.

Si entregas el pedido o envías un correo, guarda también qué acciones se completaron. Debes poder continuar tras una falla sin repetirlas.

- [TypeScript](https://timbro.do/docs/cobros/webhooks/#tab-panel-2-0)
- [Python](https://timbro.do/docs/cobros/webhooks/#tab-panel-2-1)

```ts
declare function applyPaymentOnce(payment: Payment): Promise<void>;

export async function processEvent(event: VerifiedWebhookEvent) {
  if (!webhooks.isKnownWebhookEvent(event) || event.type !== "payment.succeeded") return;
  const payment = await timbro.payments.retrieve(event.data.id);
  if (payment.status === "succeeded") await applyPaymentOnce(payment);
}
```

```python
def apply_payment_once(payment: Payment) -> None:
    """Tu aplicación: comprueba el pedido y márcalo como pagado una sola vez."""
    raise NotImplementedError


def process_event(event: webhooks.WebhookEvent) -> None:
    if webhooks.is_known_event(event) and event["type"] == "payment.succeeded":
        payment = timbro.payments.retrieve(event["data"]["id"])
        if payment["status"] == "succeeded":
            apply_payment_once(payment)
```

Los avisos pueden repetirse o llegar en un orden distinto al de los cambios. `event.id` identifica el aviso; `event.data.id` es el identificador del pago o la factura que debes consultar. Si todavía no encuentras el pedido en tu sistema, deja la tarea pendiente y vuelve a procesarla después.

## Recibe avisos de facturas

Si solo facturas, usa `@timbro/invoices` o, en Python, `timbro-invoices`. Registra una URL distinta para estos avisos usando una clave de API con acceso a Facturación:

- [TypeScript](https://timbro.do/docs/cobros/webhooks/#tab-panel-3-0)
- [Python](https://timbro.do/docs/cobros/webhooks/#tab-panel-3-1)

```ts
import {
  createTimbroInvoices,
  isRecognizedWebhookEvent,
  type ReceivedWebhookEvent,
} from "@timbro/invoices";

const invoices = createTimbroInvoices({ apiKey: process.env.TIMBRO_INVOICES_API_KEY! });
const invoiceEndpoint = await invoices.webhookEndpoints.create({
  url: "https://merchant.example/webhooks/timbro-invoices",
  enabledEvents: ["invoice.accepted", "invoice.ready", "invoice.rejected", "invoice.operatorRequired"],
  idempotencyKey: "invoices-webhook-v1",
});
```

Registra la URL una vez con `webhook_endpoints.create` del cliente de `timbro-invoices`; no necesitas `timbro-payments`:

```python
import os
from collections.abc import Mapping

from timbro_invoices import TimbroInvoices, WebhookVerificationError
from timbro_invoices import webhooks as invoice_webhooks

invoices = TimbroInvoices(api_key=os.environ["TIMBRO_INVOICES_API_KEY"])
invoice_endpoint = invoices.webhook_endpoints.create(
    {
        "url": "https://merchant.example/webhooks/timbro-invoices",
        "enabledEvents": [
            "invoice.accepted",
            "invoice.ready",
            "invoice.rejected",
            "invoice.operatorRequired",
        ],
    },
    idempotency_key="invoices-webhook-v1",
)
```

Guarda el `secret` del endpoint en `TIMBRO_INVOICES_WEBHOOK_SECRET`. Usa una ruta separada de la de pagos, pues los eventos tienen formatos distintos.

- [TypeScript](https://timbro.do/docs/cobros/webhooks/#tab-panel-4-0)
- [Python](https://timbro.do/docs/cobros/webhooks/#tab-panel-4-1)

El verificador de Invoices es asíncrono: usa `await` y pásale el texto original, sin convertirlo a JSON. Exporta `invoicePOST` como el manejador `POST` de esa ruta.

```ts
declare function saveInvoiceAndSchedule(event: ReceivedWebhookEvent): Promise<void>;

export async function invoicePOST(request: Request) {
  const secret = process.env.TIMBRO_INVOICES_WEBHOOK_SECRET;
  if (!secret) return new Response(null, { status: 503 });
  let event: ReceivedWebhookEvent;
  try {
    event = await invoices.webhooks.verify({
      body: await request.text(),
      headers: Object.fromEntries(request.headers),
      secret,
    });
  } catch {
    return new Response(null, { status: 401 });
  }
  await saveInvoiceAndSchedule(event);
  return new Response(null, { status: 204 });
}
```

Verifica los bytes originales con el `secret` del endpoint, que demuestra que el aviso viene de Timbro. Llama a `receive_invoice_webhook` desde la ruta `POST` y responde con el estado que devuelve.

```python
def save_invoice_and_schedule(event: invoice_webhooks.WebhookEvent) -> None:
    """Tu aplicación: guarda el evento y su tarea en una misma transacción."""
    raise NotImplementedError


def receive_invoice_webhook(body: bytes, headers: Mapping[str, str]) -> int:
    secret = os.environ.get("TIMBRO_INVOICES_WEBHOOK_SECRET")
    if not secret:
        return 503
    try:
        event = invoice_webhooks.verify(body=body, headers=headers, secret=secret)
    except WebhookVerificationError:
        return 401
    save_invoice_and_schedule(event)
    return 204
```

Implementa `saveInvoiceAndSchedule` (`save_invoice_and_schedule` en Python) con las mismas reglas de guardado y duplicados del ejemplo de pagos. En la tarea pendiente, `event.data.id` es el ID canónico de la factura. `invoice.accepted` avisa de la aceptación, que puede ser `accepted` o `acceptedWithObservations`. Revisa las observaciones si existen. `invoice.ready` permite revisar si los archivos están listos:

- [TypeScript](https://timbro.do/docs/cobros/webhooks/#tab-panel-5-0)
- [Python](https://timbro.do/docs/cobros/webhooks/#tab-panel-5-1)

```ts
declare function saveInvoicePdfOnce(invoiceId: string, bytes: Uint8Array): Promise<void>;

export async function processInvoiceEvent(event: ReceivedWebhookEvent) {
  if (!isRecognizedWebhookEvent(event)) return;
  if (event.type !== "invoice.accepted" && event.type !== "invoice.ready") return;
  const invoice = await invoices.invoices.retrieve(event.data.id);
  if (!invoice) return;
  const accepted = invoice.status === "accepted" || invoice.status === "acceptedWithObservations";
  if (!accepted || invoice.files.pdf.status !== "ready") return;
  const pdf = await invoices.invoices.downloadPdf(invoice.id);
  await saveInvoicePdfOnce(invoice.id, pdf.bytes);
}
```

```python
def save_invoice_pdf_once(invoice_id: str, content: bytes) -> None:
    """Tu aplicación: guarda el PDF una sola vez."""
    raise NotImplementedError


def process_invoice_event(event: invoice_webhooks.WebhookEvent) -> None:
    if not invoice_webhooks.is_known_event(event):
        return
    if event["type"] not in ("invoice.accepted", "invoice.ready"):
        return
    invoice = invoices.invoices.retrieve(event["data"]["id"])
    accepted = invoice["status"] in ("accepted", "acceptedWithObservations")
    if not accepted or invoice["files"]["pdf"]["status"] != "ready":
        return
    pdf = invoices.invoices.download_pdf(invoice["id"])
    save_invoice_pdf_once(invoice["id"], pdf.content)
```

`saveInvoicePdfOnce` (`save_invoice_pdf_once` en Python) guarda el PDF en tu aplicación sin repetirlo. Si el archivo sigue pendiente o falla una consulta, conserva la tarea y vuelve a consultar la misma factura. Para enviar la factura al cliente o al contador, sigue [Descargar y enviar la factura](https://timbro.do/docs/comprobantes/entregar-comprobante/). Procesa también rechazos y casos `operatorRequired` para avisar a la persona que debe resolverlos.

## Otros eventos

Agrega a `enabledEvents` solo los que procesas. Al recibir cada uno, consulta el recurso que cambió para conocer su estado actual:

- Con `subscription.canceled` y `subscription.status_changed`, consulta la suscripción con `timbro.subscriptions.retrieve(event.data.id)`.
- Con `payment.succeeded`, `payment.declined`, `payment.canceled`, `payment.expired` y `payment.operator_required`, consulta el pago con `timbro.payments.retrieve(event.data.id)`.
- Con `refund.succeeded`, `refund.declined`, `refund.operator_required` y `refund.invoiceAttached`, consulta el reembolso con `timbro.refunds.retrieve(event.data.id)`.
- Con `invoice.accepted`, `invoice.rejected`, `invoice.operatorRequired` e `invoice.ready`, consulta la factura con `invoices.invoices.retrieve(event.data.id)`.
- Con `invoice.deliverySent` e `invoice.deliveryFailed`, consulta el envío por correo con `invoices.invoices.retrieveDelivery`, usando `event.data.id` como `invoiceId` y `event.data.deliveryId` como `deliveryId`.

`payment.declined` avisa de cada intento que el adquirente rechazó; el pago sigue abierto y su `lastDecline` indica el motivo. No canceles el pedido por este evento: el cliente puede volver a intentarlo hasta que el pago venza.

`refund.succeeded` avisa que el adquirente confirmó el reembolso. `refund.declined` avisa que lo rechazó de forma definitiva; el monto vuelve a estar disponible y puedes solicitar otro reembolso con una `refundReference` nueva. Cada reembolso emite uno solo de estos dos eventos, y uno que queda en `operator_required` emite `refund.operator_required` en su lugar. Como cualquier aviso, un evento de reembolso es solo una notificación: verifica su firma, descarta un `event.id` que ya procesaste y consulta el reembolso antes de actualizar tu sistema.

Si solicitaste un e-CF, consulta también su [estado y sus archivos](https://timbro.do/docs/comprobantes/entregar-comprobante/). Si falta una notificación, revisa los envíos de tu webhook con `GET /v1/webhook-endpoints/{webhookEndpointId}/deliveries` y pide que Timbro los reenvíe con `POST /v1/webhook-endpoints/{webhookEndpointId}/replays` y una clave de idempotencia (`Idempotency-Key`), indicando los `eventIds` o un periodo de hasta 7 días. Ese reenvío no debe hacer que tu sistema entregue el pedido otra vez.

Timbro intenta cada envío hasta 8 veces durante un día. Si tu servidor estará fuera de servicio, puedes suspender el webhook con `POST /v1/webhook-endpoints/{webhookEndpointId}/suspend` y reactivarlo después con `.../reactivate`. Para cambiar el secreto de firma, usa `.../rotate-secret`, también con una clave de idempotencia: el secreto anterior sigue firmando durante 24 horas.

[Página anterior\
Conecta tu afiliación](https://timbro.do/docs/negocio/conectar-afiliacion/)[Siguiente página\
Manejar errores](https://timbro.do/docs/produccion/errores/)

Vende, atiende a tus clientes y cobra desde tu celular

© 2026 Timbro. Todos los derechos reservados

[Español](https://timbro.do/docs/cobros/webhooks/)[English](https://timbro.do/en/docs/cobros/webhooks/)

## Productos

[Facturación](https://timbro.do/facturacion-electronica/)[Atiende](https://timbro.do/atiende/)[Cobra](https://timbro.do/pagos/)[Portal Timbro](https://timbro.do/portal-timbro/)[Integraciones](https://timbro.do/integraciones/)[Cuenta Timbro](https://timbro.do/cuenta-timbro/)[Precios](https://timbro.do/precios/)

## Desarrolladores

[SDK y API](https://timbro.do/desarrolladores/)[Documentación](https://timbro.do/docs/)[Referencia de la API](https://timbro.do/docs/reference/payments/)

## Empresa

[INDEXA, la empresa detrás de Timbro](https://indexa.do/)[Presentaciones](https://timbro.do/presentaciones/)

## Ayuda

[Cómo empezar](https://timbro.do/como-empezar/)[Guía de facturación electrónica](https://timbro.do/guias/facturacion-electronica-republica-dominicana/)[Contacto](https://timbro.do/contacto/)<hola@timbro.do>

## Legal

[Condiciones para negocios](https://timbro.do/politicas/negocios/)[Política de uso](https://timbro.do/politicas/uso/)[Privacidad](https://timbro.do/politicas/privacidad/)[Seguridad](https://timbro.do/politicas/seguridad/)[Todos los documentos](https://timbro.do/politicas/)
