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 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êteValeur
user-agentRaconte-Webhook/1.0
x-raconte-eventLe nom de l’événement, p. ex. interview.completed
x-raconte-signaturesha256=<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 :

ChampTypeDescription
iduuidId de l’interview
titlestring | nullTitre de l’interview
localestringLangue de l’interview
invitationobjetL’invitation précise qui a démarré (voir les champs ci-dessous)
startedAtchaîne ISO 8601Quand 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 :

ChampTypeDescription
iduuidId de l’interview
titlestring | nullTitre de l’interview
localestringLangue de l’interview
invitationobjetL’invitation précise qui s’est terminée (voir les champs ci-dessous)
durationSecondsnumberDurée totale des segments audio enregistrés de l’entretien, en secondes
completedAtchaîne ISO 8601Quand COMPLETED a été atteint
summarystring | nullRésumé généré de ce que le répondant a dit (quand disponible)
messagestableau 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é.