Desde tu sistema · Webhooks
Docs · Desde tu sistema Descargar .md

Webhooks

Un webhook es un aviso que QuickPay le manda a tu servidor (un POST con JSON) cuando pasa algo con tus cobros. Es la forma recomendada de enterarte de un pago: no tienes que consultar.

Eventos

EventoCuándo llega
link.pagadoEl banco confirmó el pago de un cobro: ya puedes entregar
link.acreditadoTe transferimos el neto de ese pago a tu cuenta. Llega con la liquidación inmediata, que se está activando por etapas
link.devueltoUna devolución que hiciste desde el portal llegó a tu cliente. Las devoluciones están en piloto: si no están activas para tu cuenta, este aviso no llega
transaccion.aprobadaAPI directa: el banco confirmó la operación (LIQUIDADA). Llega una sola vez por operación, junto con transaccion.liquidada
transaccion.liquidadaAPI directa: la operación quedó LIQUIDADA (confirmada por el banco)
transaccion.rechazadaAPI directa: el banco rechazó la operación (RECHAZADA)

link.pagado sale cuando el banco confirma que el dinero se movió, no cuando recibe la orden de débito. Según el banco de tu cliente, la confirmación puede llegar en el mismo momento del pago o el cobro pasa primero por procesando y el aviso llega cuando el banco lo confirma: suele tardar menos de un minuto, pero a veces tarda bastante más. Si el banco no lo aprueba, el cobro vuelve a pendiente y no llega ningún link.pagado. Llega una sola vez por cobro. → Estados de un cobro

transaccion.aprobada sigue la misma regla: sale cuando el banco confirma la operación (LIQUIDADA), no cuando recibe la orden de débito (ACEPTADA, "enviada a BCV"). Por eso llega junto con transaccion.liquidada y su data.estado es "LIQUIDADA". Con una llave de prueba, el cobro simulado avisa transaccion.aprobada en el acto con data.estado: "ACEPTADA" y referencia: "SIMULADO". El cuerpo de los avisos transaccion.* es { "type", "sentAt", "data": { "id", "estado", "monto", "moneda", "concepto", "referencia", "operacion" } }, donde data.id es el id que te devolvió la operación.

data.operacion dice de qué es la transacción, para que un envío de dinero no se confunda con un pago que recibiste: cobro (te pagaron), pago (salió dinero: por ejemplo, la transferencia del neto a tu cuenta o un reintegro), reverso, y las tres de una devolución: devolucion_debito (el débito a tu cuenta, con la clave de tu banco, para devolverle a tu cliente), devolucion (el dinero que le llega a tu cliente) y devolucion_retorno (si el banco de tu cliente rechazó la devolución, te regresamos lo que te debitamos). Entrega mercancía sólo con operacion: "cobro".

1. Registra tu URL

Lo más fácil: en tu portal, Integraciones › Webhooks › Agregar URL (sirve aunque todavía no tengas llave de producción). O por API, con una llave sk_live_:

bash
curl -s https://pay.pagaconquickpay.com/switch/v1/webhooks \
  -H "x-switch-key: $QP_KEY" -H "content-type: application/json" \
  -d '{ "url": "https://tu-dominio.com/webhooks/quickpay", "eventos": ["link.pagado", "link.acreditado"] }'

El webhook recibe también los avisos de producción, así que crear, cambiar o borrar webhooks por API exige una sk_live_: con una sk_test_ responde 403 con { "error": "requiere_llave_live" }. Con la llave de prueba sí puedes listarlos y ver sus entregas. La URL debe empezar con https://. La respuesta trae el secret con el que verificas la firma: se muestra una sola vez, guárdalo.

