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-typeapplication/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.
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é.