---
title: Développer une intégration Inference hooks
url: https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint
description: Construisez le serveur de sécurité IA qui reçoit les requêtes Inference hooks signées, les vérifie et renvoie des verdicts d'autorisation ou de refus.
---

<Note>
  Les Inference hooks sont en bêta et disponibles pour les organisations Claude Enterprise. Les noms de champs, les formes de requêtes et les en-têtes peuvent changer pendant la bêta.
</Note>

Une intégration Inference hooks est un « AI security server » (serveur de sécurité IA) : un service HTTPS qu'Anthropic appelle. Pour chaque requête gouvernée, votre serveur reçoit un `POST` signé contenant la transcription de la conversation et répond par un verdict d'autorisation (allow) ou de refus (deny). Cette page documente le protocole permettant de construire ce serveur : les schémas de requête et de verdict, la vérification de signature et le contrat opérationnel.

Pour activer les Inference hooks et les diriger vers votre point de terminaison, consultez [Configurer les Inference hooks](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration). Pour savoir ce que sont les Inference hooks et quand les utiliser, consultez la [présentation des Inference hooks](https://platform.claude.com/docs/fr/manage-claude/inference-hooks).

## Obtenir un premier aller-retour de verdict

La plus petite intégration fonctionnelle est un serveur qui lit chaque requête et l'autorise. Exécutez l'un des serveurs suivants, exposez-le à une URL publique `https://` (par exemple, derrière un proxy inverse assurant la terminaison TLS sur un hôte que vous contrôlez, et non un service de tunnel inverse ; voir [Recevoir une requête](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#receive-a-request)), puis demandez à votre administrateur de [le définir comme point de terminaison et de tester la connexion](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) : le résultat de **Test connection** indique le verdict d'autorisation renvoyé par votre serveur.

<CodeGroup exclude="shell">
  ```python Python
  # Exécuter avec : python server.py
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


  class VerdictHandler(BaseHTTPRequestHandler):
      protocol_version = "HTTP/1.1"  # keep the connection open between verdicts

      def do_POST(self):
          # Vider le corps ; les transcriptions peuvent peser plusieurs mégaoctets.
          self.rfile.read(int(self.headers.get("Content-Length", 0)))
          verdict = b'{"action": "allow"}'
          self.send_response(200)
          self.send_header("Content-Type", "application/json")
          self.send_header("Content-Length", str(len(verdict)))
          self.end_headers()
          self.wfile.write(verdict)


  ThreadingHTTPServer(("", 8000), VerdictHandler).serve_forever()
  ```

  ```typescript TypeScript
  // Exécuter avec : node server.ts
  import { createServer } from "node:http";

  createServer((request, response) => {
    // Videz le corps avant de répondre ; les transcriptions peuvent peser plusieurs mégaoctets.
    request.resume();
    request.on("end", () => {
      response.writeHead(200, { "Content-Type": "application/json" });
      response.end('{"action": "allow"}');
    });
  }).listen(8000);
  ```

  ```csharp C#
  #:sdk Microsoft.NET.Sdk.Web
  #:property PublishAot=false
  // Exécuter avec : dotnet run server.cs

  var app = WebApplication.Create();

  app.MapPost("/{**path}", async (HttpRequest request) =>
  {
      // Vider le corps ; les transcriptions peuvent peser plusieurs mégaoctets.
      await request.Body.CopyToAsync(Stream.Null);
      return Results.Text("""{"action": "allow"}""", "application/json");
  });

  app.Run("http://0.0.0.0:8000");
  ```

  ```go Go
  // Exécuter avec : go run server.go
  package main

  import (
  	"io"
  	"log"
  	"net/http"
  )

  func main() {
  	http.HandleFunc("POST /", func(writer http.ResponseWriter, request *http.Request) {
  		// Vider le corps pour que la connexion puisse être réutilisée ; les transcriptions peuvent peser plusieurs mégaoctets.
  		io.Copy(io.Discard, request.Body)
  		writer.Header().Set("Content-Type", "application/json")
  		writer.Write([]byte(`{"action": "allow"}`))
  	})
  	log.Fatal(http.ListenAndServe(":8000", nil))
  }
  ```

  ```java Java
  // Exécuter avec : java VerdictServer.java
  import com.sun.net.httpserver.HttpServer;

  void main() throws IOException {
      HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
      server.createContext("/", exchange -> {
          // Vider le corps sans le mettre en mémoire tampon ; les transcriptions peuvent peser plusieurs mégaoctets.
          exchange.getRequestBody().transferTo(OutputStream.nullOutputStream());
          byte[] verdict = "{\"action\": \"allow\"}".getBytes(StandardCharsets.UTF_8);
          exchange.getResponseHeaders().set("Content-Type", "application/json");
          exchange.sendResponseHeaders(200, verdict.length);
          try (OutputStream responseBody = exchange.getResponseBody()) {
              responseBody.write(verdict);
          }
      });
      server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
      server.start();
  }
  ```

  ```php PHP
  <?php
  // Exécuter avec : php -S 0.0.0.0:8000 server.php

  // Vider le corps ; les transcriptions peuvent peser plusieurs mégaoctets.
  file_get_contents('php://input');

  http_response_code(200);
  header('Content-Type: application/json');
  echo '{"action": "allow"}';
  ```

  ```ruby Ruby
  # webrick est une gem ordinaire dans Ruby 3.4 : gem install webrick, ou ajoutez gem "webrick".
  # Exécutez avec : ruby server.rb
  require "webrick"

  server = WEBrick::HTTPServer.new(Port: 8000)
  server.mount_proc("/") do |request, response|
    request.body # Drain the body; transcripts can be megabytes.
    response.status = 200
    response["Content-Type"] = "application/json"
    response.body = '{"action": "allow"}'
  end
  server.start
  ```
</CodeGroup>

<Note>
  Ces serveurs acceptent toutes les requêtes, y compris celles qui ne sont pas signées. Ajoutez la [vérification de signature](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#verify-the-signature) avant d'appliquer des règles.
</Note>

## Recevoir une requête

Anthropic envoie un `POST` HTTPS à l'URL configurée par votre administrateur. L'URL configurée dans son intégralité constitue le point de terminaison : il n'y a pas de suffixe de chemin fixe, choisissez donc le chemin qui convient à votre serveur.

Hébergez votre serveur de sécurité IA à un emplacement accessible par Anthropic : une URL `https://` sur le port 443, sur un hôte routable publiquement (les plages privées, de bouclage et de NAT de niveau opérateur sont refusées au moment de la connexion), avec un certificat validé par le magasin de confiance des autorités de certification publiques, et répondant sans redirection. L'hôte doit disposer d'une adresse IPv4, qu'Anthropic utilise même lorsque l'hôte possède également des adresses IPv6 ; une URL dont l'hôte est `localhost` ou une adresse IPv6 est refusée. L'URL configurée doit être la destination finale. Les hôtes de tunnel inverse (ngrok et services de tunnel similaires) ne sont pas pris en charge : la politique réseau d'Anthropic les bloque. Hébergez votre serveur sur un domaine que vous contrôlez. [Configurer les Inference hooks](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) explique comment votre administrateur définit et teste l'URL.

Chaque requête comporte ces en-têtes fixes, ainsi que tous les [en-têtes de requête personnalisés](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) configurés par votre administrateur et, une fois que votre organisation dispose d'un secret de signature, les en-têtes de signature `webhook-*` décrits dans [Vérifier la signature](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#verify-the-signature) :

| En-tête           | Valeur             |
| ----------------- | ------------------ |
| `Content-Type`    | `application/json` |
| `User-Agent`      | `anthropic-dlp/1`  |
| `Accept-Encoding` | `identity`         |

Il existe deux événements de hook, distingués par le champ `type` de premier niveau. La « prompt frame » (trame de prompt) est envoyée une fois par requête d'inférence gouvernée, avant le début de l'inférence. La « tool call frame » (trame d'appel d'outil) est envoyée lorsqu'une réponse du modèle contient des appels d'outils, avant l'exécution de l'un d'entre eux, dans les organisations où **Validate tool calls** est activé. Dans les deux cas, Anthropic attend que votre serveur de sécurité IA réponde ou que le délai d'expiration du verdict soit écoulé.

## La trame de prompt

Le corps de la requête est un objet JSON comportant ces champs :

| Champ        | Type           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`       | string         | L'événement de hook : `"prompt"` ou `"tool_call"` (consultez [La trame d'appel d'outil](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#the-tool-call-frame)). D'autres types d'événements seront introduits à l'avenir, gérez donc une valeur non reconnue de manière appropriée (consultez [Compatibilité ascendante](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#forward-compatibility)). |
| `request_id` | string         | Identifiant opaque par trame servant à la corrélation. Égal à l'en-tête `webhook-id`.                                                                                                                                                                                                                                                                                                                                                                  |
| `tenant_id`  | string ou null | Identifiant opaque de l'organisation à laquelle appartient la requête.                                                                                                                                                                                                                                                                                                                                                                                 |
| `actor`      | object         | Le principal auquel la requête est attribuée, discriminé sur `type` (`"user"` est la seule valeur envoyée aujourd'hui) : `id` (un identifiant étiqueté, stable d'une requête à l'autre pour le même compte) et `email_address` (lorsqu'elle est disponible). `id` et `email_address` peuvent tous deux être null.                                                                                                                                      |
| `source`     | object         | L'application d'origine : `application` (voir [Valeurs de source](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#source-values)).                                                                                                                                                                                                                                                                                          |
| `messages`   | array          | La transcription de la conversation jusqu'au point d'inférence. Voir [Blocs de contenu](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#content-blocks).                                                                                                                                                                                                                                                                    |
| `session_id` | string ou null | Identifiant opaque de conversation, lorsqu'il en existe un. Ne l'analysez pas. Pour Claude Code, il s'agit d'un identifiant de session déclaré par le client, fourni au mieux.                                                                                                                                                                                                                                                                         |
| `model`      | string ou null | Identifiant public du modèle pour cette requête, lorsqu'il est disponible.                                                                                                                                                                                                                                                                                                                                                                             |
| `metadata`   | object         | Table d'extension réservée associant des clés de type chaîne à des valeurs de type chaîne, envoyée vide aujourd'hui. N'en exigez rien, et tolérez son absence, sa présence et toute clé qui y apparaît.                                                                                                                                                                                                                                                |

Un exemple de corps de requête :

```json
{
  "type": "prompt",
  "request_id": "req_abc123",
  "tenant_id": "11111111-1111-1111-1111-111111111111",
  "actor": {
    "type": "user",
    "id": "user_01AbCdEfGhIjKlMnOpQrStUv",
    "email_address": "alice@example.com"
  },
  "source": {
    "application": "claude-ai"
  },
  "session_id": "22222222-2222-2222-2222-222222222222",
  "model": "claude-sonnet-4-5",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Summarize the attached report."
        },
        {
          "type": "attachment",
          "file_name": "q2-report.pdf",
          "media_type": "application/pdf",
          "size_bytes": 48213,
          "text": "Q2 revenue grew 14% quarter over quarter..."
        }
      ]
    }
  ],
  "metadata": {}
}
```

### Blocs de contenu

Chaque entrée de `messages` possède un `role` valant `user` ou `assistant` (les résultats d'outils apparaissent sous le rôle `user`, conformément au modèle de contenu de l'API Messages publique) et un tableau `content` de blocs discriminés par `type` :

| `type` de bloc | Champs                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `text`         | `text` : le contenu textuel.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `tool_use`     | `id` : l'identifiant auquel fait référence le résultat d'outil correspondant. `tool_name` : le nom de l'outil. `input` : les arguments que le modèle a transmis à l'outil. `tool_info` : uniquement sur une trame d'appel d'outil (omis ailleurs, jamais `null`), un objet qui indique qui exécute ou fournit l'outil ; consultez [La trame d'appel d'outil](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#the-tool-call-frame).                                    |
| `tool_result`  | `content` : la sortie de l'outil sous forme de texte, les parties étant jointes par des sauts de ligne ; les parties binaires telles que les images sont remplacées par des marqueurs de substitution, et les octets bruts ne sont jamais envoyés. `is_error` : indique si l'appel d'outil a échoué. `tool_name` : le nom de l'outil, afin qu'une politique puisse se fonder sur l'identité de l'outil sans recouper un bloc antérieur. `tool_use_id` : l'`id` du bloc `tool_use` correspondant. |
| `attachment`   | `file_name` : le nom ou chemin d'origine du fichier. `media_type` : le type de média de la pièce jointe. `size_bytes` : la taille du fichier d'origine. `text` : le contenu textuel de la pièce jointe lorsqu'il est disponible, tel que le texte extrait d'un document, la transcription d'un audio ou les métadonnées d'un lien. Les octets bruts des pièces jointes ne sont jamais envoyés.                                                                                                   |

À l'exception de `type`, du `text` d'un bloc `text`, ainsi que du `content` et de l'`is_error` d'un bloc `tool_result`, chacun de ces champs peut valoir `null` lorsque la valeur n'est pas connue ; par exemple, une image arrive sous la forme d'un bloc `attachment` dont `file_name` et `text` valent `null`.

Un bloc dont vous ne reconnaissez pas le `type` est un ajout à compatibilité ascendante. Le seul champ qu'il garantit est `type` ; votre politique peut inspecter tous les autres champs présents, mais ne doit pas rejeter la requête en raison d'un type non reconnu.

### Ce que contient la transcription

La transcription est la conversation telle que l'utilisateur final la voit, jusqu'au point d'inférence : texte de la transcription, appels d'outils et leurs résultats, texte extrait des pièces jointes et tours précédents. Elle n'inclut jamais les invites système, les définitions d'outils, le contexte interne à Anthropic, le raisonnement caché de Claude ni les octets bruts des fichiers.

Un tour dont tous les blocs sont exclus est entièrement omis, ne supposez donc pas une alternance stricte entre utilisateur et assistant.

Les transcriptions sont envoyées sans troncature, de sorte qu'une longue conversation comportant des pièces jointes volumineuses produit un corps de requête volumineux. En pratique, la « context window » (fenêtre de contexte) du modèle maintient les corps sous environ 10 Mo, mais le protocole autorise jusqu'à 64 Mio. Plusieurs valeurs par défaut courantes sont bien plus petites, notamment `client_max_body_size` de nginx à 1 Mo et `express.json()` d'Express à 100 ko, et un corps rejeté compte comme un échec de webhook ; ainsi, avec la gestion des échecs **Allow the request**, un prompt surdimensionné atteindrait le modèle sans avoir été inspecté.

### Valeurs de source

`source.application` est une chaîne ouverte, et non une énumération fermée. Les valeurs courantes sont `claude-ai`, `claude-code` et `cowork` ; les [tests de connexion](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) et les [vérifications de rétablissement](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#circuit-breaker) automatiques du disjoncteur utilisent `config-test`. De nouvelles valeurs peuvent apparaître, et votre serveur ne doit pas rejeter une requête en raison d'une valeur qu'il ne reconnaît pas.

Traitez `source.application` comme une métadonnée de routage indicative, et non comme une frontière de confiance : ne faites pas reposer une décision de politique critique pour la sécurité sur elle seule.

## La trame d'appel d'outil

Lorsque Claude produit des appels d'outils, Anthropic envoie une trame d'appel d'outil qui les répertorie. Les appels à certains outils propres à claude.ai peuvent être omis, et une réponse dont tous les appels d'outils sont omis ne produit pas de trame d'appel d'outil ; consultez [Disponibilité](https://platform.claude.com/docs/fr/manage-claude/inference-hooks#availability). Un seul verdict couvre l'ensemble de la trame : vous ne pouvez pas autoriser certains appels d'outils et en refuser d'autres. La trame est envoyée au même point de terminaison que la trame de prompt, avec les mêmes en-têtes, la même signature et les mêmes champs de premier niveau. Les appels d'outils effectués par du code que Claude exécute dans l'outil d'exécution de code sont envoyés de la même manière, dans des trames d'appel d'outil distinctes, avant leur exécution.

Elle diffère de la trame de prompt sur trois points :

* `type` vaut `"tool_call"`.
* `messages` ne contient que le dernier message, le message `assistant` que Claude vient de produire : les éventuels blocs `text` et un bloc `tool_use` par appel d'outil répertorié dans la trame, dans l'ordre où le modèle les a produits. La conversation antérieure est omise, car la trame de prompt envoyée avant cet appel au modèle la contenait. Lisez la dernière entrée de `messages`, car le protocole pourra ultérieurement ajouter des messages antérieurs avant celle-ci.
* Chaque bloc `tool_use` comporte un objet `tool_info` qui indique qui exécute ou fournit l'outil.

Lorsque `session_id` est défini, il est identique sur les deux trames. La trame d'appel d'outil possède son propre `request_id`, opaque comme celui de la trame de prompt.

`tool_info` indique qui exécute ou fournit l'outil, et non ce à quoi l'outil peut accéder. Il appartient à l'une de quatre catégories, distinguées par son champ `type`, et chaque catégorie comporte ses propres champs. Anthropic envoie la première des catégories suivantes qui correspond à l'outil. De nouvelles catégories peuvent apparaître : acceptez un `type` que vous ne reconnaissez pas et, pour une telle catégorie, ne vous fiez qu'à `type`.

Un champ facultatif qui ne s'applique pas est omis, jamais `null` ; un `tool_info` peut donc se réduire à `{"type": "client"}`. `tool_name` est choisi par la personne qui a défini l'outil, et le `toolset_name` d'un serveur par la personne qui a rédigé la requête ; ne traitez donc ni l'un ni l'autre comme une frontière de confiance.

### Outils de plateforme

Un outil de plateforme est un outil que l'API Claude exécute elle-même lorsqu'elle traite la requête, comme la recherche web ou l'exécution de code.

| Champ          | Présence   | Description                                                                                                       |
| -------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- |
| `type`         | Toujours   | `"platform"`                                                                                                      |
| `tool_type`    | Toujours   | Le type versionné de l'outil, comme `web_search_20250305`. Comparez-le exactement ; n'en extrayez ni nom ni date. |
| `toolset_name` | Facultatif | Le groupe d'outils auquel il appartient.                                                                          |

### Outils d'application

Un outil d'application est un outil que l'application Anthropic à l'origine de la requête fournit elle-même, comme les outils propres à claude.ai.

| Champ          | Présence   | Description                              |
| -------------- | ---------- | ---------------------------------------- |
| `type`         | Toujours   | `"application"`                          |
| `toolset_name` | Facultatif | Le groupe d'outils auquel il appartient. |

Un bloc `tool_use` pour un outil de recherche web que claude.ai fournit lui-même :

```json
{
  "type": "tool_use",
  "id": "toolu_01LmNoPqRsTuVwXyZaBcDeFg",
  "tool_name": "web_search",
  "input": {
    "query": "comparable deals for unannounced 4.2B USD Acme Corp acquisition"
  },
  "tool_info": {
    "type": "application"
  }
}
```

claude.ai ne nomme pas toujours cet outil `web_search` ; ne vous basez donc pas uniquement sur le nom. `input` inclut toujours la requête de recherche et peut contenir d'autres champs.

### Outils tiers

Un outil tiers est un outil situé sur un serveur connu d'Anthropic, comme un connecteur claude.ai ou un serveur MCP que la requête nomme dans `mcp_servers`. Cela ne signifie pas qu'Anthropic a contrôlé le serveur.

| Champ             | Présence                             | Description                                                                                                                                                                           |
| ----------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`            | Toujours                             | `"third_party"`                                                                                                                                                                       |
| `toolset_name`    | Facultatif                           | Le nom que la requête donne au serveur.                                                                                                                                               |
| `origin`          | Facultatif                           | Le schéma, l'hôte et le port non par défaut de l'URL du serveur. Il peut s'agir d'une adresse privée ou locale, `localhost` compris. Un `origin` absent signifie inconnu, et non sûr. |
| `verified_origin` | Chaque fois que `origin` est présent | `true` uniquement lorsque les serveurs d'Anthropic se connectent eux-mêmes au serveur. Traitez `false` comme une origine qu'Anthropic n'a pas confirmée.                              |

