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
| Evento | Cuándo llega |
|---|---|
link.pagado | El banco confirmó el pago de un cobro: ya puedes entregar |
link.acreditado | Te transferimos el neto de ese pago a tu cuenta. Llega con la liquidación inmediata, que se está activando por etapas |
link.devuelto | Una 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.aprobada | API directa: el banco confirmó la operación (LIQUIDADA). Llega una sola vez por operación, junto con transaccion.liquidada |
transaccion.liquidada | API directa: la operación quedó LIQUIDADA (confirmada por el banco) |
transaccion.rechazada | API 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_:
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.xo sus equivalentes IPv6— el registro responde422con{ "error": "destino_bloqueado" }. Lo mismo al cambiar la URL conPATCH. - No seguimos redirecciones. Si tu servidor responde
301,302,307o308, el aviso cuenta como no entregado y se reintenta: registra la URL final. - Hasta 10 webhooks por comercio. Con 10, registrar otro responde
409con{ "error": "limite_webhooks" }: borra uno que no uses.
2. Lo que te llega
Cabeceras:
| Cabecera | Qué es |
|---|---|
x-quicklink-event | El evento, p. ej. link.pagado |
x-quicklink-timestamp | Segundos desde 1970 (UTC) en que se firmó |
x-quicklink-signature-v2 | v2=<hex>: HMAC-SHA256 de timestamp + "." + cuerpo |
x-quicklink-signature | sha256=<hex>: HMAC-SHA256 del cuerpo (firma anterior, se mantiene por compatibilidad) |
Cuerpo de link.pagado:
{
"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):
{
"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
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
$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.testfirmado igual que los reales (mismas cabeceras, firma v2) y te dice si respondió2xx. Ese aviso no traedata: respóndele200sin 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/:idexige unask_live_). - Historial:
GET /switch/v1/webhooks/:id/deliverieste 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 quedafallidosin reintentos, constatus: nullyerror: "destino_bloqueado"; en las demás,errorvienenull. Con unask_test_sólo salen las entregas de avisos de prueba (data.mode: "test"); las de producción, y los avisostransaccion.*, se ven con unask_live_.
7. Administrar por API
| Método | Ruta | Para |
|---|---|---|
| GET | /webhooks | Listar (sin el secreto). Sirve con cualquier llave |
| GET | /webhooks/:id | Ver uno |
| PATCH | /webhooks/:id | Cambiar url, eventos o estado (activo / inactivo). Sólo sk_live_ |
| DELETE | /webhooks/:id | Borrar. Sólo sk_live_ |
Revisa
data.modeantes de entregar. Llegan los avisos de prueba y los de producción al mismo webhook: simodeestest, el pago fue simulado y no debes entregar nada de verdad. → Modo prueba