> ## 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 : événements de transaction et signature

> Webhooks Didit pour les transactions : déclencheurs, payloads et signature HMAC. 1,000 transactions par mois incluses, puis $0.001 chacune.

Transaction Monitoring émet trois événements webhook qui couvrent ensemble tout le cycle de vie d'une transaction, ainsi qu'un quatrième optionnel qui annonce de nouvelles suggestions de règles. Tous reposent sur l'infrastructure webhook standard de Didit, avec une signature HMAC-SHA256 : consultez la [référence des webhooks](https://docs.didit.me/fr/integration/webhooks.md) pour les destinations, la signature et les tests.

## Catalogue des événements

| Événement | Moment du déclenchement |
| - | - |
| `transaction.created` | Une nouvelle transaction a été créée et l'évaluation initiale est terminée. Se déclenche aussi pour les transactions entrantes que Didit crée pour vous : un enregistrement [Travel Rule sunrise](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md#enregistrement-a-posteriori-sunrise) après un dépôt, une demande `TRP` entrante, ou un transfert sur le rail `INTERNAL` provenant d'un autre client Didit. |
| `transaction.status.updated` | Le statut de la transaction a changé, à la suite d'une revue par un analyste, de la résolution d'une session de remédiation, ou d'une action de règle lorsque Didit réévalue vos règles après une transition Travel Rule (par exemple après une preuve de propriété de portefeuille). Le changement de statut qui applique votre `timeout_outcome` lorsqu'un transfert expire n'envoie pas cet événement : réagissez plutôt à `travel_rule.status.updated` avec `EXPIRED`. |
| `travel_rule.status.updated` | Un échange [Travel Rule](https://docs.didit.me/fr/transaction-monitoring/travel-rule.md) a changé d'état, pour les transferts sortants comme pour les transferts entrants (la branche entrante sur le rail `INTERNAL`, les enregistrements sunrise et les demandes `TRP` entrantes), y compris la transition `NOT_APPLICABLE` en dessous du seuil. Pour une transaction que Didit crée, ces événements sont envoyés après son `transaction.created`. |
| `transaction_rule_suggestions.created` | Une exécution de suggestions de règles a proposé de nouvelles modifications de règles pour votre application. Se déclenche uniquement si vous activez **Webhook** dans les paramètres de notification des suggestions de règles, et uniquement pour les exécutions ayant créé des suggestions. |

Les événements au niveau des entités (`user.status.updated`, `business.status.updated`) peuvent aussi se déclencher en conséquence d'une activité de transaction : voir [webhooks d'entités](https://docs.didit.me/fr/entities/webhooks.md).

> [!NOTE]
>
> Les applications sandbox n'émettent pas de webhooks `transaction.created` ni `transaction.status.updated`. Pour tester ces événements, utilisez une application en environnement de production ou **API & Webhooks → Try Webhook** dans la console. Les échanges Travel Rule en sandbox émettent bien des webhooks `travel_rule.status.updated` avec `environment: sandbox` ; utilisez le [Didit Test VASP](https://docs.didit.me/fr/transaction-monitoring/travel-rule-sandbox.md) pour tester un échange avant la mise en production.

## Enveloppe

Les webhooks Didit utilisent un corps **plat** : les champs de l'événement se trouvent au premier niveau, à côté des champs d'enveloppe, et il n'existe pas d'objet `data` imbriqué. Chaque livraison contient :

| Champ | Description |
| - | - |
| `webhook_type` | Le nom de l'événement, par exemple `transaction.created`. |
| `event_id` | Identifiant stable de l'événement : le même `event_id` est réutilisé lors des nouvelles tentatives, dédupliquez donc sur ce champ. |
| `timestamp` / `created_at` | Secondes depuis l'epoch Unix au moment où la livraison a été envoyée. |
| `application_id` | L'application à laquelle appartient l'événement. |
| `environment` | `live` ou `sandbox`. |

## Structure des payloads

### `transaction.created` et `transaction.status.updated`

Les deux événements partagent un même payload : un résumé compact, ainsi que le bloc [`action_required`](https://docs.didit.me/fr/transaction-monitoring/sdk-transaction-submission#actions-requises-de-l’utilisateur) (ou `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
}
```

Le payload est volontairement léger : à réception, récupérez l'enregistrement complet avec [`GET /v3/transactions/{transaction_id}/`](https://docs.didit.me/fr/management-api/transactions/get). Il renvoie le détail complet (parties, `rule_runs`, `provider_results`, `travel_rule`, `cost_breakdown`, `decision_reason_code`), y compris *pourquoi* le statut a changé.

### `transaction_rule_suggestions.created`

Cet événement concerne votre ensemble de règles et non une transaction unique : il ne contient donc aucun identifiant de transaction.
Il ne contient jamais de données personnelles : uniquement des identifiants de suggestion, des types, des titres et des compteurs.

```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": {}
    }
  ]
}
```

| Champ | Description |
| - | - |
| `run_id` | L'exécution de suggestions qui a produit les nouvelles suggestions. |
| `open_count` | Nombre de suggestions ouvertes pour l'application après cette exécution. |
| `suggestions[]` | Les suggestions créées par cette exécution, chacune avec `suggestion_id`, `kind`, `title`, `rule_id` (null lorsque la suggestion propose une nouvelle règle) et `expected_impact`. |

Les champs d'enveloppe habituels (`event_id`, `timestamp`, `application_id`, `environment`) sont ajoutés au corps.

### `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"
}
```

Un événement est envoyé à chaque transition, y compris le passage à `FINISHED`, `CANCELLED` ou `EXPIRED` ; une fois qu'un transfert atteint l'un de ces états, aucun autre événement Travel Rule n'est envoyé pour lui.
Un événement peut répéter le statut de l'événement précédent, par exemple lorsqu'un renvoi ne trouve de nouveau aucun rail et que le transfert reste en `COUNTERPARTY_VASP_NOT_FOUND`.
`rail` vaut `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA`, `EMAIL`, `TEST_VASP` (un transfert sandbox auquel le Didit Test VASP a répondu, qui porte aussi `test_vasp: true` et `test_vasp_scenario`), ou `""` tant qu'aucun rail n'est sélectionné (toujours `""` pour les enregistrements sunrise).

À la création d'une transaction, lors d'un enregistrement sunrise, d'une demande `TRP` entrante ou de la branche entrante sur le rail `INTERNAL`, Didit envoie d'abord `transaction.created`, puis les événements `travel_rule.status.updated` de la transaction.
Une règle qui modifie le statut de la transaction pendant cette étape peut aussi envoyer `transaction.status.updated` avant `transaction.created`.
Un enregistrement sunrise rattaché à une transaction de dépôt que vous avez déjà créée n'envoie aucun `transaction.created`.
Chaque webhook est livré et réessayé indépendamment : un événement réessayé peut donc arriver après un événement plus récent. Utilisez `transaction_id` comme clé de votre gestionnaire et relisez la transaction lorsque vous avez besoin de son état actuel.

## Exemples de parcours de statut

### Approbation simple

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

### Examen puis approbation

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

### Vérification renforcée

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

### Transfert 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)
```