Un bloc `tool_use` pour un outil situé sur un serveur MCP que la requête nomme `crm` dans `mcp_servers` :

```json
{
  "type": "tool_use",
  "id": "toolu_01GhIjKlMnOpQrStUvWxYzAb",
  "tool_name": "crm_search",
  "input": {
    "query": "accounts renewing in Q4"
  },
  "tool_info": {
    "type": "third_party",
    "toolset_name": "crm",
    "origin": "https://mcp.crm.example.com",
    "verified_origin": true
  }
}
```

### Outils client

Un outil client est tout autre outil, généralement un outil exécuté par l'application qui appelle Claude. Un outil défini par Anthropic mais exécuté par l'application appelante, comme bash ou l'utilisation de l'ordinateur, est également un outil client, et comporte son type versionné dans `tool_type`. Un appel à un outil que la requête ne déclare pas est également `"client"`.

Un outil situé sur un serveur MCP auquel Claude Code se connecte directement depuis l'ordinateur de l'utilisateur, comme un serveur MCP local, est un outil client. Son `tool_info` est `{"type": "client"}`, et son `tool_name` est le nom que Claude Code lui donne, sous la forme `mcp__<server>__<tool>`.

Un refus arrête l'appel avant que Claude Code ne l'exécute. L'échange entre Claude Code et un serveur local ne passe pas par Anthropic ; votre serveur de sécurité IA ne voit donc le résultat de l'outil que lorsque Claude Code le renvoie. Son texte figure alors dans un bloc `tool_result` de la trame de prompt suivante.

