---
title: "Webhooks"
url: "https://raconte.ai/fr/docs/webhooks"
---

### Documentation

[Introduction](/fr/docs)[Démarrer](/fr/docs/getting-started)[Serveur MCP](/fr/docs/mcp)[Webhooks](/fr/docs/webhooks)[API REST](/fr/docs/api)[CLI](/fr/docs/cli)[SDK](/fr/docs/sdk)[Skill agent](/fr/docs/skill)

### Guides

[Configurer le serveur MCP](/fr/guides/mcp-setup)[Utiliser le serveur MCP](/fr/guides/mcp-usage)

[Raconte](/) ⟩ [Documentation](/fr/docs)

# Webhooks

Recevoir une requête POST au démarrage et à la fin de chaque interview.

Raconte émet des webhooks HTTP pour deux événements par organisation : `interview.started` et `interview.completed`. La configuration se fait depuis [Paramètres → Webhooks](/settings/webhook) dans l’app.

## Comment fonctionne l’envoi

*   Une seule URL par organisation reçoit tous les événements. Pas de routage par événement.
*   La méthode est toujours `POST`, content-type `application/json`.
*   L’envoi est best-effort. Raconte ne réessaie pas en cas d’échec : le code de réponse est journalisé, point final.
*   Le timeout est de 5 secondes. Un endpoint lent reçoit une requête interrompue.

En-têtes envoyés par Raconte :

En-tête

Valeur

`user-agent`

`Raconte-Webhook/1.0`

`x-raconte-event`

Le nom de l’événement, p. ex. `interview.completed`

`x-raconte-signature`

`sha256=<hex>` (uniquement quand un secret est défini ; voir plus bas)

L’enveloppe du payload est toujours la même :

```
{
  "event": "interview.completed",
  "createdAt": "2026-05-29T10:14:22.123Z",
  "data": { /* champs spécifiques à l'événement */ }
}
```

## Vérifier la signature

Quand un secret de signature est configuré, chaque requête porte un en-tête `x-raconte-signature` calculé ainsi :

```
HMAC-SHA256(secret, corps_brut_de_la_requête)
```

Vérifiez côté serveur en recalculant le HMAC avec votre secret et les octets **bruts** du corps (avant tout JSON parse), puis comparez en temps constant. Une signature qui ne correspond pas doit être traitée comme un 401.

Exemple en Node.js + Express :

```
import { createHmac, timingSafeEqual } from 'node:crypto'
import express, { Request, Response } from 'express'

const SECRET = process.env.RACONTE_WEBHOOK_SECRET!

function verifyRaconteSignature(rawBody: Buffer, header: string | undefined): boolean {
  if (!header || !header.startsWith('sha256=')) return false
  const expected = createHmac('sha256', SECRET).update(rawBody).digest('hex')
  const provided = header.slice('sha256='.length)
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(provided, 'hex')
  // Contrôle de longueur d'abord : timingSafeEqual lève une erreur sur des entrées de tailles différentes.
  if (a.length !== b.length) return false
  return timingSafeEqual(a, b)
}

const app = express()

// IMPORTANT : conservez les octets bruts. JSON.parse(rawBody.toString()) est
// très bien pour traiter le payload, mais le HMAC doit être calculé sur les
// octets exacts reçus.
app.post(
  '/webhooks/raconte',
  express.raw({ type: 'application/json' }),
  (req: Request, res: Response) => {
    const signature = req.header('x-raconte-signature')
    if (!verifyRaconteSignature(req.body as Buffer, signature)) {
      return res.status(401).send('invalid signature')
    }
    const payload = JSON.parse((req.body as Buffer).toString('utf8'))
    // payload.event vaut 'interview.started' ou 'interview.completed'
    // payload.data contient les champs spécifiques à l'événement (voir plus bas).
    res.status(204).end()
  },
)
```

Générez (ou régénérez) le secret depuis **Paramètres → Webhooks**. La régénération invalide l’ancien secret immédiatement ; tout serveur configuré avec l’ancienne valeur se mettra à rejeter les requêtes.

## interview.started

Émis la première fois qu’un invité envoie un message à l’IA sur une invitation donnée (l’appel quitte READY pour IN\_PROGRESS).