Les deux premiers événements proviennent de la même requête, et `transaction.created` est mis en file d'attente en premier. Chaque webhook est livré indépendamment : ne vous fiez donc pas à l'ordre d'arrivée.
Un transfert envoyé via TRUST ne reçoit jamais d'événement `COMPLETED`, car TRUST ne renvoie rien que Didit puisse lire : il reste en `AWAITING_COUNTERPARTY` jusqu'à l'échéance de confirmation, puis passe à `EXPIRED`. Via GTR, VerifyVASP et Sygna Bridge, Didit lit la réponse de la contrepartie sur le réseau, et l'événement suit cette réponse.

## Vérification de la signature

Chaque livraison est signée en HMAC-SHA256 avec le secret partagé de la destination et contient trois en-têtes de signature ainsi qu'un horodatage :

| En-tête | Schéma |
| - | - |
| `X-Signature-V2` | HMAC sur le JSON canonique (clés triées, Unicode conservé) - **recommandé**. |
| `X-Signature` | HMAC sur le JSON canonique échappé en ASCII (ancien format). |
| `X-Signature-Simple` | HMAC sur `"{timestamp}:{session_id}:{status}:{webhook_type}"` - **obsolète**. Les payloads de transaction ne contiennent pas de `session_id` ; ce schéma signe donc `"{timestamp}::{status}:{webhook_type}"` : il n'identifie ni la transaction, ni son montant, ni son score. Un payload `travel_rule.status.updated` ne contient pas non plus de `status`, il signe donc `"{timestamp}:::travel_rule.status.updated"`. Vérifiez avec `X-Signature-V2`, et consultez [l'avis de dépréciation](https://docs.didit.me/fr/integration/webhooks.md#x-signature-simple-est-déprécié). |
| `X-Timestamp` | Secondes Unix (epoch) - rejetez les livraisons datant de plus de 5 minutes. |

```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));
}
```

Consultez la [référence des webhooks](https://docs.didit.me/fr/integration/webhooks.md#vérification-de-la-signature) pour le code de vérification complet, y compris le contrôle de fraîcheur de l'horodatage.

## Réessais et garanties de livraison

* En cas de `5xx`, de `404`, de délai d'attente dépassé ou d'échec de connexion, Didit réessaie jusqu'à **2 fois** : environ 1 minute après l'échec initial, puis environ 4 minutes supplémentaires.
* Les livraisons réessayées réutilisent le même `event_id` : effectuez la déduplication à l'aide de ce champ dans votre gestionnaire.
* Après le dernier échec, la livraison de cet événement s'arrête et votre organisation en est informée ; les destinations ne sont jamais désactivées silencieusement.
* Renvoyez rapidement un `2xx` (mise en file d'attente, puis traitement) : des accusés de réception tardifs risquent de provoquer des délais d'attente dépassés et des livraisons en double.

## Idempotence

Traitez chaque webhook une seule fois en stockant `event_id` dans votre propre système de déduplication. Une nouvelle livraison du même événement est possible lorsque votre endpoint accuse réception tardivement ou se déconnecte en cours de requête.

## Abonnement

```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"
    ]
  }'
```

## Étapes suivantes

> [card] [Référence des webhooks](https://docs.didit.me/fr/integration/webhooks.md)
>
> Destinations, signature, réessais.

> [card] [Statuts](https://docs.didit.me/fr/transaction-monitoring/statuses.md)
>
> Machine à états complète.

> [card] [Guide d'intégration](https://docs.didit.me/fr/transaction-monitoring/integration-guide.md)
>
> Gérer les webhooks en production.


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