Saltar al contenido

Desarrolladores

Webhooks

Eventos de reserva, formato del payload, verificación de firma, reintentos y desactivación.

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

  1. Ve a Ajustes → Integraciones (requiere la función integrations).
  2. 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).
  3. 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"
    }
  }
}
  • id identifica el evento y se mantiene igual en los reintentos y reenvíos. Úsalo para deduplicar.
  • data.booking es la misma representación que devuelve GET /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:

  1. Lee el cuerpo sin modificar (antes de decodificar el JSON).
  2. Recalcula el HMAC y compáralo con la cabecera en tiempo constante.
  3. 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 id de evento) firmado con un timestamp nuevo.

Buenas prácticas

  • La entrega es «al menos una vez» y sin garantía de orden. Deduplica por id y, si el orden importa, compara updated_at de la reserva o vuelve a consultarla con GET /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.