Tema
Recibir webhooks
En vez de consultar cada pocos minutos, Redfloral te avisa por HTTP cuando algo cambia: pedidos (estado, foto de entrega), catálogo (productos, precios, estado, stock) y disponibilidad (bloqueos de día, alta demanda, comunas, horarios). La lista de eventos y sus cuerpos está en Eventos.
Registrar un webhook
Desde el panel, en Integraciones, o por la API:
bash
curl -X POST "$API/webhooks" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"url": "https://mitienda.cl/redfloral/webhook",
"events": ["order.status_changed", "order.photo_uploaded", "product.updated", "product.price_changed"]}'Devuelve el webhook con su secret (whsec_…), que necesitas para verificar la firma. Reglas:
- La URL debe ser https y pública: se rechazan hosts que resuelven a direcciones privadas, loopback o link-local, y URLs con usuario y contraseña. Se vuelve a validar en cada envío.
- Por la API, la key debe tener el scope del evento:
orders:readparaorder.*;catalog:readparaproduct.*yavailability.changed. Si no,403 insufficient_scope. - Comodines:
*,order.*,product.*,availability.*. Se expanden al registrar y no incluyenproduct.stock_changed, que cambia con cada venta y hay que pedirlo explícito. - Máximo 10 webhooks por florería.
Gestión: GET /webhooks, PATCH /webhooks/<id> (url, events, name, is_active), DELETE /webhooks/<id>, POST /webhooks/<id>/rotate-secret, POST /webhooks/<id>/test (manda un evento ping para probar tu URL y tu verificación) y GET /webhooks/deliveries?event=&status=&webhook_id= (últimos envíos, su resultado y errores; include_payload=true agrega el cuerpo). En el panel puedes ver las entregas y reenviar una.
Qué recibes
Un POST JSON a tu URL con estos headers:
| Header | Contenido |
|---|---|
X-Redfloral-Event | Nombre del evento (order.created, …) |
X-Redfloral-Delivery | Id único de la entrega. Úsalo para deduplicar |
X-Redfloral-Signature | t=<unix>,v1=<firma> |
Responde 2xx en menos de 10 segundos. Cualquier otra respuesta, incluidos los redirects 3xx (no se siguen), es un fallo y se reintenta. Si tu proceso es lento, guarda el evento, responde 200 y procésalo después.
Verificar la firma
v1 = hex( HMAC-SHA256( secret, "<t>.<cuerpo crudo>" ) )Calcula la firma sobre el cuerpo tal como llegó (bytes crudos, antes de parsear el JSON), compárala en tiempo constante y rechaza si t se aleja más de 5 minutos de tu reloj.
php
<?php
$secret = getenv('REDFLORAL_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_REDFLORAL_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts); // t=..., v1=...
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, $secret);
if (!isset($parts['v1'], $parts['t'])
|| !hash_equals($expected, $parts['v1'])
|| abs(time() - (int) $parts['t']) > 300) {
http_response_code(400);
exit;
}
$deliveryId = $_SERVER['HTTP_X_REDFLORAL_DELIVERY']; // deduplica por este id
$event = json_decode($body, true);
http_response_code(200);js
const crypto = require('crypto');
// Necesita el cuerpo crudo: express.raw, no express.json.
app.post('/redfloral/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Redfloral-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.REDFLORAL_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString('utf8')}`).digest('hex');
const ok = parts.v1 && expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)) &&
Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
if (!ok) return res.sendStatus(400);
const deliveryId = req.get('X-Redfloral-Delivery'); // deduplica por este id
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
});python
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["REDFLORAL_WEBHOOK_SECRET"].encode()
@app.post("/redfloral/webhook")
def redfloral_webhook():
body = request.get_data() # bytes crudos
parts = dict(p.split("=", 1) for p in request.headers.get("X-Redfloral-Signature", "").split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
if not v1 or not hmac.compare_digest(expected, v1) or abs(time.time() - int(t or 0)) > 300:
abort(400)
delivery_id = request.headers["X-Redfloral-Delivery"] # deduplica por este id
event = request.get_json()
return "", 200Si rotas el secreto (rotate-secret), el anterior deja de valer de inmediato: actualiza tu servidor enseguida.
Entrega, reintentos y reconciliación
- Al menos una vez y sin orden garantizado: un evento puede llegar más de una vez (deduplica por
X-Redfloral-Delivery) y dos eventos seguidos pueden llegar invertidos (comparaoccurred_atyupdated_at). - Reintentos: si tu servidor no responde 2xx, se reintenta a 1 min, 5 min, 30 min, 2 h y 6 h (6 intentos en total).
- Desactivación: tras 10 envíos fallidos consecutivos el webhook se desactiva (
is_active: false, condisabled_reason) y avisamos por email a la florería. Los eventos ocurridos mientras está desactivado no se reenvían. Reactívalo desde el panel o conPATCH /webhooks/<id> {"is_active": true}. - Reconcilia periódicamente: ningún webhook garantiza que no se pierda un evento. Al arrancar y, por ejemplo, cada hora, ponte al día con
GET /products?updated_since=<última sincronización>yGET /orders?updated_since=<última sincronización>. Los webhooks bajan la latencia; la consulta conupdated_sincees tu red de seguridad. - Agrupación de catálogo: los cambios de un mismo producto en una ventana de unos 45 segundos se envían una sola vez, con el estado final del producto y la lista
changesde todo lo que cambió. Los pedidos no se agrupan: cada cambio de estado es un evento. - Productos que revendes: recibes los cambios de datos, variantes, imágenes, estado y borrado del producto original, visto desde tu catálogo (tu precio y tu stock). Si la reventa usa el precio y el stock del original, también recibes sus cambios de precio, oferta y stock.
- Sin evento: un cambio hecho por una operación masiva interna, sin pasar por el guardado normal (raro). La reconciliación lo cubre.