Webhook de notificaciones¶
GDI envia un POST a la URL de callback configurada en la API Key TAD
(BackOffice → /api-key) cuando hay una novedad para el portal. Asi el portal se
entera sin hacer polling.
| Evento | Cuando llega |
|---|---|
documents.signed |
Termino de firmarse un documento que el portal dio de alta. Trae el numero oficial y el PDF. |
documents.signature_failed |
Esa firma fallo definitivamente. |
documents.notified |
Un agente municipal notifico documentos de un expediente al ciudadano. |
Disponibilidad por ambiente
El campo event en los eventos de firma (documents.signed y
documents.signature_failed) esta disponible en DEV y en HML; llega a
produccion con el proximo pase. En documents.notified y en webhook.test ya viene
en todos los ambientes. Detalle en API TAD Ciudadano.
Como rutear los eventos en tu handler
Todos llegan a la misma URL de callback y todos traen el campo event:
ruteá por ahí. Y descartá en silencio los event que no conozcas — es lo que hace
que un evento nuevo en una version futura de GDI no te tire el handler.
Evento documents.signed¶
El cierre del alta de documento: POST /tad/documents devolvio 202 y esto es lo
que avisa que la firma termino, con el numero oficial y el link al PDF.
{
"event": "documents.signed",
"document_id": "007a5613-f796-4280-8f3a-ddf60e6c6743",
"official_number": "PROV-2026-00003039-MDEV-TAD",
"pdf_url": "https://...firma-presignada...&X-Amz-Expires=600&...",
"status": "signed",
"sent_at": "2026-07-24T18:12:31.412Z"
}
sent_at es el momento del envio: en un reintento se refresca junto con el pdf_url.
El pdf_url expira en 10 minutos
Es un link presignado de descarga directa (600 segundos). Si el portal quiere
guardar el PDF, debe descargarlo al recibir el webhook. Siempre se puede volver a
obtener un link fresco via
GET /tad/documents/{id} o, si
el documento ya esta vinculado a un expediente, via GET /tad/cases/{id}.
El campo puede venir en null si el link no se pudo generar en ese momento: el
documento igual esta firmado y numerado, y el PDF se obtiene por la via de
arriba.
Evento documents.signature_failed¶
La firma de ese documento no salio y no se va a reintentar sola. El documento queda en el sistema sin numerar.
{
"event": "documents.signature_failed",
"document_id": "007a5613-f796-4280-8f3a-ddf60e6c6743",
"status": "failed",
"failure_reason": "notary_business_error",
"sent_at": "2026-07-24T18:12:31.412Z"
}
failure_reason es un codigo para soporte, no un texto para mostrarle al ciudadano. Que
significa cada valor y cuando conviene escalar en vez de reintentar esta en
Documentos.
Ante este evento, el portal puede volver a dar de alta el documento.
El re-alta va con una Idempotency-Key NUEVA
La clave del alta original ya quedo consumida: ese pedido si fue aceptado, lo que
fallo fue la firma posterior. Si reintentas con la misma clave recibis el 202 viejo
con Idempotent-Replay: true y el document_id de siempre — no se crea nada y el
tramite queda esperando para siempre un aviso que ya llego y fracaso.
Evento documents.notified¶
{
"event": "documents.notified",
"sent_at": "2026-07-24T18:11:09.097Z",
"municipality": {"name": "Municipalidad del Futuro", "acronym": "MDF"},
"citizen": {
"id": "2c6d5586-9cc7-46ad-809d-e9aa437002cc",
"country_id": "27333444556",
"full_name": "Maria Portal"
},
"case": {
"id": "584ee1f9-2237-4d69-93dd-0dae54f43ba2",
"number": "EE-2026-000227-MDF-INNO",
"reference": "Habilitacion comercial local Calle Falsa 123"
},
"documents": [
{
"id": "8bd9b4a2-692d-44dc-826f-22c6533545ac",
"official_number": "CAEX-2026-00003045-MDF-TAD",
"name": "Creacion EE-2026-000227-MDF-INNO",
"url": "https://...presignado-600s..."
}
]
}
Los url de los documentos son links presignados de 10 minutos, regenerados en cada intento de envio: descargarlos al recibir el webhook, o pedir links frescos con GET /tad/cases/{id}.
Verificacion de firma HMAC¶
Cada request lleva el header:
La firma es HMAC-SHA256 con el webhook secret de la API Key (se muestra una unica vez al configurarlo en BackOffice), sobre el payload:
donde path_del_callback es el path de la URL configurada (sin query string) y sha256_hex(body) es el hash SHA-256 en hexadecimal del cuerpo crudo del request.
Verificacion en Python:
import base64, hashlib, hmac, time
def verify_gdi_signature(header: str, secret: str, path: str, body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, received = parts["t"], parts["v1"]
if abs(time.time() - int(ts)) > 300: # ventana anti-replay: 5 minutos
return False
body_hash = hashlib.sha256(body).hexdigest()
payload = f"{ts}|POST|{path}|{body_hash}".encode()
expected = base64.b64encode(
hmac.new(secret.encode(), payload, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, received)
Verificar siempre la firma
Sin verificacion, cualquiera que conozca la URL del callback puede inyectar notificaciones falsas. Rechazar requests con firma invalida o timestamp fuera de la ventana de 5 minutos.
Entrega y reintentos¶
- El portal debe responder
2xxrapido (idealmente encolar y procesar despues). - Si el envio falla, GDI reintenta con backoff exponencial durante varias horas.
- La entrega es al menos una vez: ante reintentos el portal puede recibir el mismo evento repetido. Para deduplicar: en los eventos de firma, por
document_id(si ese documento ya tiene numero guardado, descartar); endocuments.notified, por el par (case.id,documents[].id).sent_atno sirve para deduplicar: se refresca en cada reintento.
Probar el webhook (sandbox)¶
Dispara un webhook de prueba al webhook_url configurado en tu API Key TAD, firmado con el mismo HMAC que un webhook real. Sirve para verificar que tu receptor recibe el POST y valida bien la firma, sin depender de un tramite real ni tocar produccion. No lleva X-Citizen-ID ni body.
El evento es webhook.test (no documents.notified) y trae datos ficticios: tu receptor debe reconocerlo y no procesarlo como una notificacion real.
curl -X POST "https://gateway.your-domain.com/api/v1/tad/webhook/test" \
-H "X-API-Key: tu-api-key-tad"
La respuesta te dice que respondio tu propio servidor (la llamada es sincrona):
{
"delivered": true,
"webhook_url": "https://portal.tu-muni.gob.ar/avisos-gdi",
"status_code": 200,
"signature": "t=1784924465,v1=4//7q...",
"event": "webhook.test",
"error": null
}
delivered: true→ tu servidor respondio2xx: recibe y (si tu codigo valida la firma) el circuito funciona.delivered: falseconstatus_code→ tu servidor respondio pero con error (revisar tu handler); constatus_code: nullyerror→ GDI no pudo ni conectar (URL mal, DNS, TLS, timeout, firewall).422→ tu municipio todavia no tiene una API Key TAD conwebhook_urly secret configurados en BackOffice.
El cuerpo del webhook.test que recibe tu servidor tiene la misma forma que documents.notified, mas un campo note que aclara que es una prueba:
{
"event": "webhook.test",
"sent_at": "2026-07-24T20:00:00.000Z",
"municipality": {"name": "...", "acronym": "..."},
"citizen": {"id": "00000000-...", "country_id": "20000000001", "full_name": "Ciudadano de Prueba"},
"case": {"id": "00000000-...", "number": "EE-2026-000000-TEST-XXXX", "reference": "Expediente de prueba (webhook.test)"},
"documents": [{"id": "00000000-...", "official_number": "TEST-2026-00000000-XXXX-TAD", "name": "Documento de prueba", "url": "https://ejemplo.invalido/documento-de-prueba.pdf"}],
"note": "Webhook de PRUEBA disparado desde POST /api/v1/tad/webhook/test. No corresponde a un tramite real."
}