Suivre et intégrer
Webhook de fin d'appel
Configuration
Créer le webhook
POST
/v1/webhooksavec unnameet votreendpointUrl.{ "name": "CRM sync", "endpointUrl": "https://example.com/hooks/callshift?token=YOUR_SECRET" }L'attribuer à un agent
PUT
/v1/agents/{agentId}/webhookavec{"enabled": true, "webhookId": "…"}. Un webhook peut servir plusieurs agents, et un agent dupliqué conserve son webhook.
Payload
{
"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
metadatadu contact sont souscontact, pas à la racine : mappezcontact.metadata.plan. entityvautnullpour un appel d'un agent et d'un numéro partagés. Routez en fonction deentity.externalId, votre propre identifiant, plutôt que sur le nom.qualificationCriteriaest indexé par les noms des informations à collecter de l'agent, et ses valeurs arrivent sous forme de texte : unbooleanvaut"true"ou"false".callerNumberest 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".durationest en secondes, la même valeur que lebillingSecondsde l'appel.contactest omis quand le numéro ne correspond à aucun contact, etrecordingUrlpeut être vide.phoneetqualificationCriteriacorrespondent 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
2xximmé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 afterwardsGérer les webhooks
/v1/webhooksGET/v1/webhooks/{webhookId}PATCH/v1/webhooks/{webhookId}DELETE/v1/webhooks/{webhookId}Supprimer un webhook arrête les envois de tous les agents qui l'utilisent.
