Webhooks de verificación duplicados: deduplique antes de conceder nada
Una pregunta pública plantea cómo evitar que una aplicación Laravel procese dos veces el mismo evento de webhook. No vemos esa aplicación, no representamos a su autor y no afirmamos arreglar ninguna plataforma. Sin embargo, el problema análogo en la incorporación con verificación de identidad es concreto. Un proveedor reintenta un callback, su manejador se ejecuta dos veces y un permiso se concede dos veces, o una corrección posterior queda sobrescrita. Su decisión es dónde reside la garantía de deduplicación: en una cola, en una caché o en la misma transacción de base de datos que cambia el estado de verificación. Este artículo usa el fixture local de Python verificado para mostrar una respuesta y sus límites. El fixture no es código Laravel, y su indicador de permiso no es un permiso de negocio real.
curl https://gridzen.ai/console/api/quickstart/trial \
-H 'Content-Type: application/json' \
-d '{"task": "identity_onboarding", "scenario": "success"}'El problema y la decisión
Los reintentos son un comportamiento normal de los webhooks. Un emisor que agota el tiempo de espera, o que recibe una respuesta inesperada, puede volver a entregar el mismo evento. Si su manejador registra el evento en un paso y cambia el estado en otro, dos entregas concurrentes pueden superar ambas la comprobación. La decisión para su aplicación es si la protección contra duplicados se aplica de forma atómica, junto con el cambio de estado, o solo como mejor esfuerzo. El resto del artículo defiende la versión atómica y la prueba localmente.
Qué implementa realmente el fixture
El ejemplo descargable verification_delivery guarda sesiones y eventos en SQLite. La tabla events tiene clave primaria (session, event), de modo que el mismo ID de evento solo puede registrarse una vez por sesión. El manejador de eventos abre una transacción BEGIN IMMEDIATE. Comprueba que tenant y subject coinciden con la sesión y devuelve BINDING_MISMATCH si no coinciden. Si el ID de evento ya existe, devuelve el estado actual sin cambios. En caso contrario, registra el evento y actualiza el estado en la misma transacción. El manejador solo acepta entradas sintéticas. Los IDs de evento deben empezar por fixture-, mode debe ser synthetic_fixture y source debe ser fixture_callback. Cualquier otra entrada provoca PRODUCTION_SIGNATURE_VERIFIER_NOT_IMPLEMENTED. No se verifica ninguna firma real.
Escenario hipotético y salidas esperadas
Hipotéticamente, un flujo de incorporación de vendedores crea fixture-session con deadline 60. Después llega el evento fixture-approved con estado Approved y policy=True explícito. La salida esperada de aprobación es: status approved, deadline 60, permission_granted true, grant_count 1, live_permission_changed false, policy_version 1, allow_action true. A continuación se entrega de nuevo el mismo evento. La salida del duplicado es idéntica y grant_count se mantiene en 1. Luego llega un evento Declined distinto. La salida de la corrección es: status declined, permission_granted false, grant_count 1. El indicador local se revoca y el contador conserva el historial. En otra ejecución sin callback, recover en el instante 61 devuelve: status expired, permission_granted false, grant_count 0, next review_or_authorized_provider_refresh, provider_polled false.
Manejo de fallos que sigue siendo suyo
Los estados desconocidos pasan a unknown. Nunca se tratan como aprobados. Una vez que una sesión está approved, declined o expired, el único cambio que acepta es Approved seguido de Declined; cualquier otro evento la deja sin cambios. Un evento para una sesión inexistente devuelve BINDING_MISMATCH. Una lectura de una sesión inexistente devuelve SESSION_NOT_FOUND. Recover hace expirar la sesión localmente y no consulta a ningún proveedor. En un diseño Laravel propuesto, usted elegiría sus propios equivalentes. Una opción es una restricción única sobre el ID de evento del proveedor más la sesión, escrita en la misma transacción de base de datos que la actualización de estado. También necesitaría validación de firma específica del proveedor antes de esa escritura, un acuse de recibo rápido y una reconciliación de estado acotada a través de una ruta autorizada. El fixture no demuestra nada de esto, y nada de ello es comportamiento Laravel probado.
Diseño propuesto frente a comportamiento implementado
Lo que implementa el fixture: eventos sintéticos idempotentes, comprobaciones de vinculación, una única concesión local, revocación ante un Declined posterior y expiración local. Lo que sigue siendo propuesta: comprobaciones de firma reales, semántica de colas, reintentos contra un proveedor y la correspondencia entre una concesión local y permisos reales de vendedor. Approved no es un permiso incondicional. Un valor sintético de permission_granted es un registro del fixture, no un permiso de negocio real.
Lista de aceptación
Antes de adaptar este patrón, confirme cada uno de estos puntos. 1. Un evento repetido devuelve el estado sin cambios con grant_count 1. 2. La deduplicación y el cambio de estado se confirman en una sola transacción. 3. Se rechaza una discrepancia de tenant o subject. 4. Un Declined posterior elimina la concesión. 5. Los estados desconocidos nunca conceden nada. 6. Un plazo vencido produce expired con provider_polled false. 7. Reiniciar con la misma base de datos SQLite conserva el estado. 8. La verificación de firmas reales figura como requisito pendiente. Ejecute primero los comandos unittest de la guía. Después decida si una solicitud de sandbox encaja con su tarea.
Ejecutar el ejemplo sintético comprobado
Descargue y extraiga https://gridzen.ai/downloads/verification-delivery.zip, entre en verification_delivery y ejecute con Python 3.10+. El ejemplo local no hace llamadas de red. Use una base nueva para otra prueba de identidad.
python3 delivery.py identity --db /tmp/gridzen-article-fixture.sqliteFuentes y alcance
https://gridzen.ai/guides/missing-verification-webhook/ https://gridzen.ai/guides/current-capabilities/ Basado en las fuentes comprobadas y el ejemplo local; las reglas propuestas son recomendaciones de diseño, no comportamiento de producción demostrado. Redacción asistida por IA con revisión independiente.
Probar por separado tres resultados del sandbox Crear una cuenta para continuar la integración
Una descarga no demuestra una ejecución correcta. Los ejemplos locales y el sandbox en línea se prueban por separado.