> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks KYT: eventos de transacciones y firma HMAC

> Webhooks de transacciones de Didit: disparadores, payloads y firma HMAC. 1,000 transacciones al mes incluidas, luego $0.001 cada una.

Transaction Monitoring emite tres eventos de webhook que, en conjunto, cubren el ciclo de vida completo de una transacción, además de un cuarto opcional que anuncia nuevas sugerencias de reglas. Todos usan la infraestructura estándar de webhooks de Didit con firma HMAC-SHA256. Consulta la [referencia de webhooks](https://docs.didit.me/es/integration/webhooks.md) para ver destinos, firma y pruebas.

## Catálogo de eventos

| Evento | Cuándo se dispara |
| - | - |
| `transaction.created` | Se creó una nueva transacción y se completó su evaluación inicial. También se dispara para las transacciones entrantes que Didit crea por ti: un [registro sunrise de Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md#registro-sunrise-posterior-al-depósito) posterior a un depósito, una consulta `TRP` entrante o una transferencia a través de la red `INTERNAL` desde otro cliente de Didit. |
| `transaction.status.updated` | Cambió el estado de la transacción. El cambio puede venir de la revisión de un analista, de la resolución de una sesión de remediación o de la acción de una regla cuando Didit vuelve a evaluar tus reglas tras una transición de Travel Rule (por ejemplo, después de una prueba de propiedad de la wallet). El cambio de estado que aplica tu `timeout_outcome` cuando una transferencia expira no envía este evento; en su lugar, reacciona a `travel_rule.status.updated` con `EXPIRED`. |
| `travel_rule.status.updated` | Un intercambio de [Travel Rule](https://docs.didit.me/es/transaction-monitoring/travel-rule.md) cambió de estado. Se dispara en transferencias salientes y entrantes (el tramo entrante por el rail `INTERNAL`, los registros sunrise y las consultas `TRP` entrantes) e incluye la transición `NOT_APPLICABLE` por debajo del umbral. En una transacción que crea Didit, estos eventos se envían después de su `transaction.created`. |
| `transaction_rule_suggestions.created` | Una ejecución de sugerencias de reglas propuso nuevos cambios de reglas para tu aplicación. Solo se dispara cuando activas **Webhook** en la configuración de notificaciones de sugerencias de reglas, y solo para las ejecuciones que crearon sugerencias. |

Los eventos a nivel de entidad (`user.status.updated`, `business.status.updated`) también pueden dispararse como consecuencia de la actividad de las transacciones. Consulta los [webhooks de entidades](https://docs.didit.me/es/entities/webhooks.md).

> [!NOTE]
>
> Las aplicaciones de sandbox no emiten los webhooks `transaction.created` ni `transaction.status.updated`. Para probar esos eventos, usa una aplicación del entorno de producción o **API & Webhooks → Try Webhook** en la consola. Los intercambios de Travel Rule en sandbox sí emiten webhooks `travel_rule.status.updated` con `environment: sandbox`; usa el [Didit Test VASP](https://docs.didit.me/es/transaction-monitoring/travel-rule-sandbox.md) para probar un intercambio antes de pasar a producción.

## Envoltorio

Los webhooks de Didit usan un cuerpo **plano**: los campos del evento están en el nivel superior, junto a los campos del envoltorio, y no hay ningún objeto `data` anidado.
Cada entrega incluye:

| Campo | Descripción |
| - | - |
| `webhook_type` | El nombre del evento, por ejemplo `transaction.created`. |
| `event_id` | Identificador estable del evento. Se reutiliza el mismo `event_id` en los reintentos, así que úsalo para eliminar duplicados. |
| `timestamp` / `created_at` | Segundos desde la época Unix cuando se envió la entrega. |
| `application_id` | La aplicación a la que pertenece el evento. |
| `environment` | `live` o `sandbox`. |

## Estructura de los payloads

### `transaction.created` y `transaction.status.updated`

Ambos eventos comparten un mismo payload: un resumen compacto más el bloque [`action_required`](https://docs.didit.me/es/transaction-monitoring/sdk-transaction-submission#acciones-requeridas-del-usuario) (o `null`):

```json theme={null}
{
  "webhook_type": "transaction.created",
  "event_id": "9c0c8b8a-1111-4222-9333-666666666666",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "9c7d2a30-...",
  "txn_id": "tx-0001",
  "status": "IN_REVIEW",
  "score": 65,
  "severity": "MEDIUM",
  "amount": "500.00",
  "currency": "EUR",
  "direction": "OUTBOUND",
  "action_required": null
}
```

El payload es deliberadamente escueto: al recibirlo, obtén el registro completo con [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/es/management-api/transactions/get). Este endpoint devuelve todos los detalles (partes, `rule_runs`, `provider_results`, `travel_rule`, `cost_breakdown`, `decision_reason_code`), incluido *por qué* cambió el estado.

### `transaction_rule_suggestions.created`

Este evento se refiere a tu conjunto de reglas, no a una transacción concreta, por lo que no incluye ningún ID de transacción.
Nunca contiene datos personales: solo IDs, tipos, títulos y recuentos de las sugerencias.

```json theme={null}
{
  "webhook_type": "transaction_rule_suggestions.created",
  "run_id": "5b2f0c1e-0000-0000-0000-000000000000",
  "open_count": 3,
  "suggestions": [
    {
      "suggestion_id": "9a7d3e22-0000-0000-0000-000000000000",
      "kind": "...",
      "title": "...",
      "rule_id": "...",
      "expected_impact": {}
    }
  ]
}
```

| Campo | Descripción |
| - | - |
| `run_id` | La ejecución que generó las nuevas sugerencias. |
| `open_count` | Cuántas sugerencias quedan abiertas para la aplicación después de esta ejecución. |
| `suggestions[]` | Las sugerencias que creó esta ejecución, cada una con `suggestion_id`, `kind`, `title`, `rule_id` (null cuando la sugerencia propone una regla nueva) y `expected_impact`. |

Los campos habituales del envoltorio (`event_id`, `timestamp`, `application_id`, `environment`) se añaden al cuerpo.

### `travel_rule.status.updated`

```json theme={null}
{
  "webhook_type": "travel_rule.status.updated",
  "event_id": "9c0c8b8a-1111-4222-9333-777777777777",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "txn_id": "trv-2026-07-05-001",
  "travel_rule_status": "COMPLETED",
  "rail": "INTERNAL",
  "direction": "OUTBOUND"
}
```

Se dispara en cada transición, incluido el paso a `FINISHED`, `CANCELLED` o `EXPIRED`; una vez que una transferencia llega a uno de esos estados, no se envían más eventos de Travel Rule para ella.
Un evento puede repetir el estado del anterior, por ejemplo cuando un reenvío vuelve a no encontrar ningún rail y la transferencia permanece en `COUNTERPARTY_VASP_NOT_FOUND`.
`rail` es `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA`, `EMAIL`, `TEST_VASP` (una transferencia de sandbox a la que respondió el Didit Test VASP, que además incluye `test_vasp: true` y `test_vasp_scenario`), o `""` mientras no se haya seleccionado ningún rail (siempre `""` en los registros sunrise).

Al crear una transacción, en un registro sunrise, en una consulta `TRP` entrante o en el tramo entrante por el rail `INTERNAL`, Didit envía primero `transaction.created` y, después, los eventos `travel_rule.status.updated` de la transacción.
Una regla que cambie el estado de la transacción durante ese paso también puede enviar `transaction.status.updated` antes de `transaction.created`.
Un registro sunrise que se asocia a una transacción de depósito que ya creaste no envía `transaction.created`.
Cada webhook se entrega y se reintenta por separado, así que un evento reintentado puede llegar después de uno posterior: usa `transaction_id` como clave en tu handler y vuelve a leer la transacción cuando necesites su estado actual.

## Ejemplos de recorridos de estado

### Aprobación simple

```
transaction.created (APPROVED, score 0)
```

### Revisión y luego aprobación

```
transaction.created (IN_REVIEW, score 65)
...analyst reviews...
transaction.status.updated (status: APPROVED)
```

### Verificación adicional (step-up)

```
transaction.created (AWAITING_USER, action_required.type: verification_session)
...user completes the biometric re-check...
transaction.status.updated (status: APPROVED, action_required: null)
```

### Transferencia con Travel Rule

```
transaction.created (APPROVED)
travel_rule.status.updated (AWAITING_COUNTERPARTY, rail: EMAIL)
...counterparty accepts the pickup...
travel_rule.status.updated (COMPLETED)
...you broadcast on-chain and PATCH the hash...
travel_rule.status.updated (FINISHED)
```

Los dos primeros eventos provienen de la misma solicitud, y `transaction.created` se pone en cola primero; cada webhook se entrega por separado, así que no dependas del orden de llegada.
Una transferencia enviada a través de TRUST nunca recibe un evento `COMPLETED`, porque TRUST no devuelve nada que Didit pueda leer: permanece en `AWAITING_COUNTERPARTY` hasta que vence el plazo de confirmación y termina en `EXPIRED`. A través de GTR, VerifyVASP y Sygna Bridge, Didit lee la respuesta de la contraparte desde la red y el evento la refleja.

## Verificación de firma

Cada entrega se firma con HMAC-SHA256 usando el secreto compartido del destino e incluye tres encabezados de firma más una marca de tiempo:

| Encabezado | Esquema |
| - | - |
| `X-Signature-V2` | HMAC sobre el JSON canónico (claves ordenadas, Unicode conservado): **recomendado**. |
| `X-Signature` | HMAC sobre el JSON canónico con escape ASCII (heredado). |
| `X-Signature-Simple` | HMAC sobre `"{timestamp}:{session_id}:{status}:{webhook_type}"`: **obsoleto**. Los payloads de transacciones no incluyen `session_id`, así que esto firma `"{timestamp}::{status}:{webhook_type}"`: no identifica ni la transacción ni su importe o puntuación. Un payload `travel_rule.status.updated` tampoco incluye `status`, así que firma `"{timestamp}:::travel_rule.status.updated"`. Verifica con `X-Signature-V2` y consulta [el aviso de obsolescencia](https://docs.didit.me/es/integration/webhooks.md#x-signature-simple-está-obsoleto). |
| `X-Timestamp` | Segundos de época Unix: rechaza las entregas con más de 5 minutos de antigüedad. |

```typescript theme={null}
import { createHmac, timingSafeEqual } from 'crypto';

function verify(rawBody: string, signatureV2: string, secret: string) {
  const canonical = JSON.stringify(
    Object.keys(JSON.parse(rawBody)).sort()
      .reduce((acc, k) => ({ ...acc, [k]: JSON.parse(rawBody)[k] }), {})
  );
  const expected = createHmac('sha256', secret).update(canonical).digest('hex');
  return timingSafeEqual(Buffer.from(signatureV2), Buffer.from(expected));
}
```

Consulta la [referencia de webhooks](https://docs.didit.me/es/integration/webhooks.md#verificación-de-firma) para ver el código de verificación completo, incluida la comprobación de vigencia de la marca de tiempo.

## Reintentos y garantías de entrega

* Ante un `5xx`, un `404`, un tiempo de espera agotado o un fallo de conexión, Didit reintenta hasta **2 veces**: aproximadamente 1 minuto después del fallo inicial y, después, unos 4 minutos más tarde.
* Las entregas reintentadas reutilizan el mismo `event_id`: úsalo para eliminar duplicados en tu handler.
* Tras el último fallo, la entrega de ese evento se detiene y se notifica a tu organización; los destinos nunca se desactivan de forma silenciosa.
* Devuelve un `2xx` rápidamente (encola y luego procesa): las confirmaciones lentas pueden provocar tiempos de espera agotados y entregas duplicadas.

## Idempotencia

Procesa cada webhook exactamente una vez guardando `event_id` en tu propio almacén de deduplicación. Es posible que el mismo evento se vuelva a entregar si tu endpoint confirma lentamente o se desconecta a mitad de la solicitud.

## Suscripción

```bash theme={null}
curl -X POST https://verification.didit.me/v3/webhook/destinations/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "TM events",
    "url": "https://yourapp.com/webhooks/didit-tm",
    "subscribed_events": [
      "transaction.created",
      "transaction.status.updated",
      "travel_rule.status.updated"
    ]
  }'
```

## Próximos pasos

> [card] [Referencia de webhooks](https://docs.didit.me/es/integration/webhooks.md)
>
> Destinos, firma y reintentos.

> [card] [Estados](https://docs.didit.me/es/transaction-monitoring/statuses.md)
>
> Máquina de estados completa.

> [card] [Guía de integración](https://docs.didit.me/es/transaction-monitoring/integration-guide.md)
>
> Gestión de webhooks en producción.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.