Reglas de la URL:

  • Tiene que llegar a una dirección pública de Internet. Si apunta (o su nombre resuelve) a una red interna —localhost, 127.x, 10.x, 172.16–31.x, 192.168.x, 100.64–127.x, 169.254.x o sus equivalentes IPv6— el registro responde 422 con { "error": "destino_bloqueado" }. Lo mismo al cambiar la URL con PATCH.
  • No seguimos redirecciones. Si tu servidor responde 301, 302, 307 o 308, el aviso cuenta como no entregado y se reintenta: registra la URL final.
  • Hasta 10 webhooks por comercio. Con 10, registrar otro responde 409 con { "error": "limite_webhooks" }: borra uno que no uses.

2. Lo que te llega

Cabeceras:

CabeceraQué es
x-quicklink-eventEl evento, p. ej. link.pagado
x-quicklink-timestampSegundos desde 1970 (UTC) en que se firmó
x-quicklink-signature-v2v2=<hex>: HMAC-SHA256 de timestamp + "." + cuerpo
x-quicklink-signaturesha256=<hex>: HMAC-SHA256 del cuerpo (firma anterior, se mantiene por compatibilidad)

Cuerpo de link.pagado:

json
{
  "type": "link.pagado",
  "sentAt": "2026-10-08T14:03:22.000Z",
  "data": {
    "link_id": "3f6c2a9e-8b1d-4c7a-9e2f-5a1b7c3d9e40",
    "txn_id": "b639fac6-1d2e-4f00-9a1b-3c4d5e6f7a8b",
    "estado": "pagado",
    "monto": 150,
    "moneda": "VES",
    "concepto": "Pedido #1024",
    "referencia": "10242808",
    "mode": "live",
    "codigo_verificacion": "QP-7K3M9-XQ4TA",
    "propina": 0,
    "total": 150,
    "pagador": { "alta_cliente": false }
  }
}

monto es lo que pidió el cobro. Si activaste propinas y tu cliente agregó una, viene en propina y total es lo que se le debitó (monto + propina, en un solo débito). Sin propina, propina es 0 y total es igual a monto.

codigo_verificacion es el código que lleva el comprobante de tu cliente: sirve para verificar que es auténtico. Puede venir null; ignóralo en ese caso.

pagador.alta_cliente es true si quien pagó marcó en la página de pago "Quiero ser cliente de tu comercio y recibir sus novedades". Esa persona ya aparece en Clientes del portal; el aviso no trae su cédula, teléfono ni nombre. → Página de pago

mode dice si el aviso es de un cobro de producción (live) o de prueba (test). En link.acreditado viene igual.

metodo sólo viene cuando tu cliente pagó con tarjeta (piloto): "metodo": "tarjeta", recargo_tarjeta es el recargo por pago con tarjeta que pagó tu cliente (no se te liquida) y referencia es la del recibo de la tarjeta. monto, propina y total son los del cobro, sin el recargo. Con débito inmediato el aviso no trae metodo ni recargo_tarjeta.

Cuerpo de link.devuelto (piloto):

json
{
  "type": "link.devuelto",
  "sentAt": "2026-10-14T21:33:59.304Z",
  "data": {
    "link_id": "4e38874a-eb60-457b-ba53-371a74dbaff4",
    "devolucion_id": "786d2085-00d3-4f61-95c4-b7509cfb5276",
    "estado": "completada",
    "monto": 1,
    "monto_devuelto": 1,
    "moneda": "VES",
    "referencia_bancaria": "SIMULADO",
    "mode": "test"
  }
}

Con una llave de prueba referencia_bancaria llega como "SIMULADO"; en producción es la referencia del crédito a tu cliente.

monto es lo que se devolvió en esta devolución y monto_devuelto lo devuelto en total de ese cobro (una devolución puede ser parcial y un cobro puede tener varias). Llega una vez por devolución, cuando el dinero ya salió hacia tu cliente.

