Skip to content

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.

Tenés dos identificadores estables para hacer dedup. Cualquiera de los dos funciona; elegí el que mejor encaje con tu modelo de datos.

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.

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

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}
  • Rate limits: cómo funciona la ventana deslizante por key y qué hacer cuando recibís 429.