Claude Platform Docs
AdministrationHooks d'inférence

Développer une intégration Inference hooks

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.

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. Pour savoir ce que sont les Inference hooks et quand les utiliser, consultez la présentation des 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), puis demandez à votre administrateur de le définir comme point de terminaison et de tester la connexion : le résultat de Test connection indique le verdict d'autorisation renvoyé par votre serveur.

# 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()

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 là où Anthropic peut l'atteindre : 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, répondant sans redirection. 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 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 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 :

En-têteValeur
Content-Typeapplication/json
User-Agentanthropic-dlp/1
Accept-Encodingidentity

Il existe aujourd'hui un seul événement de hook : la « prompt frame » (trame de prompt), envoyée une fois par requête d'inférence gouvernée, avant le début de l'inférence. Anthropic retient la requête jusqu'à ce 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 :

ChampTypeDescription
typestringL'événement de hook. Toujours "prompt" aujourd'hui ; d'autres types d'événements seront introduits à l'avenir, gérez donc une valeur non reconnue avec souplesse (voir Compatibilité ascendante).
request_idstringIdentifiant opaque par appel d'inférence, destiné à la corrélation. Égal à l'en-tête webhook-id.
tenant_idstring ou nullIdentifiant opaque de l'organisation à laquelle appartient la requête.
actorobjectLe 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.
sourceobjectL'application d'origine : application (voir Valeurs de source).
messagesarrayLa transcription de la conversation jusqu'au point d'inférence. Voir Blocs de contenu.
session_idstring ou nullIdentifiant 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.
modelstring ou nullIdentifiant public du modèle pour cette requête, lorsqu'il est disponible.
metadataobjectTable 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 :

{
  "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 blocChamps
texttext : le contenu textuel.
tool_useid : l'identifiant auquel le résultat d'outil correspondant fait référence. tool_name : le nom de l'outil. input : les arguments que le modèle a transmis à l'outil.
tool_resultcontent : 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.
attachmentfile_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.

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 avec de volumineuses pièces jointes produit un corps de requête volumineux, jusqu'à une limite supérieure de 10 Mo. Augmentez la limite de taille de corps de votre serveur pour accepter ce plafond. 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 (autoriser la requête), un prompt trop volumineux atteindrait le modèle sans inspection.

Valeurs de source

source.application est une chaîne ouverte, et non une énumération fermée. Les valeurs connues sont claude-ai et claude-code ; les tests de connexion utilisent config-test. De nouvelles valeurs peuvent apparaître, et votre serveur ne doit pas rejeter une requête à cause 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.

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 :

{
  "action": "allow"
}

Pour la refuser :

{
  "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"
}
ChampContraintesSémantique
action"allow" ou "deny" ; obligatoireallow laisse l'inférence se poursuivre ; deny la rejette.
deny_reasonstring ou null ; au plus 500 caractères, les valeurs plus longues sont tronquéesAffiché à l'utilisateur final lorsque action vaut deny ; ignoré pour allow.
reference_idstring 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é 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 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.

Vérifier la signature

Les requêtes sont signées conformément à la spécification Standard Webhooks, à 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êteContenu
webhook-idIdentifiant 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-timestampHeure 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-signatureUne 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, et l'activation des Inference hooks en exige un, rejetez donc toute requête qui arrive non signée. Une exception : un test de connexion envoyé avant le premier enregistrement de votre organisation arrive non signé, car le secret de signature n'existe pas encore. Acceptez les requêtes non signées jusqu'à ce que votre administrateur confirme que le secret existe, puis rejetez-les.

La rotation du 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 publie également des bibliothèques de vérification pour la plupart des langages.

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()
    )

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'intégralité de l'échange : connexion, négociation TLS, requête et réponse.

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 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 teste si votre serveur s'est rétabli : au plus environ une fois par minute, une requête, issue du propre trafic de votre organisation, est livrée à votre serveur pour inspection, signée et structurée comme n'importe quelle autre. Répondez-y normalement. Un verdict valide, autorisation ou refus, réinitialise le disjoncteur et l'application des règles reprend. Un échec de webhook laisse le disjoncteur déclenché, et les tests se poursuivent. Dans les deux cas, la requête de test elle-même se poursuit pour son utilisateur : son verdict n'est pas appliqué, et un test échoué ne la bloque pas, même avec Block the request (bloquer la requête). Un administrateur peut également réinitialiser le disjoncteur à tout moment, et les modifications de configuration par un administrateur arrêtent les tests automatiques ; voir Disjoncteur.

Chaque déclenchement est enregistré sous la forme d'une activité inference_hooks_circuit_breaker_tripped dans le flux d'activité, 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 règles ajoute l'aller-retour de votre serveur de sécurité IA à la « latency » (latence) de chaque requête gouvernée de votre organisation. Gardez le verdict 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 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 dans la trame de prompt.
  • 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.

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, et des échecs persistants déclenchent le disjoncteur.

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é 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, 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

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.

Ce que sont les Inference hooks, comment fonctionne l'aller-retour de verdict et quand les utiliser.

Was this page helpful?