Webhooks
Los webhooks notifican a tu aplicación en tiempo real cuando ocurren eventos (por ejemplo, se crea o actualiza una reserva). Tú registras una URL; CitaPro envía una petición POST a esa URL con el payload del evento.
Para el formato del payload, lista de eventos y campos rastreables por evento, consulta la Referencia API de Webhooks. Esta guía se centra en recibir y verificar webhooks.
Recibir webhooks
Sección titulada «Recibir webhooks»Usar el body raw
Sección titulada «Usar el body raw»La verificación de la firma usa el cuerpo raw exacto de la petición. Si tu framework parsea el body (p. ej. como JSON) antes de que se ejecute tu handler, la cadena usada para verificar puede ser distinta (codificación, orden de claves, espacios) y la verificación fallará.
- Sí lee el body como string o buffer raw y pásalo a tu función HMAC.
- No uses el body ya parseado como JSON para recalcular la firma.
Responder 2xx rápido
Sección titulada «Responder 2xx rápido»Responde con un estado 2xx antes de hacer trabajo pesado. Si tu endpoint tarda o devuelve error, CitaPro reintentará (ver Reintentos). Confirma la recepción primero y luego procesa el evento de forma asíncrona si hace falta.
Idempotencia
Sección titulada «Idempotencia»Usa el header X-CitaPro-Delivery-Id como identificador único de la entrega. Guarda los IDs ya procesados e ignora duplicados para no aplicar el mismo evento dos veces si llega un reintento.
Verificar la firma
Sección titulada «Verificar la firma»CitaPro firma cada petición con HMAC-SHA256 usando el secret de tu webhook. La firma se envía en el header X-CitaPro-Signature. Debes verificarla para asegurarte de que la petición vino de CitaPro y no fue alterada.
Pasos:
- Leer el body raw de la petición (sin modificar).
- Calcular HMAC-SHA256(rawBody, secret). Usa la misma codificación que el header (hex).
- Comparar el resultado con
X-CitaPro-Signatureusando una comparación en tiempo constante para evitar ataques por tiempo.
El secret solo se devuelve al crear el webhook. Guárdalo de forma segura (p. ej. variable de entorno).
Node.js (Express)
Sección titulada «Node.js (Express)»Usa express.raw() para la ruta del webhook y deja el body sin parsear. Si usas express.json() global, registra la ruta del webhook antes o usa middleware solo para esa ruta.
const express = require('express');const crypto = require('crypto');
const app = express();const WEBHOOK_SECRET = process.env.CITAPRO_WEBHOOK_SECRET;
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body; const signature = req.headers['x-citapro-signature'];
if (!signature || !WEBHOOK_SECRET) { return res.status(401).send('Missing signature or secret'); }
const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(rawBody) .digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'))) { return res.status(401).send('Invalid signature'); }
const payload = JSON.parse(rawBody.toString()); const event = req.headers['x-citapro-event']; const deliveryId = req.headers['x-citapro-delivery-id'];
res.status(200).send('OK');});
app.use(express.json());Usa el stream de entrada raw. No uses $_POST ni JSON ya parseado para la firma.
<?php$webhookSecret = getenv('CITAPRO_WEBHOOK_SECRET');$rawBody = file_get_contents('php://input');$signature = $_SERVER['HTTP_X_CITAPRO_SIGNATURE'] ?? '';
if (empty($signature) || empty($webhookSecret)) { http_response_code(401); exit('Missing signature or secret');}
$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expected, $signature)) { http_response_code(401); exit('Invalid signature');}
$payload = json_decode($rawBody, true);// Procesar $payload['event'] y $payload['data'] ...http_response_code(200);echo 'OK';Python (Flask)
Sección titulada «Python (Flask)»Usa request.get_data() para obtener los bytes raw. No uses request.get_json() para verificar.
import hmacimport hashlibimport osfrom flask import Flask, request
app = Flask(__name__)WEBHOOK_SECRET = os.environ.get('CITAPRO_WEBHOOK_SECRET', '').encode('utf-8')
@app.route('/webhook', methods=['POST'])def webhook(): raw_body = request.get_data() signature = request.headers.get('X-CitaPro-Signature', '')
if not signature or not WEBHOOK_SECRET: return 'Missing signature or secret', 401
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature): return 'Invalid signature', 401
payload = request.get_json(force=True, silent=True) or {} # Procesar payload['event'] y payload['data'] ... return 'OK', 200Ruby (Rack / Sinatra)
Sección titulada «Ruby (Rack / Sinatra)»Lee el body con request.body.read.
require 'openssl'require 'json'
WEBHOOK_SECRET = ENV['CITAPRO_WEBHOOK_SECRET']
post '/webhook' do raw_body = request.body.read signature = request.env['HTTP_X_CITAPRO_SIGNATURE'].to_s
if signature.empty? || WEBHOOK_SECRET.to_s.empty? status 401 return 'Missing signature or secret' end
expected = OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, raw_body)
unless Rack::Utils.secure_compare(expected, signature) status 401 return 'Invalid signature' end
payload = JSON.parse(raw_body) status 200 'OK'endLee el body raw con io.ReadAll y usa hmac.Equal para comparar.
rawBody, _ := io.ReadAll(r.Body)signature := r.Header.Get("X-CitaPro-Signature")
mac := hmac.New(sha256.New, webhookSecret)mac.Write(rawBody)expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return}Opcional: tolerancia del timestamp
Sección titulada «Opcional: tolerancia del timestamp»Para mitigar ataques de replay puedes rechazar peticiones demasiado viejas. Usa el header X-CitaPro-Timestamp (Unix en segundos). Por ejemplo, rechaza si abs(ahora - timestamp) > 300 (5 minutos).
Reintentos
Sección titulada «Reintentos»Si tu endpoint no responde con un estado 2xx, CitaPro reintenta hasta 3 veces con backoff progresivo: 10s, 60s, 5min. Usa X-CitaPro-Delivery-Id para idempotencia y no procesar la misma entrega más de una vez.
Resumen
Sección titulada «Resumen»| Paso | Acción |
|---|---|
| 1 | Leer el body raw de la petición (no parsear antes de verificar). |
| 2 | Calcular HMAC-SHA256(rawBody, secret) (hex). |
| 3 | Comparar con X-CitaPro-Signature en tiempo constante. |
| 4 | Responder 2xx rápido; procesar el evento de forma asíncrona si hace falta. |
| 5 | Usar X-CitaPro-Delivery-Id para deduplicar reintentos. |
Para estructura del payload, tipos de evento y campos rastreables, consulta la Referencia API de Webhooks.