Parcourir la documentation

Suivre et intégrer

Webhook de fin d'appel

À la fin d'un appel, CallShift envoie son résultat à votre endpoint : qui a appelé, ce qui s'est dit, ce que l'agent a collecté et comment l'appel s'est terminé. Branchez-le sur votre CRM, ou sur un workflow n8n ou Make.

Configuration

  1. Créer le webhook

    POST/v1/webhooks avec un name et votre endpointUrl.

    JSON
    { "name": "CRM sync", "endpointUrl": "https://example.com/hooks/callshift?token=YOUR_SECRET" }
  2. L'attribuer à un agent

    PUT/v1/agents/{agentId}/webhook avec {"enabled": true, "webhookId": "…"}. Un webhook peut servir plusieurs agents, et un agent dupliqué conserve son webhook.

Payload

JSON
{
  "callId": "8f14e45f-ceea-467a-9575-6d3c0a5f2b1e",
  "type": "outbound",
  "status": "COMPLETED",
  "createdAt": "2026-10-30T09:11:58.000Z",
  "startedAt": "2026-10-30T09:12:00.000Z",
  "endedAt": "2026-10-30T09:13:24.000Z",
  "duration": 84,
  "from": "+33187654321",
  "to": "+33612345678",
  "callerNumber": "+33612345678",
  "campaignId": "3b241101-e2bb-4255-8caf-4136c566a962",
  "endedReason": "customer-ended-call",
  "isReached": true,
  "isSignificant": true,
  "isSuccessful": true,
  "isSilent": false,
  "summary": "Ada booked a visit for Saturday at 10am.",
  "transcript": "Assistant: Hello, this is Alice...\nUser: Hi, I'd like to book a visit...",
  "recordingUrl": "https://recordings.example.com/recordings/8f14e45f.ogg",
  "agent": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Alice" },
  "phone": { "id": "0d3a7e6b-2b8e-4c5b-9c4e-5f4b9d2a1c8e", "number": "+33187654321" },
  "entity": { "id": "9b2d3c4e-5f60-4a1b-8c2d-3e4f5a6b7c8d", "externalId": "club-42", "name": "Lyon Part-Dieu" },
  "qualificationCriteria": { "Visit booked": "true", "Preferred day": "Weekend" },
  "contact": {
    "id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com",
    "phoneNumber": "+33612345678",
    "information": "VIP customer",
    "metadata": { "plan": "pro" }
  }
}
  • Les metadata du contact sont sous contact, pas à la racine : mappez contact.metadata.plan.
  • entity vaut null pour un appel d'un agent et d'un numéro partagés. Routez en fonction de entity.externalId, votre propre identifiant, plutôt que sur le nom.
  • qualificationCriteria est indexé par les noms des informations à collecter de l'agent, et ses valeurs arrivent sous forme de texte : un boolean vaut "true" ou "false".
  • callerNumber est le numéro du client, quel que soit le sens de l'appel : sur un appel sortant, il est égal à to. Pour un appel depuis un navigateur, il vaut "web-call".
  • duration est en secondes, la même valeur que le billingSeconds de l'appel.
  • contact est omis quand le numéro ne correspond à aucun contact, et recordingUrl peut être vide.
  • phone et qualificationCriteria correspondent aux mêmes champs de GET/v1/calls/{callId}.

Envoi

CallShift envoie un seul POST avec un corps JSON à la fin de l'appel, attend une réponse pendant 10 secondes, et ne réessaie pas. Donc :

  • répondez 2xx immédiatement et faites le traitement ensuite, dans une file d'attente ;
  • rendez le traitement idempotent en vous appuyant sur callId ;
  • rattrapez ce que vous avez manqué en interrogeant GET/v1/calls à intervalles réguliers.

Chaque envoi, abouti ou en échec, apparaît dans les logs du tableau de bord avec la réponse de votre endpoint. Aucun webhook n'est envoyé pour un appel qui n'a jamais commencé : appels entrants refusés (REJECTED) et appels de campagne non composés.

Sécuriser votre endpoint

Les envois ne sont pas encore signés. Mettez un long jeton aléatoire dans l'URL de l'endpoint et refusez les requêtes qui ne le contiennent pas. Avant de donner suite à un payload important (un remboursement, une réservation), récupérez de nouveau l'appel avec GET/v1/calls/{callId} et votre clé API.

import express from "express";

const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/hooks/callshift", (req, res) => {
  if (req.query.token !== process.env.CALLSHIFT_HOOK_TOKEN) return res.sendStatus(401);
  res.sendStatus(204); // answer within 10 seconds
  queue.add("callshift-call", req.body, { jobId: req.body.callId }); // idempotent on callId
});

app.listen(3000);
import os
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request

app = FastAPI()

@app.post("/hooks/callshift", status_code=204)
async def callshift_hook(request: Request, tasks: BackgroundTasks, token: str = ""):
    if token != os.environ["CALLSHIFT_HOOK_TOKEN"]:
        raise HTTPException(status_code=401)
    payload = await request.json()
    tasks.add_task(process_call, payload)  # answer first, work afterwards

Gérer les webhooks

Supprimer un webhook arrête les envois de tous les agents qui l'utilisent.