---
title: "Receive webhooks | Timbro"
description: "Receive updates and confirm the result from your server"
canonical: "https://timbro.do/en/docs/cobros/webhooks/"
---

Webhooks and errors

# Receive webhooks

A webhook announces that a payment or invoice changed. Verify its signature and store it before retrieving the current resource. This guide covers payments first, followed by an independent Invoices flow that does not require the Payments SDK.

## Register the URL

- [TypeScript](https://timbro.do/en/docs/cobros/webhooks/#tab-panel-0-0)
- [Python](https://timbro.do/en/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",
)
```

Store the endpoint `secret` on your server as `TIMBRO_WEBHOOK_SECRET`: it is returned only when issued. Keep the same idempotency key (`Idempotency-Key`), `idempotencyKey` in TypeScript or `idempotency_key` in Python, if you retry registration. Test and Live need their own endpoints and secrets.

## Receive and store the event

The handler below uses `saveAndSchedule` (`save_and_schedule` in Python), a function from **your application** that saves the complete event and schedules its processing in one durable transaction. It deduplicates by `event.id` and preserves pending work if another delivery arrives.

- [TypeScript](https://timbro.do/en/docs/cobros/webhooks/#tab-panel-1-0)
- [Python](https://timbro.do/en/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 });
}
```

Call `receive_webhook` from your application’s `POST` route with the original body, as `bytes`, and the request headers. Answer with the HTTP status it returns.

```python
def save_and_schedule(event: webhooks.WebhookEvent) -> None:
    """Your application: save the event and its task in one transaction."""
    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` needs only the signing secret, not an API key. Verify the original bytes before parsing the JSON. Return `2xx` only after storing the event; if storage fails, return an error so Timbro retries. Keep your server clock in sync: the signature allows a five-minute difference.

A signed event whose type this SDK version does not know yet is still delivered as an unrecognized event. Store it and skip it when processing, using `webhooks.isKnownWebhookEvent` in Payments and `isRecognizedWebhookEvent` in Invoices, or `webhooks.is_known_event` in both Python packages, instead of rejecting it.

## Confirm the payment in the background

`applyPaymentOnce` (`apply_payment_once` in Python) also belongs to your application. It must check the reference, currency, and amount against the order, then record the payment once. External effects such as delivery or email must also be recoverable without duplication.

- [TypeScript](https://timbro.do/en/docs/cobros/webhooks/#tab-panel-2-0)
- [Python](https://timbro.do/en/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:
    """Your application: check the order and record the payment once."""
    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)
```

Events can be repeated or arrive out of order. `event.id` identifies the event; `event.data.id` identifies the Payment or Invoice you retrieve. If the order does not exist yet, keep the work so you can retry it.

## Receive invoice notifications

For an invoices-only integration, use `@timbro/invoices` or, in Python, `timbro-invoices`. Register a separate URL with an API key that has Invoicing access:

- [TypeScript](https://timbro.do/en/docs/cobros/webhooks/#tab-panel-3-0)
- [Python](https://timbro.do/en/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",
});
```

Register the URL once with `webhook_endpoints.create` on the `timbro-invoices` client; you do not need `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",
)
```

Store the endpoint `secret` as `TIMBRO_INVOICES_WEBHOOK_SECRET`. Use a separate route for invoices because payment events have a different envelope.

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

Invoices verification is asynchronous: use `await` and pass the original text without parsing JSON. Export `invoicePOST` as the `POST` handler on that route.

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

Verify the original bytes with the endpoint’s `secret`, which proves the delivery comes from Timbro. Call `receive_invoice_webhook` from the `POST` route and answer with the status it returns.

```python
def save_invoice_and_schedule(event: invoice_webhooks.WebhookEvent) -> None:
    """Your application: save the event and its task in one transaction."""
    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
```

Implement `saveInvoiceAndSchedule` (`save_invoice_and_schedule` in Python) using the same durable storage and duplicate rules as the payment example. In the queued worker, `event.data.id` is the canonical invoice ID. `invoice.accepted` announces acceptance, which can be `accepted` or `acceptedWithObservations`. Review any observations. `invoice.ready` lets you check file readiness:

- [TypeScript](https://timbro.do/en/docs/cobros/webhooks/#tab-panel-5-0)
- [Python](https://timbro.do/en/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:
    """Your application: store the PDF once."""
    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` in Python) stores the PDF in your application without duplication. If a file remains pending or a query fails, retain the task and retrieve the same invoice again. Follow [Download and send the invoice](https://timbro.do/en/docs/comprobantes/entregar-comprobante/) to send the invoice to the customer or accountant. Also process rejected and `operatorRequired` outcomes so the responsible person can resolve them.

## Other events

Add only the events you process to `enabledEvents`. When each one arrives, retrieve the resource that changed to learn its current state:

- For `subscription.canceled` and `subscription.status_changed`, retrieve the subscription with `timbro.subscriptions.retrieve(event.data.id)`.
- For `payment.succeeded`, `payment.declined`, `payment.canceled`, `payment.expired`, and `payment.operator_required`, retrieve the payment with `timbro.payments.retrieve(event.data.id)`.
- For `refund.succeeded`, `refund.declined`, `refund.operator_required`, and `refund.invoiceAttached`, retrieve the refund with `timbro.refunds.retrieve(event.data.id)`.
- For `invoice.accepted`, `invoice.rejected`, `invoice.operatorRequired`, and `invoice.ready`, retrieve the invoice with `invoices.invoices.retrieve(event.data.id)`.
- For `invoice.deliverySent` and `invoice.deliveryFailed`, retrieve the email delivery with `invoices.invoices.retrieveDelivery`, passing `event.data.id` as `invoiceId` and `event.data.deliveryId` as `deliveryId`.

`payment.declined` reports each attempt the acquirer declined; the payment stays open and its `lastDecline` gives the reason. Do not cancel the order on this event: the customer can try again until the payment expires.

`refund.succeeded` reports that the acquirer confirmed the money was returned. `refund.declined` reports a definitive refusal; the amount becomes available again, and you may request another refund with a new `refundReference`. Each refund emits only one of these two events, and a refund that ends in `operator_required` emits `refund.operator_required` instead. Like any event, a refund event is only a notification: verify its signature, skip an `event.id` you already processed, and retrieve the refund before you update your system.

For the e-CF, retrieve its [status and files](https://timbro.do/en/docs/comprobantes/entregar-comprobante/) separately. If a notification is missing, review your webhook’s deliveries with `GET /v1/webhook-endpoints/{webhookEndpointId}/deliveries` and ask Timbro to resend them with `POST /v1/webhook-endpoints/{webhookEndpointId}/replays` and an idempotency key (`Idempotency-Key`), sending the `eventIds` or a period of up to 7 days. Receiving an event again must not make your system fulfill the order twice.

Timbro attempts each delivery up to 8 times over one day. If your server will be down, you can suspend the webhook with `POST /v1/webhook-endpoints/{webhookEndpointId}/suspend` and reactivate it later with `.../reactivate`. To change the signing secret, use `.../rotate-secret`, also with an idempotency key: the previous secret keeps signing for 24 hours.

[Previous\
Connect your merchant account](https://timbro.do/en/docs/negocio/conectar-afiliacion/)[Next\
Handle errors](https://timbro.do/en/docs/produccion/errores/)

Sell, serve your customers, and collect payments from your phone

© 2026 Timbro. All rights reserved

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

## Products

[Invoicing](https://timbro.do/en/facturacion-electronica/)[Serve](https://timbro.do/en/atiende/)[Collect](https://timbro.do/en/pagos/)[Portal Timbro](https://timbro.do/en/portal-timbro/)[Integrations](https://timbro.do/en/integraciones/)[Cuenta Timbro](https://timbro.do/en/cuenta-timbro/)[Pricing](https://timbro.do/en/precios/)

## Developers

[SDKs and API](https://timbro.do/en/desarrolladores/)[Documentation](https://timbro.do/en/docs/)[API reference](https://timbro.do/en/docs/reference/payments/)

## Company

[INDEXA, the company behind Timbro](https://indexa.do/)[Presentations](https://timbro.do/en/presentaciones/)

## Help

[How to start](https://timbro.do/en/como-empezar/)[Electronic invoicing guide](https://timbro.do/en/guias/facturacion-electronica-republica-dominicana/)[Contact](https://timbro.do/en/contacto/)<hola@timbro.do>

## Legal

[Business Terms](https://timbro.do/en/politicas/negocios/)[Terms of use](https://timbro.do/en/politicas/uso/)[Privacy](https://timbro.do/en/politicas/privacidad/)[Security](https://timbro.do/en/politicas/seguridad/)[All documents](https://timbro.do/en/politicas/)