| Champ          | Présence   | Description                                                                                                                                   |
| -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`         | Toujours   | `"client"`                                                                                                                                    |
| `tool_type`    | Facultatif | Le type versionné de l'outil, comme `bash_20250124`, lorsqu'Anthropic définit l'outil. Comparez-le exactement ; n'en extrayez ni nom ni date. |
| `toolset_name` | Facultatif | Le groupe d'outils auquel il appartient, comme `browser`.                                                                                     |

Un bloc `tool_use` pour un outil que l'application appelante déclare :

```json
{
  "type": "tool_use",
  "id": "toolu_01AbCdEfGhIjKlMnOpQrStUv",
  "tool_name": "read_file",
  "input": {
    "path": "reports/q3.txt"
  },
  "tool_info": {
    "type": "client"
  }
}
```

Un bloc `tool_use` pour bash, qu'Anthropic définit et que l'application appelante exécute :

```json
{
  "type": "tool_use",
  "id": "toolu_01HiJkLmNoPqRsTuVwXyZaBc",
  "tool_name": "bash",
  "input": {
    "command": "ls -la reports/"
  },
  "tool_info": {
    "type": "client",
    "tool_type": "bash_20250124"
  }
}
```

## Renvoyer un verdict

Répondez avec HTTP 200 et un corps de verdict JSON dans les deux cas ; le champ `action` fait la distinction. Pour autoriser la requête :

```json
{
  "action": "allow"
}
```

Pour la refuser :

```json
{
  "action": "deny",
  "deny_reason": "This prompt appears to contain customer payment card data, which your organization's policy does not allow.",
  "reference_id": "scan_01HXPT4R9V"
}
```

| Champ          | Contraintes                                                                      | Sémantique                                                                                                                                                                                                                                                                                                                                       |
| -------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `action`       | `"allow"` ou `"deny"` ; obligatoire                                              | `allow` laisse l'inférence se poursuivre ; `deny` la rejette.                                                                                                                                                                                                                                                                                    |
| `deny_reason`  | string ou null ; au plus 500 caractères, les valeurs plus longues sont tronquées | Affiché à l'utilisateur final lorsque `action` vaut `deny` ; ignoré pour `allow`.                                                                                                                                                                                                                                                                |
| `reference_id` | string ou null ; au plus 50 caractères parmi `[A-Za-z0-9._:/-]`                  | Votre propre identifiant pour cette évaluation. Il est enregistré dans l'[activité de conformité](https://platform.claude.com/docs/fr/manage-claude/compliance-activity-feed) `inference_hooks_request_denied` du refus et n'est jamais affiché à l'utilisateur final. Gardez-le opaque : aucun contenu de requête ni aucune donnée personnelle. |

Un refus n'est jamais écarté pour un problème de formatage : un `deny_reason` trop long est tronqué, un `reference_id` mal formé est silencieusement supprimé, et l'`action` est tout de même honorée.

L'inverse n'est pas vrai. Toute réponse autre qu'un HTTP 200 avec un verdict analysable est un échec de webhook, et la [gestion des échecs](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) de votre organisation s'applique à la place d'un verdict. En particulier :

* Ne signalez pas un refus par un code d'erreur. Une réponse autre que 200 est un échec, pas un refus.
* Toute valeur d'`action` autre que `allow` ou `deny` est traitée comme un échec de webhook.

Anthropic lit au plus 64 Kio du corps de la réponse, et le corps doit être non compressé. Les redirections ne sont pas suivies et les cookies sont ignorés. Les champs inconnus dans le corps du verdict sont ignorés, vous pouvez donc renvoyer un objet plus riche en plus des champs documentés ici.

Une trame d'appel d'outil accepte le même corps de verdict, et chaque règle de cette section s'y applique sans changement. `allow` permet l'exécution des appels d'outils et la réponse se poursuit ; `deny` les arrête tous et termine la réponse avec la même erreur qu'un prompt refusé, y compris votre `deny_reason`. Le texte transmis avant le premier appel d'outil n'est pas retiré.

## Vérifier la signature

Les requêtes sont signées conformément à la spécification [Standard Webhooks](https://www.standardwebhooks.com/), à l'aide de trois en-têtes. Anthropic envoie les noms d'en-têtes en minuscules, et les proxys sont libres d'en modifier la casse, recherchez-les donc sans tenir compte de la casse.

| En-tête             | Contenu                                                                                                                                                                                                                                                                       |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook-id`        | Identifiant unique de cette livraison. Égal au `request_id` du corps. Utilisez-le comme clé d'idempotence et comme premier composant de la charge utile signée.                                                                                                               |
| `webhook-timestamp` | Heure Unix en secondes, sous forme de chaîne décimale, à laquelle la requête a été signée. Rejetez un horodatage s'écartant de plus de cinq minutes de l'horloge de votre serveur, dans un sens ou dans l'autre.                                                              |
| `webhook-signature` | Une ou plusieurs valeurs `v1,<base64>` séparées par des espaces, chacune étant un HMAC-SHA256 calculé sur `{webhook-id}.{webhook-timestamp}.{raw body bytes}`. Acceptez la requête si l'une des valeurs correspond à la vôtre, en utilisant une comparaison à temps constant. |