Payload `data` :

Champ

Type

Description

`id`

uuid

Id de l’interview

`title`

string | null

Titre de l’interview

`locale`

string

Langue de l’interview

`invitation`

objet

L’invitation précise qui a démarré (voir les champs ci-dessous)

`startedAt`

chaîne ISO 8601

Quand IN\_PROGRESS a été atteint

L’objet `invitation` contient `id`, `slug`, `status`, `name` (nom de l’invité, ou `null`), `recipientEmail` et `recipientPhone`.

Exemple d’enveloppe :

```
{
  "event": "interview.started",
  "createdAt": "2026-05-29T10:14:22.123Z",
  "data": {
    "id": "demo_interview_id",
    "title": "Sample interview",
    "locale": "en",
    "invitation": {
      "id": "demo_invitation_id",
      "slug": "demo-slug",
      "status": "in_progress",
      "name": "Jane Doe",
      "recipientEmail": "jane.doe@example.com",
      "recipientPhone": null
    },
    "startedAt": "2026-05-29T10:14:22.000Z"
  }
}
```

## interview.completed

Émis lorsque l’invité termine l’appel (le statut passe à COMPLETED). Le webhook attend la fin de la passe d’insights de l’IA pour que le résumé et les sentiments par message soient inclus quand ils sont prêts. Un échec d’insights ne bloque pas l’envoi : dans ce cas `summary` est `null` et les `sentiment` des messages sont `null`.

Payload `data` :

Champ

Type

Description

`id`

uuid

Id de l’interview

`title`

string | null

Titre de l’interview

`locale`

string

Langue de l’interview

`invitation`

objet

L’invitation précise qui s’est terminée (voir les champs ci-dessous)

`durationSeconds`

number

Durée totale des segments audio enregistrés de l’entretien, en secondes

`completedAt`

chaîne ISO 8601

Quand COMPLETED a été atteint

`summary`

string | null

Résumé généré de ce que le répondant a dit (quand disponible)

`messages`

tableau de `{role, content, createdAt, sentiment}`

Transcript complet, chronologique

L’objet `invitation` contient `id`, `slug`, `status`, `name` (nom de l’invité, ou `null`), `recipientEmail` et `recipientPhone`.

Exemple d’enveloppe :

```
{
  "event": "interview.completed",
  "createdAt": "2026-05-29T10:21:07.541Z",
  "data": {
    "id": "demo_interview_id",
    "title": "Sample interview",
    "locale": "en",
    "invitation": {
      "id": "demo_invitation_id",
      "slug": "demo-slug",
      "status": "completed",
      "name": "Jane Doe",
      "recipientEmail": "jane.doe@example.com",
      "recipientPhone": null
    },
    "durationSeconds": 184,
    "completedAt": "2026-05-29T10:21:07.000Z",
    "summary": "Jane appreciated the onboarding flow but flagged the pricing page as confusing.",
    "messages": [
      {
        "role": "assistant",
        "content": "Hi Jane, thanks for taking the time. How was your first week with the product?",
        "createdAt": "2026-05-29T10:18:07.000Z",
        "sentiment": null
      },
      {
        "role": "user",
        "content": "Onboarding felt smooth, but I got stuck on the pricing page.",
        "createdAt": "2026-05-29T10:19:07.000Z",
        "sentiment": "neutral"
      }
    ]
  }
}
```

## Tester votre endpoint

Depuis **Paramètres → Webhooks**, le bouton **Envoyer un test** envoie un payload de test pour l’événement sélectionné vers l’URL configurée. L’enveloppe de test ajoute `"test": true` dans le bloc `data` pour que vous puissiez le détecter en développement.

**ℹ️ 2xx == livré**

Tout code de réponse entre `200` et `299` compte comme livré. Tout le reste est journalisé côté Raconte mais l’événement n’est pas réessayé.

Sommaire

[1\. Comment fonctionne l’envoi](#comment-fonctionne-lenvoi)[2\. Vérifier la signature](#vérifier-la-signature)[3\. interview.started](#interviewstarted)[4\. interview.completed](#interviewcompleted)[5\. Tester votre endpoint](#tester-votre-endpoint)
