Monitor and integrate
After-call webhook
Set it up
Create the webhook
POST
/v1/webhookswith anameand yourendpointUrl.{ "name": "CRM sync", "endpointUrl": "https://example.com/hooks/callshift?token=YOUR_SECRET" }Assign it to an agent
PUT
/v1/agents/{agentId}/webhookwith{"enabled": true, "webhookId": "…"}. A webhook can serve many agents, and a duplicated agent keeps its 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" }
}
}- The contact's
metadatais undercontact, not at the root: mapcontact.metadata.plan. entityisnullfor a call of a shared agent and number. Route onentity.externalId, your own identifier, rather than on the name.qualificationCriteriais keyed by the names of the agent's information to collect, and its values arrive as text: abooleanis"true"or"false".callerNumberis the customer's number, whatever the direction: on an outbound call it equalsto. A call from a browser has"web-call".durationis in seconds, the same figure as the call'sbillingSeconds.contactis left out when the number matches no contact, andrecordingUrlcan be empty.phoneandqualificationCriteriamatch the same fields of GET/v1/calls/{callId}.
Delivery
CallShift sends one POST with a JSON body when the call ends, waits 10 seconds for an answer, and does not retry. So:
- answer
2xxat once and do the work afterwards, in a queue; - make the processing idempotent on
callId; - catch up on anything you missed by listing GET
/v1/callson a schedule.
Every delivery, answered or failed, shows in the logs of the dashboard with your endpoint's answer. No webhook is sent for a call that never started: refused inbound calls (REJECTED) and campaign calls that were not dialed.
Secure your endpoint
Deliveries are not signed yet. Put a long random token in the endpoint URL and refuse requests without it. Before acting on a payload that matters (a refund, a booking), read the call again with GET/v1/calls/{callId} and your API key.
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 afterwardsManage webhooks
/v1/webhooksGET/v1/webhooks/{webhookId}PATCH/v1/webhooks/{webhookId}DELETE/v1/webhooks/{webhookId}Deleting a webhook stops the deliveries of every agent that sends to it.
