Webhooks
El negocio configura en su panel una URL https y genera un secreto de firma (se muestra una
sola vez). papedir hace POST a esa URL con cada evento:
{"id":"8c1e…","type":"order.created","created_at":"2026-10-08T15:00:00Z", "data":{"order_id":"…","order_num":128,"status":"pending","payment_status":"pending", "payment_type":"cash","is_paid":false,"order_type":"takeout","total":6500,"currency":"COP", "created_at":"2026-10-08T15:00:00Z", "items":[{"menu_item_id":43,"sku":"PAN-01","external_id":"b3f1…","name":"Pan de yuca", "quantity":2,"unit_price":2000,"line_total":4000}]}}type | Cuándo | Qué hacer |
|---|---|---|
order.created | Se creó el pedido (papedir ya descontó la existencia) | Registra la venta, responde 2xx y manda la existencia |
order.updated | Cambió el estado o el pago | Opcional |
order.cancelled | Se canceló (papedir devolvió la existencia) | Revierte la venta y manda la existencia |
integration.ping | Prueba desde el panel | Responde 2xx |
Nunca traen datos personales del cliente.
Cabeceras
Sección titulada «Cabeceras»| Cabecera | Valor |
|---|---|
X-Papedir-Event | El type |
X-Papedir-Event-Id | El id (deduplica por este) |
X-Papedir-Timestamp | Segundos Unix |
X-Papedir-Signature | sha256=<hex(HMAC-SHA256(secreto, "<timestamp>.<cuerpo crudo>"))> |
Verificar la firma
Sección titulada «Verificar la firma»Calcula el HMAC sobre el cuerpo crudo (sin re-serializar el JSON), compáralo en tiempo constante y rechaza timestamps de más de 5 minutos.
const crypto = require("node:crypto");
function verificar(headers, cuerpoCrudo, secreto) { const ts = Number(headers["x-papedir-timestamp"]); if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; const esperado = "sha256=" + crypto.createHmac("sha256", secreto).update(`${ts}.${cuerpoCrudo}`).digest("hex"); const a = Buffer.from(esperado); const b = Buffer.from(String(headers["x-papedir-signature"] ?? "")); return a.length === b.length && crypto.timingSafeEqual(a, b);}import hashlib, hmac, time
def verificar(headers, cuerpo_crudo: bytes, secreto: str) -> bool: ts = int(headers.get("X-Papedir-Timestamp", "0")) if abs(time.time() - ts) > 300: return False esperado = "sha256=" + hmac.new( secreto.encode(), f"{ts}.".encode() + cuerpo_crudo, hashlib.sha256 ).hexdigest() return hmac.compare_digest(esperado, headers.get("X-Papedir-Signature", ""))function verificar(array $headers, string $cuerpoCrudo, string $secreto): bool { $ts = (int) ($headers['X-Papedir-Timestamp'] ?? 0); if (abs(time() - $ts) > 300) return false; $esperado = 'sha256=' . hash_hmac('sha256', $ts . '.' . $cuerpoCrudo, $secreto); return hash_equals($esperado, $headers['X-Papedir-Signature'] ?? '');}Responder y reintentos
Sección titulada «Responder y reintentos»- Verifica la firma (si falla, responde 401).
- Deduplica por
id: el mismo evento puede llegar más de una vez. - Responde 2xx en menos de 10 s y procesa después. El 2xx confirma el evento.
Cualquier otro código, un timeout o un error de red se reintenta: a los 30 s, 2 min, 10 min, 1 h y
luego cada 6 h, hasta 8 intentos. Después el evento queda dead y el negocio puede reenviarlo
desde su panel. Entre pedidos distintos pueden llegar desordenados: usa created_at. papedir no
sigue redirecciones y nunca llama a direcciones privadas.
Sin webhook: el feed
Sección titulada «Sin webhook: el feed»Si no puedes exponer una URL, lee GET /orders?after=<cursor> periódicamente y confirma lo que
procesaste con POST /orders/ack ({"event_ids": [...]}). Mientras un order.created no esté
confirmado, papedir reserva esa venta al aplicar tu existencia (hasta 48 h). Detalle en la
guía de inventario externo.