Skip to content

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:read para order.*; catalog:read para product.* y availability.changed. Si no, 403 insufficient_scope.
  • Comodines: *, order.*, product.*, availability.*. Se expanden al registrar y no incluyen product.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:

HeaderContenido
X-Redfloral-EventNombre del evento (order.created, …)
X-Redfloral-DeliveryId único de la entrega. Úsalo para deduplicar
X-Redfloral-Signaturet=<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 "", 200

Si 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 (compara occurred_at y updated_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, con disabled_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 con PATCH /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> y GET /orders?updated_since=<última sincronización>. Los webhooks bajan la latencia; la consulta con updated_since es 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 changes de 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.

API de socios de Redfloral, versión 1 (beta).