Idempotencia
El mismo evento de Meta PUEDE llegar más de una vez a tu webhook (reintentos de red, lease vencido, recovery post-crash del worker). Tu receiver DEBE ser idempotente.
Dos estrategias de dedup
Section titled “Dos estrategias de dedup”Tenés dos identificadores estables para hacer dedup. Cualquiera de los dos funciona; elegí el que mejor encaje con tu modelo de datos.
1. X-Kalipto-Delivery-Id (recomendado)
Section titled “1. X-Kalipto-Delivery-Id (recomendado)”Es el id del row platform_deliveries.id en nuestro lado. Es idéntico
en cada reintento del mismo delivery: si lo viste antes, es duplicado.
seen_ids = redis.Set("kalipto_delivery_ids", ttl_seconds=86400 * 7) # 7d
@app.post("/webhook")async def webhook(request: Request): raw = await request.body() verify_signature(...) delivery_id = request.headers["X-Kalipto-Delivery-Id"] if not seen_ids.add(delivery_id): # add returns False if already present return {"ok": True, "duplicate": True} # … process payload return {"ok": True}Un TTL de 7 días es generoso. El
MAX_ATTEMPTS=5+BACKOFF=[1,5,30, 120,360]minutos cierra el ciclo de reintentos en menos de 9 horas.
2. Message id nativo de Meta
Section titled “2. Message id nativo de Meta”Adentro del payload tenés el id de Meta:
- WhatsApp:
entry[].changes[].value.messages[].id(wamid.…). - Instagram:
entry[].messaging[].message.mid.
Esto es útil si querés deduplicar a nivel mensaje (no a nivel “intento de
delivery”). Tené en cuenta que status callbacks no tienen un message id
único: ahí necesitás X-Kalipto-Delivery-Id.
def extract_message_ids(payload: dict) -> list[str]: ids = [] for entry in payload.get("entry", []): # WhatsApp for change in entry.get("changes", []): value = change.get("value", {}) for msg in value.get("messages", []) or []: ids.append(msg["id"]) # Instagram for msg_event in entry.get("messaging", []) or []: mid = (msg_event.get("message") or {}).get("mid") if mid: ids.append(mid) return ids¿Y si fallo procesando?
Section titled “¿Y si fallo procesando?”Si tu receiver levanta una excepción y devolvés 5xx, vamos a reintentar. Aprovechalo: el dedup-set solo cuenta los procesados exitosamente (200 OK desde tu lado), así un retry post-error sí re-procesa.
@app.post("/webhook")async def webhook(request: Request): raw = await request.body() verify_signature(...) delivery_id = request.headers["X-Kalipto-Delivery-Id"] if seen_ids.contains(delivery_id): return {"ok": True, "duplicate": True} try: process_payload(raw) seen_ids.add(delivery_id) # mark seen DESPUÉS del éxito except Exception: # Levantá 500 — Kalipto va a reintentar raise HTTPException(status_code=500, detail="processing failed") return {"ok": True}Próximo paso
Section titled “Próximo paso”- Rate limits: cómo funciona la ventana deslizante por key y qué hacer cuando recibís 429.