Webhooks
Conecte Pricely con Zapier, Make o su propio código. Pricely envía un POST JSON firmado a su URL cuando ocurren eventos clave, y lo reintenta si su endpoint no está disponible durante un momento.
Un webhook es una URL a la que Pricely llama cuando ocurre algo en su espacio de trabajo. Diríjalo a un escenario de Zapier o Make para actualizar un CRM, publicar en un canal de chat o añadir una fila a una hoja de cálculo, o a su propio servidor. Cada solicitud va firmada, de modo que su código puede comprobar que procede de Pricely.

Configure un webhook
- Abra Automatización
Elija Automatizaciones en la barra lateral y busque la tarjeta Webhooks.
- Añada su endpoint
Haga clic en Configurar webhook y pegue la URL del endpoint, por ejemplo una URL de Catch Hook de Zapier o de Custom webhook de Make. Debe ser una dirección
https://pública. - Copie el secreto de firma
El cuadro de diálogo muestra un Secreto de firma que empieza por
whsec_. Haga clic en Copiar y guárdelo con la configuración de su endpoint. Lo necesitará para verificar las firmas. - Envíe una prueba
Haga clic en Enviar prueba. Pricely envía un evento
pingfirmado y muestra lo que ha respondido su endpoint, por ejemplo Entregado. Su endpoint respondió 200. - Guarde
Haga clic en Guardar webhook. La tarjeta muestra entonces la URL a la que envía los eventos, y el botón pasa a ser Actualizar webhook.
Cada espacio de trabajo tiene una sola URL de webhook. Para cambiarla, abra el mismo cuadro de diálogo y guarde una URL nueva. Para dejar de enviar eventos, haga clic en Quitar.
Eventos
| Evento | Se envía cuando |
|---|---|
quote.accepted | Un presupuesto pasa a aceptado: el cliente lo firma en el enlace del presupuesto, o alguien de su equipo usa Aceptar en la aplicación. |
invoice.paid | Una factura se paga por completo por cualquier vía: un pago con tarjeta a través de Stripe, un pago que registra usted mismo, Marcar como pagada por completo, una acción masiva o una nota de crédito que salda el importe. |
invoice.overdue | Una factura impagada supera su fecha de vencimiento. Se envía una sola vez por factura, en la comprobación diaria de las 07:00 UTC. |
ping | Usted hace clic en Enviar prueba. |
Las facturas que se crean ya pagadas, por ejemplo mediante una importación, no envían invoice.paid. Un pago parcial tampoco lo envía; el evento se dispara cuando el saldo pendiente llega a cero.
Formato de la solicitud
Cada evento es un POST con un cuerpo JSON y estas cabeceras:
| Cabecera | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Pricely-Webhooks/1.0 |
X-Pricely-Event | El nombre del evento, por ejemplo invoice.paid. |
X-Pricely-Delivery | Un ID único de esta entrega, como evt_3f9a…. Los reintentos conservan el mismo ID. |
X-Pricely-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>. Consulte Verifique las firmas. |
El cuerpo siempre tiene las mismas cuatro claves: id (el ID de la entrega, igual que X-Pricely-Delivery), event, data y timestamp.
POST /pricely-webhook HTTP/1.1
Content-Type: application/json
User-Agent: Pricely-Webhooks/1.0
X-Pricely-Event: invoice.paid
X-Pricely-Delivery: evt_3f9a6c0e1b7d4a52a8e2f1c9d0b4e7a6
X-Pricely-Signature: t=1793372645,v1=5d1c0f…
{
"id": "evt_3f9a6c0e1b7d4a52a8e2f1c9d0b4e7a6",
"event": "quote.accepted",
"data": {
"id": "q_8f2c1a",
"number": "QT-0042",
"clientId": "c_71b0d2",
"clientName": "Northwind Studio",
"clientEmail": "ops@example.com",
"total": 4800,
"currency": "USD",
"acceptedAt": "2026-10-30T15:04:05.000Z"
},
"timestamp": "2026-10-30T15:04:05.120Z"
}
{
"id": "evt_9b2e4d7a0c5f4e1b9a3d6c8e2f7a1b40",
"event": "invoice.paid",
"data": {
"id": "inv_51c9e0",
"number": "INV-0042",
"clientId": "c_71b0d2",
"clientName": "Northwind Studio",
"clientEmail": "ops@example.com",
"total": 4800,
"amountPaid": 4800,
"balanceDue": 0,
"currency": "USD",
"dueDate": "2026-11-13T12:00:00.000Z",
"paidAt": "2026-10-30T15:04:05.000Z"
},
"timestamp": "2026-10-30T15:04:05.120Z"
}
invoice.overdue incluye los mismos campos data que invoice.paid, con el importe aún adeudado en balanceDue y paidAt en null.
| Campo | Tipo | Notas |
|---|---|---|
id | string | El ID del presupuesto o de la factura en Pricely. |
number | string | El número de documento que ve su cliente. |
clientId, clientName, clientEmail | string | Cadenas vacías cuando el documento no tiene un cliente vinculado o el cliente no tiene correo electrónico. |
total, amountPaid, balanceDue | number | Importes en unidades principales, por ejemplo 4800 para $4,800.00. |
currency | string | El código de moneda de tres letras del documento. |
acceptedAt, dueDate, paidAt, timestamp | string o null | Fechas y horas ISO 8601 en UTC. |
El evento de prueba usa las mismas cabeceras y la misma firma. Su cuerpo tiene este aspecto:
{
"id": "evt_0c4d2a9e7f1b4c3d8e6a5b2f9d1c7e30",
"event": "ping",
"data": {
"message": "Test event from Pricely",
"orgId": "org_2f81c4"
},
"timestamp": "2026-10-30T15:04:05.000Z"
}
Verifique las firmas
Compruebe la firma antes de confiar en una solicitud. X-Pricely-Signature tiene dos partes: t, la hora Unix en segundos en que se firmó la solicitud, y v1, un HMAC-SHA256 en hexadecimal de la cadena t + . + el cuerpo sin procesar de la solicitud, con su secreto de firma como clave.
- Lea el cuerpo sin procesar
Use exactamente los bytes recibidos. Si analiza el JSON y lo vuelve a serializar, los bytes cambian y la firma deja de coincidir.
- Divida la cabecera
Divídala por las comas y luego divida cada parte por su primer
=para obtenertyv1. - Vuelva a calcular el HMAC
Calcule el HMAC-SHA256 de
t.bodycon su secretowhsec_…y compárelo conv1mediante una comparación de tiempo constante. - Compruebe la hora
Rechace las solicitudes cuyo
tdifiera en más de cinco minutos del reloj de su servidor. Así se evita que se reproduzcan solicitudes antiguas.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.PRICELY_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;
function verifyPricelySignature(header, rawBody, secret) {
const parts = {};
for (const piece of String(header || "").split(",")) {
const i = piece.indexOf("=");
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && crypto.timingSafeEqual(expected, given);
}
// express.raw keeps the exact bytes Pricely signed.
app.post("/pricely-webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verifyPricelySignature(req.get("X-Pricely-Signature"), rawBody, SECRET)) {
return res.status(400).send("Invalid signature");
}
const delivery = JSON.parse(rawBody);
res.sendStatus(200); // answer first, then do the work
// Skip delivery.id if you've already handled it (retries reuse it).
if (delivery.event === "invoice.paid") {
console.log(`${delivery.data.number} paid: ${delivery.data.amountPaid} ${delivery.data.currency}`);
}
});
app.listen(3000);
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_pricely_signature(header: str, raw_body: bytes, secret: str) -> bool:
parts = {}
for piece in (header or "").split(","):
key, sep, value = piece.partition("=")
if sep:
parts[key.strip()] = value.strip()
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE_SECONDS or "v1" not in parts:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
# Flask: verify_pricely_signature(request.headers.get("X-Pricely-Signature"),
# request.get_data(), SECRET)
Entrega y reintentos
- Solo HTTPS. Pricely solo entrega a URL
https://públicas. Las direcciones locales y de redes privadas, comolocalhost,*.localo192.168.x.x, están bloqueadas. - Sin redirecciones. Si su URL redirige a otro sitio, la entrega falla. Use la URL final.
- Responda con un 2xx en menos de 10 segundos. Cualquier código
2xxconfirma la recepción. Responda primero y haga el trabajo lento después. - Reintentos automáticos. Pricely hace hasta tres intentos en total, con una espera de 1 segundo y luego de 2 segundos entre ellos. Cada intento se vuelve a firmar con un
tnuevo.
| Si su endpoint… | Qué hace Pricely |
|---|---|
Responde 2xx | Entregado. Sin reintento. |
| No está accesible o tarda más de 10 segundos | Reintenta. |
Responde 408, 429 o 5xx | Reintenta. |
Responde cualquier otro 4xx, como 400 o 401 | Se detiene. El fallo es definitivo. |
El botón Enviar prueba hace un único intento sin reintentos, para que vea el resultado al instante. El cuadro de diálogo muestra también la Última entrega: el evento, si se entregó o falló, el código de estado, cuándo ocurrió y cuántos intentos fueron necesarios.
Solución de problemas
| Problema | Qué comprobar |
|---|---|
| Use una URL https:// pública. Las direcciones locales y de redes privadas están bloqueadas. | La URL apunta a su propio equipo o a una red privada. Use una URL HTTPS pública, o un túnel que le proporcione una. |
| Su endpoint respondió 401. (u otro código) | Pricely llegó a su endpoint, pero este rechazó la solicitud. Cualquier código fuera del intervalo 200–299 cuenta como fallo. Revise los registros de su endpoint. |
| No se pudo contactar con el endpoint: sin respuesta en 10 s | Su endpoint es demasiado lento. Responda primero con un 2xx y haga el trabajo después. |
| No se pudo contactar con el endpoint (cualquier otro motivo) | El servidor no respondió, su certificado TLS no es válido o la URL redirige. Compruebe que la URL se abre en un navegador y que es su dirección final. |
| La firma no coincide | Verifique con el cuerpo sin procesar, no con el JSON vuelto a serializar, y use el secreto que se muestra actualmente en el cuadro de diálogo. Si hizo clic en Nuevo secreto, guarde el webhook y actualice su endpoint. |
No hay cabecera X-Pricely-Signature | El webhook se guardó sin secreto. Abra el cuadro de diálogo y haga clic en Guardar webhook para guardar uno. |
| El mismo evento llegó dos veces | Un reintento tras un tiempo de espera agotado o un 5xx. Elimine los duplicados con X-Pricely-Delivery. |
No llega invoice.overdue para una factura antigua | Es lo esperado. El evento se envía una sola vez, cuando la factura supera por primera vez su fecha de vencimiento, no para facturas que ya estaban vencidas antes. |
Planes
Los webhooks están incluidos en Business y Enterprise. En los demás planes, la tarjeta Webhooks muestra un candado y un enlace para mejorar el plan.