Los webhooks te avisan por HTTP en cuanto cambia una reserva, para que no tengas que consultar la API continuamente. Pranarp Club envía una petición POST con un cuerpo JSON firmado a la URL que configures.
Eventos
| Evento | Cuándo se envía |
|---|---|
booking.created |
Se crea una reserva, desde la API, el personal del club o un jugador. Las reservas en borrador también se notifican, con status: "draft"; al confirmar el borrador llega un booking.updated. |
booking.updated |
Cambia un campo de la reserva que expone la API (hora, espacio, notas, titular, precio…). Varias ediciones en una misma operación generan un único evento; los cambios que solo afectan a campos no expuestos no generan ninguno. |
booking.cancelled |
Se cancela una reserva. Se emite una sola vez, la primera que pasa a canceled. |
Configurar un endpoint
- Ve a Ajustes → Integraciones (requiere la función
integrations). - En Endpoints de webhooks, pulsa crear e indica la URL, una descripción opcional, los eventos a los que te suscribes y, si quieres, los espacios (sin ninguno se envían los de todo el club).
- Copia el secreto de firma (
whsec_…). Se muestra una sola vez.
La URL debe ser HTTPS, pública y sin usuario ni contraseña. Se rechazan los destinos que resuelven a direcciones privadas o locales (por ejemplo localhost o 10.x.x.x). Para desarrollar en local, expón tu servidor con un túnel HTTPS.
Formato de la petición
POST /webhooks/pranarp HTTP/1.1
Content-Type: application/json
User-Agent: PranarpClub-Webhooks/1
X-PranarpClub-Timestamp: 1792692000
X-PranarpClub-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
{
"id": "evt_01j9z3k8m2x4v6b7n8p9q0r1s2",
"type": "booking.created",
"created_at": "2026-10-22T17:40:12+02:00",
"data": {
"booking": {
"id": 1234,
"space_id": 7,
"starts_at": "2026-10-22T18:00:00+02:00",
"ends_at": "2026-10-22T19:00:00+02:00",
"status": "confirmed",
"source": "api",
"external_ref": "pedido-9981",
"notes": null,
"headcount": null,
"holder": { "name": "Laura Pons", "email": null, "phone": null },
"players": [],
"price": { "amount_cents": 2400, "currency": "EUR", "paid_externally": true },
"canceled_at": null,
"created_at": "2026-10-22T17:40:12+02:00",
"updated_at": "2026-10-22T17:40:12+02:00"
}
}
}
ididentifica el evento y se mantiene igual en los reintentos y reenvíos. Úsalo para deduplicar.data.bookinges la misma representación que devuelveGET /bookings/{id}, en su estado final tras el cambio.
Verificar la firma
Cada petición lleva dos cabeceras:
X-PranarpClub-Timestamp: segundos Unix en el momento del envío.X-PranarpClub-Signature:sha256=<hex>, el HMAC-SHA256 de la cadena{timestamp}.{cuerpo}(el timestamp, un punto y el cuerpo crudo de la petición) calculado con el secreto del endpoint (whsec_…).
Pasos:
- Lee el cuerpo sin modificar (antes de decodificar el JSON).
- Recalcula el HMAC y compáralo con la cabecera en tiempo constante.
- Rechaza la petición si el timestamp se aleja más de 5 minutos de tu reloj (protección frente a repeticiones).
PHP
<?php
function verifyPranarpSignature(string $rawBody, string $secret, int $tolerance = 300): bool
{
$timestamp = $_SERVER['HTTP_X_PRANARPCLUB_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PRANARPCLUB_SIGNATURE'] ?? '';
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > $tolerance) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
if (!verifyPranarpSignature($rawBody, getenv('PRANARP_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
// ... encola el procesamiento y responde enseguida
http_response_code(204);
JavaScript (Node.js)
const crypto = require('node:crypto');
const express = require('express');
const app = express();
const SECRET = process.env.PRANARP_WEBHOOK_SECRET;
function verify(rawBody, timestamp, signature, tolerance = 300) {
if (!/^\d+$/.test(timestamp ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > tolerance) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Importante: el cuerpo crudo, no el JSON ya parseado.
app.post('/webhooks/pranarp', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
const ok = verify(raw, req.get('X-PranarpClub-Timestamp'), req.get('X-PranarpClub-Signature'));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(raw);
// ... encola el procesamiento y responde enseguida
res.sendStatus(204);
});
app.listen(3000);
Para probar tu verificación, puedes generar una firma a mano:
BODY='{"id":"evt_test"}'
TS=$(date +%s)
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')"
echo "$TS $SIG"
Entrega, reintentos y desactivación
- Responde con cualquier estado 2xx en menos de 10 segundos. Haz el trabajo pesado después, en una cola.
- No se siguen redirecciones (3xx cuenta como fallo).
- Si respondes con otro estado, o hay un error de red o de tiempo de espera, Pranarp Club reintenta hasta 8 veces más con espera exponencial: 15 s, 30 s, 1 min, 2 min… hasta unos 32 min (9 intentos en total).
- Tras 20 entregas consecutivas que agoten sus reintentos, el endpoint se desactiva y debes reactivarlo desde el panel. Cualquier entrega correcta reinicia la cuenta.
- Las entregas se conservan 30 días. Desde el panel puedes ver su historial (estado, intentos, respuesta) y reenviarlas; un reenvío lleva el payload original (mismo
idde evento) firmado con un timestamp nuevo.
Buenas prácticas
- La entrega es «al menos una vez» y sin garantía de orden. Deduplica por
idy, si el orden importa, comparaupdated_atde la reserva o vuelve a consultarla conGET /bookings/{id}. - Verifica siempre la firma y el timestamp antes de procesar nada.
- Responde rápido y procesa de forma asíncrona.
- Si tu endpoint se desactiva, corrige la causa antes de reactivarlo y, si hace falta, reenvía las entregas perdidas.