Els webhooks t'avisen per HTTP tan bon punt canvia una reserva, perquè no hagis de consultar l'API contínuament. Pranarp Club envia una petició POST amb un cos JSON signat a l'URL que configuris.
Esdeveniments
| Esdeveniment | Quan s'envia |
|---|---|
booking.created |
Es crea una reserva, des de l'API, el personal del club o un jugador. Les reserves en esborrany també es notifiquen, amb status: "draft"; en confirmar l'esborrany arriba un booking.updated. |
booking.updated |
Canvia un camp de la reserva que exposa l'API (hora, espai, notes, titular, preu…). Diverses edicions en una mateixa operació generen un únic esdeveniment; els canvis que només afecten camps no exposats no en generen cap. |
booking.cancelled |
Es cancel·la una reserva. S'emet una sola vegada, la primera que passa a canceled. |
Configurar un endpoint
- Ves a Configuració → Integracions (requereix la funció
integrations). - A Endpoints de webhooks, prem crear i indica l'URL, una descripció opcional, els esdeveniments als quals et subscrius i, si vols, els espais (sense cap s'envien els de tot el club).
- Copia el secret de signatura (
whsec_…). Es mostra una sola vegada.
L'URL ha de ser HTTPS, pública i sense usuari ni contrasenya. Es rebutgen les destinacions que resolen a adreces privades o locals (per exemple localhost o 10.x.x.x). Per desenvolupar en local, exposa el teu servidor amb un túnel HTTPS.
Format de la petició
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": "comanda-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 l'esdeveniment i es manté igual en els reintents i reenviaments. Fes-lo servir per deduplicar.data.bookingés la mateixa representació que retornaGET /bookings/{id}, en el seu estat final després del canvi.
Verificar la signatura
Cada petició porta dues capçaleres:
X-PranarpClub-Timestamp: segons Unix en el moment de l'enviament.X-PranarpClub-Signature:sha256=<hex>, l'HMAC-SHA256 de la cadena{timestamp}.{cos}(el timestamp, un punt i el cos cru de la petició) calculat amb el secret de l'endpoint (whsec_…).
Passos:
- Llegeix el cos sense modificar (abans de decodificar el JSON).
- Recalcula l'HMAC i compara'l amb la capçalera en temps constant.
- Rebutja la petició si el timestamp s'allunya més de 5 minuts del teu rellotge (protecció contra repeticions).
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);
// ... posa el processament en cua i respon de seguida
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);
}
// Important: el cos cru, no el JSON ja parsejat.
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);
// ... posa el processament en cua i respon de seguida
res.sendStatus(204);
});
app.listen(3000);
Per provar la teva verificació, pots generar una signatura a mà:
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"
Lliurament, reintents i desactivació
- Respon amb qualsevol estat 2xx en menys de 10 segons. Fes la feina pesada després, en una cua.
- No se segueixen redireccions (un 3xx compta com a fallada).
- Si respons amb un altre estat, o hi ha un error de xarxa o de temps d'espera, Pranarp Club reintenta fins a 8 vegades més amb espera exponencial: 15 s, 30 s, 1 min, 2 min… fins a uns 32 min (9 intents en total).
- Després de 20 lliuraments consecutius que esgotin els reintents, l'endpoint es desactiva i l'has de reactivar des del tauler. Qualsevol lliurament correcte reinicia el compte.
- Els lliuraments es conserven 30 dies. Des del tauler en pots veure l'historial (estat, intents, resposta) i reenviar-los; un reenviament porta el payload original (mateix
idd'esdeveniment) signat amb un timestamp nou.
Bones pràctiques
- El lliurament és «almenys una vegada» i sense garantia d'ordre. Deduplica per
idi, si l'ordre importa, compara l'updated_atde la reserva o torna-la a consultar ambGET /bookings/{id}. - Verifica sempre la signatura i el timestamp abans de processar res.
- Respon ràpid i processa de manera asíncrona.
- Si el teu endpoint es desactiva, corregeix-ne la causa abans de reactivar-lo i, si cal, reenvia els lliuraments perduts.