Recibir webhooks de Peakly
Configurá endpoints HTTP que reciban eventos en tiempo real cuando ARCA autoriza una factura, en lugar de hacer polling sobre la API
Los webhooks te permiten recibir notificaciones HTTP POST cada vez que ocurre un evento importante en una factura — por ejemplo, cuando ARCA autoriza un comprobante y devuelve el CAE. Es la forma más eficiente de mantener sincronizados sistemas externos (ERP, contabilidad, e-commerce) sin hacer polling sobre GET /v1/sales/sales-receipts.
Cuándo usar webhooks
- Querés disparar lógica en tu sistema apenas un comprobante recibe CAE (registrar el asiento contable, enviar la factura por email, actualizar stock, etc.)
- Tu integración tiene volumen alto y el polling se vuelve ineficiente o caro
- Necesitás reaccionar a anulaciones (Notas de Crédito) en tiempo real
Eventos disponibles
- invoice.authorized — ARCA devolvió CAE para una Factura, Nota de Crédito o Nota de Débito. El comprobante queda en estado
Creada. - invoice.voided — Un comprobante fue anulado mediante una Nota de Crédito. La NC en sí dispara su propio
invoice.authorized. - invoice.paid — *Reservado para una iteración futura.* En el formulario aparece como "Factura cobrada", pero suscribirte hoy no entrega eventos.
Crear un endpoint
- Ingresá a Desarrolladores > Webhooks (grupo Conectar del menú lateral)
- Hacé clic en Agregar webhook
- Completá el formulario:
- URL del endpoint: la URL HTTPS de tu sistema que recibirá los POST (ej.
https://tu-sistema.com/webhooks/peakly) - Descripción: opcional, para identificar la integración
- Header de autorización: opcional. Si tu endpoint exige autenticación, ingresá el valor que Peakly debe mandar en el header
Authorizationde cada entrega (ej.Bearer <token>). - Eventos a suscribir: marcá los eventos que querés recibir (Factura autorizada, Factura anulada) o Todos los eventos
- URL del endpoint: la URL HTTPS de tu sistema que recibirá los POST (ej.
- Hacé clic en Crear webhook
- En la ventana Webhook creado, copiá el secreto de firma (formato
whsec_...) y guardalo en tu secrets manager. Después hacé clic en Listo
El secreto solo se muestra una vez
También vía API
Header de autorización
Si tu endpoint está protegido por un token, podés indicarle a Peakly un valor de Authorization para que lo envíe en cada entrega. Esto te permite validar del lado tuyo que el request realmente viene de Peakly (además de la firma HMAC) o atravesar un API gateway que exige autenticación.
- Lo configurás en el campo Header de autorización del formulario, o con el campo
authorizational crear/editar el endpoint vía API. - Peakly lo envía tal cual en el header
Authorizationde cada POST. Por ejemploBearer <token>,Basic <base64>o cualquier esquema que tu endpoint espere. - Es opcional. Si no lo configurás, las entregas no incluyen header
Authorization(la autenticidad se valida con la firma HMAC). - Límite de 4096 caracteres. No puede contener saltos de línea ni caracteres de control.
El valor es de solo escritura
Editar vía API
Estructura del payload
Toda entrega usa el mismo sobre. El campo data cambia según el evento:
{
"event": "invoice.authorized",
"deliveryId": "9f4b1d4a-7b6c-4e5b-9a90-2f1c5e4e6b21",
"data": {
"id": "uuid-receipt-123",
"number": "00001-00000042",
"status": "Creada",
"total": "12100.00",
"date": "2026-05-04T00:00:00.000Z",
"customerId": 7,
"cae": "75123456789012",
"caeExpiration": "2026-05-14T00:00:00.000Z"
}
}Campos del objeto data:
- id (UUID): id del comprobante. Usalo para traer el detalle completo con
GET /v1/sales/sales-receipts/:idsi necesitás más info (líneas, impuestos, observaciones). - number (string): número formateado
<pos>-<seq>. Los webhooks se disparan después de la confirmación, así que siempre es un número real. - status (string): estado del comprobante (
Creada,Anulada). - total (string decimal): monto total. Va como string para preservar precisión — parsealo con tu librería de decimal preferida.
- date (ISO 8601): fecha del comprobante.
- customerId (number): id del cliente.
- cae (string | null): código de autorización de ARCA. Puede ser
nullen algunos casos deinvoice.voided. - caeExpiration (ISO 8601 | null): vencimiento del CAE.
nullcuandocaeesnull.
Headers HTTP
Cada POST a tu endpoint incluye:
- Content-Type: siempre
application/json - X-Peakly-Event: el tipo de evento (ej.
invoice.authorized) - X-Peakly-Signature: firma HMAC-SHA256 del cuerpo crudo, formato
sha256=<hex> - X-Peakly-Delivery: id único de la entrega (UUID). Usalo como clave de idempotencia del lado tuyo.
- Authorization: solo si configuraste un header de autorización en el endpoint. Se envía con el valor exacto que guardaste.
Timeout: 30 segundos
Verificar la firma HMAC
Calculá HMAC-SHA256 sobre el cuerpo crudo del request (no re-serialices el JSON parseado) usando el secreto del endpoint. Compará contra X-Peakly-Signature con comparación de tiempo constante para evitar timing attacks.
// Node.js / TypeScript
import { createHmac, timingSafeEqual } from "crypto";
function verifyPeaklySignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
// Express ejemplo: usá express.raw() para acceder al body crudo
app.post(
"/webhooks/peakly",
express.raw({ type: "application/json" }),
(req, res) => {
const raw = req.body.toString("utf8");
const sig = req.header("X-Peakly-Signature") ?? "";
if (!verifyPeaklySignature(raw, sig, process.env.PEAKLY_WEBHOOK_SECRET!)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(raw);
// procesá event.data acá
res.status(200).send("ok");
},
);# Python
import hmac, hashlib
def verify_peakly_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# Flask ejemplo
from flask import request, abort
@app.post("/webhooks/peakly")
def peakly_webhook():
raw = request.get_data() # bytes crudos, antes del parse
sig = request.headers.get("X-Peakly-Signature", "")
if not verify_peakly_signature(raw, sig, os.environ["PEAKLY_WEBHOOK_SECRET"]):
abort(401)
event = request.get_json()
# procesá event["data"] acá
return "ok", 200Usá el cuerpo crudo, no el parseado
Idempotencia y orden
Una misma entrega puede llegar varias veces (hasta 7 intentos). Para evitar procesar el mismo evento dos veces:
- Guardá el header X-Peakly-Delivery (o
deliveryIddel cuerpo) en una tabla con índice único - Antes de procesar, verificá si ya viste ese id; si sí, devolvé 200 y salí
- Dentro de los reintentos de una entrega el cuerpo es byte-idéntico, así que la firma es estable
El orden no está garantizado
Reintentos
- Hasta 7 intentos por entrega (el original más los reintentos)
- Backoff exponencial factor 3, con esperas de entre 10 minutos y 24 horas
- Reintenta ante: respuesta no-2xx, timeout de 30s, o error de red
- Después de agotar los intentos, la entrega queda con estado Fallo y podés reintentarla manualmente con el botón Reintentar del panel Entregas
Inspeccionar entregas
En Desarrolladores > Webhooks ves tus endpoints en una tabla con URL, Estado (Activo / Inactivo), Eventos y Última entrega; el filtro Filtrar por URL o descripción te ayuda cuando tenés varios. Abrí el menú de tres puntos de la fila y elegí Ver entregas para abrir el panel Entregas: ahí filtrás por período (24h, 7 días, 30 días o Personalizado) y por Estado (Todos / OK / Fallo / Pendiente), y ves el tipo de evento, el estado, los intentos y el código HTTP de cada entrega. Al expandir una entrega ves el detalle y el payload enviado. Las entregas fallidas tienen un botón Reintentar para reencolarlas manualmente.
El mismo menú de tres puntos tiene Editar (URL, descripción, header de autorización y eventos; el secreto de firma no cambia), Deshabilitar / Habilitar y Eliminar. Debajo de la tabla, la Guía de integración resume headers, payload, verificación de firma e idempotencia.
Vía API podés consultar GET /v1/webhooks/:id/deliveries (paginado, máximo 100 por página) y reintentar con POST /v1/webhooks/:id/deliveries/:deliveryId/retry.
Probar localmente
Para probar webhooks contra tu máquina local sin desplegar, exponé el puerto con un túnel HTTP:
- ngrok:
ngrok http 3000— copiá la URLhttps://...ngrok.appy registrala como endpoint - Cloudflare Tunnel:
cloudflared tunnel --url http://localhost:3000 - localtunnel:
lt --port 3000
Endpoints de prueba
Límites
- Hasta 10 endpoints por organización
- Timeout de request: 30 segundos
- Intentos: hasta 7 por entrega (factor 3, esperas de 10 min a 24 h)
- Listado de entregas: máximo 100 por página (default 50)
- Throttle en endpoints de gestión: 20 requests / minuto
Buenas prácticas
- Usá siempre HTTPS: en producción Peakly rechaza URLs http. Tampoco se aceptan URLs que resuelvan a direcciones privadas (localhost, redes internas) ni se siguen redirecciones
- Verificá la firma HMAC en toda entrega antes de procesarla
- Implementá idempotencia con
X-Peakly-Deliverydesde el día uno - Devolvé 200 rápido y procesá en un job en background si tu lógica es lenta
- Guardá el secreto en variables de entorno o secrets manager, nunca en el código fuente
- Suscribite solo a los eventos que realmente vas a procesar — reduce ruido y carga
- Monitoreá entregas fallidas regularmente; un endpoint caído acumula
failedrápidamente
Checklist de integración
- Crear el endpoint en Desarrolladores > Webhooks y guardar el secreto
- Implementar el handler HTTP que: lee el body crudo, verifica
X-Peakly-Signature, deduplica conX-Peakly-Delivery, procesa el evento y devuelve 2xx en menos de 30s - Para
invoice.authorized: persistirdata.caeydata.caeExpirationasociados adata.iden tu sistema - Para
invoice.voided: marcar el comprobante referenciado pordata.idcomo anulado en tu sistema - Probar contra webhook.site o ngrok antes de apuntar a producción
- Monitorear el panel de entregas en los primeros días y resolver fallos rápido
Errores comunes
- Firma inválida — calculaste HMAC sobre el JSON parseado en vez del cuerpo crudo, o mezclaste el secreto de otro endpoint
- Eventos duplicados — no implementaste idempotencia; el mismo
deliveryIdse procesa más de una vez por reintentos - Timeout — el handler hace trabajo pesado en línea (envío de mails, sync con ERP); moverlo a un job
- Eventos perdidos — el endpoint estuvo caído durante todos los intentos (hasta 7); usá el botón Reintentar o
POST /v1/webhooks/:id/deliveries/:deliveryId/retry - No llegan eventos — el endpoint está marcado como
Inactivo, o no estás suscripto al evento, o la URL devuelve 4xx en el primer intento
Artículos relacionados
Integraciones
Integraciones
Ventas
Factura Electrónica