Deux détails sont à l'origine de la plupart des bogues de vérification :

* **Vérifiez les octets bruts.** Calculez le HMAC sur le corps exactement tel qu'il a été reçu, avant toute analyse JSON ou tout réencodage.
* **Décodez le secret avec un décodeur base64 standard.** Le secret de signature est la valeur située après le préfixe `whsec_`, encodée avec l'alphabet base64 standard (`+` et `/`), tout comme la signature dans l'en-tête. Un décodeur URL-safe dérive des octets de clé erronés dès que le secret contient `+` ou `/`, ce qui est le cas la plupart du temps.

Une fois que votre organisation dispose d'un secret de signature, chaque requête envoyée par Anthropic est signée, y compris le test de connexion, car le processus de configuration génère le secret avant le premier test. [L'activation des Inference hooks nécessite un secret](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) ; rejetez donc toute requête qui arrive sans signature. Une exception : une organisation ayant activé les Inference hooks avant que le secret ne devienne obligatoire continue d'envoyer des requêtes non signées jusqu'à ce que son administrateur en génère un. N'acceptez les requêtes non signées que jusqu'à ce que votre administrateur confirme l'existence du secret, puis rejetez-les.

[La rotation du secret](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration#rotate-your-signing-secret) est une bascule immédiate, mais des requêtes signées avec le secret précédent peuvent encore arriver pendant environ une minute après, en plus de tout ce qui est déjà en cours de transmission. Faites en sorte que votre serveur de sécurité IA accepte les signatures des deux secrets pendant la transition afin que ces requêtes tardives ne soient pas rejetées.

Les exemples suivants sont des implémentations de serveur, il n'y a donc pas d'onglet shell : un serveur de sécurité IA est un service HTTPS de longue durée plutôt qu'une requête ponctuelle. Chaque exemple n'utilise que la bibliothèque standard du langage ; le projet [Standard Webhooks](https://www.standardwebhooks.com/) publie également des bibliothèques de vérification pour la plupart des langages.

<CodeGroup exclude="shell">
  ```python Python
  import base64
  import hashlib
  import hmac
  import time

  TOLERANCE_SECONDS = 300


  def verify(secret: str, headers: dict[str, str], body: bytes) -> bool:
      """Return True if the body was signed by Anthropic for this organization.

      Anthropic sends header names in lowercase, but proxies are free to
      re-case them, so normalize the lookup to lowercase.
      """
      lowercased = {name.lower(): value for name, value in headers.items()}
      try:
          message_id = lowercased["webhook-id"]
          timestamp = lowercased["webhook-timestamp"]
          signatures = lowercased["webhook-signature"]
      except KeyError:
          return False  # unsigned request: not from Anthropic

      try:
          signed_at = int(timestamp)
      except ValueError:
          return False
      if abs(time.time() - signed_at) > TOLERANCE_SECONDS:
          return False  # replayed, or the clocks disagree

      try:
          key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
      except ValueError:
          return False  # misconfigured secret: reject rather than crash

      payload = f"{message_id}.{timestamp}.".encode() + body
      expected = b"v1," + base64.b64encode(
          hmac.new(key, payload, hashlib.sha256).digest()
      )

      # Comparer des octets : compare_digest sur str lève une exception en cas d'entrée non ASCII.
      return any(
          hmac.compare_digest(expected, candidate.encode())
          for candidate in signatures.split()
      )
  ```

  ```typescript TypeScript
  import { createHmac, timingSafeEqual } from "node:crypto";
  import type { IncomingHttpHeaders } from "node:http";

  const TOLERANCE_SECONDS = 300;

  /**
   * Returns true if the body was signed by Anthropic for this organization.
   *
   * Node lowercases incoming header names, matching how Anthropic sends
   * them, so look them up in lowercase.
   */
  export function verify(secret: string, headers: IncomingHttpHeaders, body: Buffer): boolean {
    const messageId = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];
    const signatures = headers["webhook-signature"];
    if (
      typeof messageId !== "string" ||
      typeof timestamp !== "string" ||
      typeof signatures !== "string"
    ) {
      return false; // unsigned request: not from Anthropic
    }

    const signedAt = Number(timestamp);
    if (
      !Number.isFinite(signedAt) ||
      Math.abs(Date.now() / 1000 - signedAt) > TOLERANCE_SECONDS
    ) {
      return false; // replayed, or the clocks disagree
    }

    const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const payload = Buffer.concat([Buffer.from(`${messageId}.${timestamp}.`), body]);
    const expected = Buffer.from(
      "v1," + createHmac("sha256", key).update(payload).digest("base64")
    );

    return signatures.split(" ").some((candidate) => {
      const candidateBytes = Buffer.from(candidate);
      return (
        candidateBytes.length === expected.length && timingSafeEqual(candidateBytes, expected)
      );
    });
  }
  ```

  ```csharp C#
  using System.Security.Cryptography;
  using System.Text;

  static class InferenceHooks
  {
      private const int ToleranceSeconds = 300;

      /// <summary>
      /// Renvoie true si le corps a été signé par Anthropic pour cette organisation.
      /// Anthropic envoie les noms d'en-têtes en minuscules, mais les proxys peuvent
      /// en modifier la casse ; comparez-les donc sans tenir compte de la casse.
      /// </summary>
      public static bool Verify(string secret, IReadOnlyDictionary<string, string> headers, byte[] body)
      {
          // TryAdd conserve la première valeur si un proxy a transmis des noms
          // dupliqués par la casse ; le constructeur de copie lèverait une exception.
          var lookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
          foreach (var (name, value) in headers)
          {
              lookup.TryAdd(name, value);
          }

          if (!lookup.TryGetValue("webhook-id", out var messageId) ||
              !lookup.TryGetValue("webhook-timestamp", out var timestamp) ||
              !lookup.TryGetValue("webhook-signature", out var signatures))
          {
              return false; // unsigned request: not from Anthropic
          }

          if (!long.TryParse(timestamp, out var signedAt) ||
              Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - signedAt) > ToleranceSeconds)
          {
              return false; // replayed, or the clocks disagree
          }

          // Alphabet base64 standard : un décodeur URL-safe produirait des octets de clé erronés.
          var encodedKey = secret.StartsWith("whsec_") ? secret["whsec_".Length..] : secret;
          byte[] key;
          try
          {
              key = Convert.FromBase64String(encodedKey);
          }
          catch (FormatException)
          {
              return false; // misconfigured secret: reject rather than crash
          }

          byte[] payload = [.. Encoding.UTF8.GetBytes($"{messageId}.{timestamp}."), .. body];
          var expected = Encoding.UTF8.GetBytes(
              "v1," + Convert.ToBase64String(HMACSHA256.HashData(key, payload)));

          // FixedTimeEquals est à temps constant et renvoie false si les longueurs diffèrent.
          return signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries).Any(candidate =>
              CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(candidate), expected));
      }
  }
  ```

  ```go Go
  package hooks

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/base64"
  	"net/http"
  	"strconv"
  	"strings"
  	"time"
  )

  const toleranceSeconds = 300

  // verify indique si body a été signé par Anthropic pour cette organisation.
  // net/http canonicalise les noms d'en-têtes à la recherche, donc les noms avec une casse modifiée correspondent toujours.
  func verify(secret string, header http.Header, body []byte) bool {
  	messageID := header.Get("webhook-id")
  	timestamp := header.Get("webhook-timestamp")
  	signatures := header.Get("webhook-signature")
  	if messageID == "" || timestamp == "" || signatures == "" {
  		return false // unsigned request: not from Anthropic
  	}

  	signedAt, err := strconv.ParseInt(timestamp, 10, 64)
  	if err != nil {
  		return false
  	}
  	age := time.Now().Unix() - signedAt
  	if age > toleranceSeconds || age < -toleranceSeconds {
  		return false // replayed, or the clocks disagree
  	}

  	// Alphabet base64 standard : un décodeur URL-safe dérive des octets de clé incorrects.
  	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
  	if err != nil {
  		return false
  	}

  	mac := hmac.New(sha256.New, key)
  	mac.Write([]byte(messageID + "." + timestamp + "."))
  	mac.Write(body)
  	expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))

  	for _, candidate := range strings.Fields(signatures) {
  		if hmac.Equal([]byte(candidate), []byte(expected)) { // constant-time
  			return true
  		}
  	}
  	return false
  }
  ```

  ```java Java
  import java.nio.charset.StandardCharsets;
  import java.security.GeneralSecurityException;
  import java.security.MessageDigest;
  import java.time.Instant;
  import java.util.Base64;
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.Map;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  public final class InferenceHookVerifier {
      private static final long TOLERANCE_SECONDS = 300;

      /**
       * Returns true if the body was signed by Anthropic for this organization.
       *
       * <p>Anthropic sends header names in lowercase, but proxies are free to
       * re-case them, so normalize the lookup to lowercase.
       */
      public static boolean verify(String secret, Map<String, String> headers, byte[] body) {
          Map<String, String> lowercased = new HashMap<>();
          headers.forEach((name, value) -> lowercased.put(name.toLowerCase(Locale.ROOT), value));

          String messageId = lowercased.get("webhook-id");
          String timestamp = lowercased.get("webhook-timestamp");
          String signatures = lowercased.get("webhook-signature");
          if (messageId == null || timestamp == null || signatures == null) {
              return false; // unsigned request: not from Anthropic
          }

          long signedAt;
          try {
              signedAt = Long.parseLong(timestamp);
          } catch (NumberFormatException _) {
              return false;
          }
          if (Math.abs(Instant.now().getEpochSecond() - signedAt) > TOLERANCE_SECONDS) {
              return false; // replayed, or the clocks disagree
          }

          // Alphabet base64 standard : un décodeur URL-safe dériverait des octets de clé incorrects.
          byte[] key;
          try {
              key = Base64.getDecoder().decode(
                      secret.startsWith("whsec_") ? secret.substring("whsec_".length()) : secret);
          } catch (IllegalArgumentException _) {
              return false; // misconfigured secret: reject rather than crash
          }

          byte[] expected;
          try {
              Mac mac = Mac.getInstance("HmacSHA256");
              mac.init(new SecretKeySpec(key, "HmacSHA256"));
              mac.update((messageId + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8));
              expected = ("v1," + Base64.getEncoder().encodeToString(mac.doFinal(body)))
                      .getBytes(StandardCharsets.UTF_8);
          } catch (GeneralSecurityException impossible) {
              // Toute JVM fournit HmacSHA256, donc ceci ne se déclenche jamais à l'exécution.
              throw new IllegalStateException(impossible);
          }

          for (String candidate : signatures.split(" ")) {
              if (MessageDigest.isEqual(candidate.getBytes(StandardCharsets.UTF_8), expected)) {
                  return true; // MessageDigest.isEqual is constant-time
              }
          }
          return false;
      }
  }
  ```

  ```php PHP
  const TOLERANCE_SECONDS = 300;

  /**
   * Returns true if the body was signed by Anthropic for this organization.
   *
   * Anthropic sends header names in lowercase, but proxies are free to
   * re-case them, so normalize the lookup to lowercase.
   */
  function verify(string $secret, array $headers, string $body): bool
  {
      $lowercased = array_change_key_case($headers, CASE_LOWER);
      $messageId = $lowercased['webhook-id'] ?? null;
      $timestamp = $lowercased['webhook-timestamp'] ?? null;
      $signatures = $lowercased['webhook-signature'] ?? null;
      if ($messageId === null || $timestamp === null || $signatures === null) {
          return false; // unsigned request: not from Anthropic
      }

      $signedAt = filter_var($timestamp, FILTER_VALIDATE_INT);
      if ($signedAt === false || abs(time() - $signedAt) > TOLERANCE_SECONDS) {
          return false; // replayed, or the clocks disagree
      }

      // Alphabet base64 standard : un décodeur URL-safe dériverait des octets de clé incorrects.
      $encodedKey = str_starts_with($secret, 'whsec_') ? substr($secret, strlen('whsec_')) : $secret;
      $key = base64_decode($encodedKey, strict: true);
      if ($key === false) {
          return false;
      }

      $payload = "{$messageId}.{$timestamp}." . $body;
      $expected = 'v1,' . base64_encode(hash_hmac('sha256', $payload, $key, binary: true));

      foreach (explode(' ', $signatures) as $candidate) {
          if (hash_equals($expected, $candidate)) { // constant-time
              return true;
          }
      }
      return false;
  }
  ```

  ```ruby Ruby
  # base64 est une gem intégrée dans Ruby 3.4 : les applis gérées par Bundler ajoutent gem "base64".
  require "base64"
  require "openssl"

  TOLERANCE_SECONDS = 300

  # Renvoie true si le corps a été signé par Anthropic pour cette organisation.
  #
  # Anthropic envoie les noms d'en-têtes en minuscules, mais les proxys peuvent
  # en modifier la casse ; normalisez donc la recherche en minuscules.
  def verify(secret, headers, body)
    lowercased = headers.transform_keys(&:downcase)
    message_id = lowercased["webhook-id"]
    timestamp = lowercased["webhook-timestamp"]
    signatures = lowercased["webhook-signature"]
    if message_id.nil? || timestamp.nil? || signatures.nil?
      return false # unsigned request: not from Anthropic
    end

    signed_at = Integer(timestamp, exception: false)
    if signed_at.nil? || (Time.now.to_i - signed_at).abs > TOLERANCE_SECONDS
      return false # replayed, or the clocks disagree
    end

    # Alphabet base64 standard : un décodeur URL-safe dérive des octets de clé erronés.
    begin
      key = Base64.strict_decode64(secret.delete_prefix("whsec_"))
    rescue ArgumentError
      return false # misconfigured secret: reject rather than crash
    end

    # Fournissez le corps séparément pour que son encodage n'ait jamais à correspondre à celui du préfixe.
    hmac = OpenSSL::HMAC.new(key, "SHA256")
    hmac.update("#{message_id}.#{timestamp}.")
    hmac.update(body)
    expected = "v1," + Base64.strict_encode64(hmac.digest)

    signatures.split(" ").any? do |candidate|
      # fixed_length_secure_compare lève une erreur si les longueurs diffèrent ; vérifiez-les d'abord.
      candidate.bytesize == expected.bytesize &&
        OpenSSL.fixed_length_secure_compare(candidate, expected)
    end
  end
  ```
</CodeGroup>

## Sémantique opérationnelle

### Délai d'expiration et nouvelle tentative

Votre administrateur définit un délai d'expiration du verdict compris entre 1 et 10 000 ms (5 000 ms par défaut). Ce budget couvre l'ensemble de l'échange : connexion, négociation TLS, requête et réponse. Le même délai d'expiration s'applique aux trames d'appel d'outil.

Anthropic effectue exactement une nouvelle tentative, après un délai de 100 ms, et uniquement lorsque la tentative de connexion échoue. La nouvelle tentative partage le même budget de délai d'expiration et porte le même `webhook-id` et la même signature. Une fois que votre serveur de sécurité IA a répondu, l'échange ne fait jamais l'objet d'une nouvelle tentative.

### Échecs de webhook

Les dépassements de délai, les codes autres que 200 (redirections comprises), les corps de réponse non analysables ou trop volumineux et les points de terminaison injoignables sont tous des échecs de webhook. Un échec de webhook ne devient jamais un refus ; à la place, le paramètre de [gestion des échecs](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration) de votre organisation détermine si la requête concernée est bloquée ou se poursuit sans inspection.

### Disjoncteur

Des échecs de webhook persistants imputables à votre serveur de sécurité IA déclenchent un « circuit breaker » (disjoncteur) qui interrompt l'application des règles : Anthropic cesse de contacter votre serveur, et la gestion des échecs s'applique à chaque requête.

À partir de 10 minutes après le déclenchement, Anthropic vérifie si votre serveur s'est rétabli : au plus environ une fois par minute, il envoie à votre serveur la même requête de test synthétique que celle envoyée par **Test connection** (`source.application` vaut `config-test`), signée comme toute autre requête et ne contenant aucun contenu utilisateur. Répondez-y normalement. Un verdict valide, d'autorisation ou de refus, réinitialise le disjoncteur et l'application des verdicts reprend ; un échec de webhook laisse le disjoncteur déclenché, et les vérifications se poursuivent. Un administrateur peut également réinitialiser le disjoncteur à tout moment, et les modifications de configuration effectuées par un administrateur interrompent les vérifications automatiques ; consultez [Disjoncteur](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration#circuit-breaker).

Les trames de prompt et les trames d'appel d'outil partagent un même disjoncteur, et les échecs sur l'une ou l'autre sont comptabilisés.

Chaque déclenchement est enregistré sous la forme d'une activité `inference_hooks_circuit_breaker_tripped` dans le [flux d'activité](https://platform.claude.com/docs/fr/manage-claude/compliance-activity-feed), une activité par déclenchement. Tant que le disjoncteur est déclenché, aucune activité Inference hooks par requête n'est enregistrée, de sorte que l'activité de déclenchement est la seule trace, dans le flux, de la période de déclenchement.

### Latence

L'application des verdicts ajoute l'aller-retour vers votre serveur de sécurité IA à la « latency » (latence) de chaque requête gouvernée de votre organisation. Lorsque **Validate tool calls** est activé, une réponse qui produit une trame d'appel d'outil attend également son verdict, y compris en mode shadow. Veillez à ce que le verdict soit rapide, et effectuez des tests de charge sur votre serveur avant de le déployer dans une grande organisation.

### Adresses IP sources

Les requêtes vers votre serveur de sécurité IA proviennent de `160.79.106.0/24`, qui fait partie des [plages d'adresses IP sortantes](https://platform.claude.com/docs/fr/api/ip-addresses) publiées par Anthropic. Ajoutez ce bloc à votre liste d'autorisation, et non les plages entrantes de la même page, qui ne le couvrent pas. La liste d'autorisation réduit l'exposition de votre serveur, mais elle ne remplace pas la vérification de signature : ce bloc transporte du trafic sortant d'Anthropic au-delà des Inference hooks.

## Compatibilité ascendante

Le protocole évolue sans casser les serveurs correctement écrits. Votre serveur doit ignorer :

* Les champs de premier niveau inconnus sur l'une ou l'autre trame.
* Les clés inconnues dans `metadata`.
* Les nouvelles valeurs de `source.application`.
* Les nouvelles valeurs de `actor.type`. `actor` est une union discriminée sur `type`, et `"user"` est le seul genre envoyé aujourd'hui ; un genre futur garantit uniquement la présence de `type`.
* Les blocs de contenu dont le `type` n'est pas reconnu.
* Les messages dont le `role` est autre que `user` ou `assistant`, qu'une révision ultérieure pourrait ajouter.

Ne rejetez jamais une requête en raison d'un type de bloc ou d'un champ non reconnu ; lisez les champs que vous connaissez et ignorez le reste.

D'autres types d'événements de hook seront introduits à l'avenir. Un nouveau type d'événement est un ajout que votre serveur ne peut pas gérer en ignorant un champ : la requête nécessite tout de même un verdict. Lorsque le `type` de premier niveau est une valeur que vous ne reconnaissez pas, renvoyez un verdict d'autorisation plutôt qu'un code d'erreur ; une réponse d'erreur est un [échec de webhook](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#webhook-failures), et des échecs persistants déclenchent le [disjoncteur](https://platform.claude.com/docs/fr/manage-claude/inference-hooks-endpoint#circuit-breaker).

## Concevoir votre intégration

Un serveur de sécurité IA de production fait quelques choix de conception au-delà du protocole de transmission.

**Dédupliquez sur `webhook-id`.** L'en-tête `webhook-id` est unique par livraison et égal au `request_id` du corps, et une nouvelle tentative après échec de connexion le réutilise, il fonctionne donc comme clé d'idempotence. Si vous enregistrez les verdicts, indexez les enregistrements sur cette clé.

**Enregistrez les verdicts et rapprochez les refus.** Stockez chaque verdict que vous renvoyez avec son `reference_id`. Chaque refus est enregistré sous la forme d'une activité de conformité `inference_hooks_request_denied` portant le `reference_id` renvoyé par votre serveur, ce qui vous permet de rapprocher les refus du [flux d'activité](https://platform.claude.com/docs/fr/manage-claude/compliance-activity-feed) des enregistrements correspondants dans votre propre système.

**Archivez avec un serveur qui autorise toujours.** Pour capturer les transcriptions en temps réel sans les contrôler, renvoyez `{"action": "allow"}` sans condition et conservez la trame après avoir répondu. Il s'agit d'une alternative en mode push à l'interrogation de l'[API Compliance](https://platform.claude.com/docs/fr/manage-claude/compliance-api), et répondre avant de conserver maintient votre aller-retour hors du chemin critique de l'utilisateur.

**Rédigez `deny_reason` pour l'utilisateur final.** Le texte que vous renvoyez est ce que l'utilisateur voit lorsque sa requête est bloquée, tronqué à 500 caractères. Indiquez-lui ce qu'il doit modifier, par exemple quel type de contenu supprimer, plutôt que d'émettre un code de scanner que seule votre équipe peut interpréter.

## Étapes suivantes

<CardGroup cols={2}>
  <Card title="Configurer les Inference hooks" href="https://platform.claude.com/docs/fr/manage-claude/inference-hooks-configuration">
    Activez les Inference hooks, connectez et testez votre point de terminaison, et contrôlez l'application des règles, la gestion des échecs et le déploiement.
  </Card>

  <Card title="Présentation des Inference hooks" href="https://platform.claude.com/docs/fr/manage-claude/inference-hooks">
    Ce que sont les Inference hooks, comment fonctionne l'aller-retour de verdict et quand les utiliser.
  </Card>
</CardGroup>
