Webhooks
Integrá ForkCast con tu sistema de facturación, ERP o cualquier servicio externo.
En esta página
Descripción general
ForkCast dispara webhooks HTTP POST cuando ocurren eventos en la plataforma. Los webhooks permiten integrar sistemas externos — facturación, ERP, notificaciones, analytics — sin modificar ForkCast.
Configuración
Los webhooks se configuran por usuario (restaurante o proveedor) desde Configuración o via API:
POST /api/webhooks
{
"url": "https://tu-sistema.com/webhook",
"events": ["order.created", "order.status_updated"]
}
Seguridad: El secret se genera automáticamente (64 caracteres hex) y se retorna en el response de creación. Guardalo de forma segura — no se puede recuperar después. La URL debe usar HTTPS (excepto localhost en desarrollo).
Verificación de firma
Cada request incluye dos headers de seguridad:
X-Webhook-Signature | HMAC-SHA256 del payload firmado con el secret |
X-Webhook-Timestamp | Unix timestamp (segundos) del momento del dispatch |
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Timestamp: 1743456789
La firma se calcula sobre "${timestamp}.${body}" (timestamp + punto + body JSON) para prevenir replay attacks.
Para verificar en Node.js:
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const timestamp = req.headers['x-webhook-timestamp'];
const receivedSig = req.headers['x-webhook-signature'];
// Rechazar si el timestamp tiene más de 5 minutos
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
throw new Error('Timestamp demasiado antiguo (posible replay attack)');
}
// Verificar firma con timingSafeEqual
const body = JSON.stringify(req.body);
const signaturePayload = `${timestamp}.${body}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signaturePayload)
.digest('hex');
const a = Buffer.from(receivedSig);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error('Firma inválida');
}
}
Importante: Usá siempre crypto.timingSafeEqual() para comparar firmas, no ===. La comparación directa es vulnerable a timing attacks.
Eventos disponibles
| Evento | Descripción |
|---|---|
order.created |
Pedido creado |
order.status_updated |
Estado actualizado |
Payload
Ambos eventos envían el mismo formato:
{
"event": "order.status_updated",
"data": {
"orderId": 123,
"restaurantId": 1,
"supplierId": 5,
"priceListId": 4,
"status": "delivered",
"date": "2026-04-01T00:00:00.000Z",
"items": [
{
"name": "GIN BOSQUE NATIVO 500ML",
"quantity": 10,
"unit": "un",
"price": 4407.81
}
],
"isBillable": true,
"client": {
"forkcastId": 1,
"name": "CAMINANTES S.R.L",
"externalMappings": [
{
"provider": "contabilium",
"externalId": "91003015",
"externalName": "CAMINANTES S.R.L"
}
]
}
}
}
Campos del payload
data
| Campo | Tipo | Descripción |
|---|---|---|
orderId | number | ID del pedido en ForkCast |
restaurantId | number | ID del restaurante |
supplierId | number | null | ID del proveedor |
priceListId | number | null | ID de la lista de precios |
status | string | pending, approved, rejected, delivered |
date | ISO 8601 | Fecha del pedido |
items | array | Productos del pedido |
isBillable | boolean | Si la lista es facturable |
client | object | Datos del cliente |
items[]
| Campo | Tipo | Descripción |
|---|---|---|
name | string | Nombre del producto |
quantity | number | Cantidad pedida |
unit | string | Unidad (kg, un, l, etc.) |
price | number | Precio unitario |
client
| Campo | Tipo | Descripción |
|---|---|---|
forkcastId | number | ID del restaurante en ForkCast |
name | string | Nombre del restaurante |
externalMappings | array | Mappings con sistemas externos |
externalMappings[]
| Campo | Tipo | Descripción |
|---|---|---|
provider | string | Sistema externo (contabilium, xubio, etc.) |
externalId | string | ID del cliente en el sistema externo |
externalName | string | null | Razón social en el sistema externo |
Estados del pedido
Caso de uso: Facturación automática
Cuando un pedido se marca como entregado y la lista de precios es facturable:
- 1Recibir webhook
order.status_updatedconstatus: "delivered" - 2Verificar
isBillable: true - 3Usar
client.externalMappingspara encontrar el ID del cliente en tu ERP - 4Crear el comprobante en el ERP con los
itemsdel pedido
Ejemplos de código
Node.js (Express)
const crypto = require('crypto');
const WEBHOOK_SECRET = process.env.FORKCAST_WEBHOOK_SECRET;
app.post('/webhook/forkcast', (req, res) => {
// 1. Verificar firma y timestamp
const timestamp = req.headers['x-webhook-timestamp'];
const sig = req.headers['x-webhook-signature'];
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return res.status(403).json({ error: 'Timestamp expired' });
}
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${JSON.stringify(req.body)}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(403).json({ error: 'Invalid signature' });
}
// 2. Procesar evento
const { event, data } = req.body;
if (event === 'order.status_updated'
&& data.status === 'delivered'
&& data.isBillable) {
const mapping = data.client.externalMappings
.find(m => m.provider === 'contabilium');
if (mapping) {
crearComprobante({
clienteId: mapping.externalId,
items: data.items.map(i => ({
concepto: i.name,
cantidad: i.quantity,
precioUnitario: i.price,
})),
});
}
}
res.status(200).json({ ok: true });
});
Python (FastAPI)
import hmac, hashlib, time, os
WEBHOOK_SECRET = os.environ["FORKCAST_WEBHOOK_SECRET"]
@app.post("/webhook/forkcast")
async def webhook(request: Request):
raw_body = await request.body()
timestamp = request.headers.get("x-webhook-timestamp", "")
received_sig = request.headers.get("x-webhook-signature", "")
# Verificar timestamp (max 5 min)
if abs(time.time() - int(timestamp)) > 300:
return {"error": "Timestamp expired"}, 403
# Verificar firma
signature_payload = f"{timestamp}.{raw_body.decode()}"
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), signature_payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(received_sig, expected):
return {"error": "Invalid signature"}, 403
# Procesar evento
body = await request.json()
event = body.get("event")
data = body.get("data", {})
if (event == "order.status_updated"
and data.get("status") == "delivered"
and data.get("isBillable")):
mappings = data["client"]["externalMappings"]
contabilium = next(
(m for m in mappings if m["provider"] == "contabilium"),
None
)
if contabilium:
crear_comprobante(
cliente_id=contabilium["externalId"],
items=[{
"concepto": i["name"],
"cantidad": i["quantity"],
"precio_unitario": i["price"],
} for i in data["items"]]
)
return {"ok": True}
Retry policy
Los webhooks se envían una vez (fire-and-forget). Si tu servidor no responde o devuelve error, el webhook no se reintenta.
Recomendamos implementar un mecanismo de reconciliación consultando GET /api/purchase-orders periódicamente para detectar pedidos que no fueron procesados.