link.acreditado trae además liquidacion_txn_id, monto_bruto, comision y monto_acreditado. Con pago dividido (piloto) llega cuando todo el reparto terminó (cada parte pagada o cerrada sin pagar) y trae además reparto; monto_acreditado es sólo tu parte. Con propina, monto_bruto es el total (la comisión se calcula sobre lo debitado y la propina te llega con el resto del pago). Si devolviste parte de ese pago desde el portal antes de que te lo acreditáramos (piloto de devoluciones), monto_acreditado llega ya descontado: lo devuelto y el costo del banco de la devolución. En ese caso monto_acreditado es menor que monto_bruto − comision. Si devolviste todo lo que te tocaba, no llega link.acreditado. → Glosario

3. Verifica la firma (siempre)

Usa la firma v2 y rechaza avisos con más de 5 minutos: así nadie te reenvía uno viejo. Calcula sobre el cuerpo exacto que llegó, antes de parsearlo.

Node.js

js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verificar(cuerpo, firma, timestamp, secreto) {
  if (!firma || !timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const esperado = Buffer.from('v2=' + createHmac('sha256', secreto).update(`${timestamp}.${cuerpo}`).digest('hex'));
  const recibido = Buffer.from(String(firma));
  return esperado.length === recibido.length && timingSafeEqual(esperado, recibido);
}

// Express: app.post('/webhooks/quickpay', express.text({ type: '*/*' }), (req, res) => { … })
//   verificar(req.body, req.get('x-quicklink-signature-v2'), req.get('x-quicklink-timestamp'), process.env.QUICKPAY_WEBHOOK_SECRET)

El SDK trae esto listo: verificarWebhook({ cuerpo, firma, timestamp, secreto }).

PHP

php
$cuerpo = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_QUICKLINK_TIMESTAMP'] ?? '';
$firma = $_SERVER['HTTP_X_QUICKLINK_SIGNATURE_V2'] ?? '';
$esperado = 'v2=' . hash_hmac('sha256', $ts . '.' . $cuerpo, getenv('QUICKPAY_WEBHOOK_SECRET'));
if (!$ts || abs(time() - (int)$ts) > 300 || !hash_equals($esperado, $firma)) { http_response_code(401); exit; }

4. Responde rápido

Responde 200 en menos de 8 segundos y procesa después. Si no respondemos con 2xx, reintentamos hasta 6 veces: de inmediato, a los 30 segundos, 2 minutos, 5 minutos, 15 minutos y 1 hora.

5. No entregues dos veces

Un aviso puede llegar más de una vez (por los reintentos). Guarda qué link_id (en los avisos transaccion.*, data.id; en link.devuelto, devolucion_id) ya procesaste para cada evento y, si llega repetido, responde 200 sin volver a entregar.

6. Prueba y revisa las entregas

  • Probar: en el portal, Integraciones › Webhooks › Probar envía a tu URL un aviso webhook.test firmado igual que los reales (mismas cabeceras, firma v2) y te dice si respondió 2xx. Ese aviso no trae data: respóndele 200 sin procesarlo.
  • Eliminar: en el portal, Integraciones › Webhooks › Eliminar borra la URL aunque todavía no tengas llave de producción (por API, DELETE /switch/v1/webhooks/:id exige una sk_live_).
  • Historial: GET /switch/v1/webhooks/:id/deliveries te da las últimas entregas con su estado, el HTTP que respondió tu servidor y los intentos. Si en el momento de entregar tu URL resolvía a una red interna, la entrega queda fallido sin reintentos, con status: null y error: "destino_bloqueado"; en las demás, error viene null. Con una sk_test_ sólo salen las entregas de avisos de prueba (data.mode: "test"); las de producción, y los avisos transaccion.*, se ven con una sk_live_.

7. Administrar por API

MétodoRutaPara
GET/webhooksListar (sin el secreto). Sirve con cualquier llave
GET/webhooks/:idVer uno
PATCH/webhooks/:idCambiar url, eventos o estado (activo / inactivo). Sólo sk_live_
DELETE/webhooks/:idBorrar. Sólo sk_live_

Revisa data.mode antes de entregar. Llegan los avisos de prueba y los de producción al mismo webhook: si mode es test, el pago fue simulado y no debes entregar nada de verdad. → Modo prueba