Webhook de notificaciones¶
Cuando un agente municipal notifica documentos de un expediente a un ciudadano desde la app GDI, GDI envia un POST a la URL de callback configurada en la API Key TAD (BackOffice → /api-key). Asi el portal se entera de novedades sin hacer polling.
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. Usar el par (
case.id,sent_at) o el contenido dedocumentspara deduplicar.
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."
}