PricelyDocs

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.

Business9 min de lectura

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.

El cuadro de diálogo Webhook con una URL de endpoint, un secreto de firma, la lista de eventos y el botón Enviar prueba
El cuadro de diálogo Webhook, abierto desde la página Automatización.

Configure un webhook

  1. Abra Automatización

    Elija Automatizaciones en la barra lateral y busque la tarjeta Webhooks.

  2. 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.

  3. 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.

  4. Envíe una prueba

    Haga clic en Enviar prueba. Pricely envía un evento ping firmado y muestra lo que ha respondido su endpoint, por ejemplo Entregado. Su endpoint respondió 200.

  5. 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

EventoSe envía cuando
quote.acceptedUn 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.paidUna 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.overdueUna 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.
pingUsted 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:

CabeceraValor
Content-Typeapplication/json
User-AgentPricely-Webhooks/1.0
X-Pricely-EventEl nombre del evento, por ejemplo invoice.paid.
X-Pricely-DeliveryUn ID único de esta entrega, como evt_3f9a…. Los reintentos conservan el mismo ID.
X-Pricely-Signaturet=<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.

Cabeceras de la solicitud
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…
quote.accepted
{
  "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"
}
invoice.paid
{
  "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.

CampoTipoNotas
idstringEl ID del presupuesto o de la factura en Pricely.
numberstringEl número de documento que ve su cliente.
clientId, clientName, clientEmailstringCadenas vacías cuando el documento no tiene un cliente vinculado o el cliente no tiene correo electrónico.
total, amountPaid, balanceDuenumberImportes en unidades principales, por ejemplo 4800 para $4,800.00.
currencystringEl código de moneda de tres letras del documento.
acceptedAt, dueDate, paidAt, timestampstring o nullFechas y horas ISO 8601 en UTC.

El evento de prueba usa las mismas cabeceras y la misma firma. Su cuerpo tiene este aspecto:

ping
{
  "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.

  1. 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.

  2. Divida la cabecera

    Divídala por las comas y luego divida cada parte por su primer = para obtener t y v1.

  3. Vuelva a calcular el HMAC

    Calcule el HMAC-SHA256 de t.body con su secreto whsec_… y compárelo con v1 mediante una comparación de tiempo constante.

  4. Compruebe la hora

    Rechace las solicitudes cuyo t difiera en más de cinco minutos del reloj de su servidor. Así se evita que se reproduzcan solicitudes antiguas.

Node.js (Express)
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);
Python
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, como localhost, *.local o 192.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 2xx confirma 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 t nuevo.
Si su endpoint…Qué hace Pricely
Responde 2xxEntregado. Sin reintento.
No está accesible o tarda más de 10 segundosReintenta.
Responde 408, 429 o 5xxReintenta.
Responde cualquier otro 4xx, como 400 o 401Se 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

ProblemaQué 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 sSu 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 coincideVerifique 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-SignatureEl 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 vecesUn reintento tras un tiempo de espera agotado o un 5xx. Elimine los duplicados con X-Pricely-Delivery.
No llega invoice.overdue para una factura antiguaEs 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.

¿Le ha resultado útil esta página? ¿Falta algo o no está claro? Díganoslo y corregiremos la página.Escribir a support@pricely.me
Esc
↑↓ para moverse↵ para abrirEsc para cerrar