Browse the docs

Monitor and integrate

After-call webhook

When a call ends, CallShift posts its outcome to your endpoint: who called, what was said, what the agent collected and how the call ended. Plug it into your CRM, or into an n8n or Make workflow.

Set it up

  1. Create the webhook

    POST/v1/webhooks with a name and your endpointUrl.

    JSON
    { "name": "CRM sync", "endpointUrl": "https://example.com/hooks/callshift?token=YOUR_SECRET" }
  2. Assign it to an agent

    PUT/v1/agents/{agentId}/webhook with {"enabled": true, "webhookId": "…"}. A webhook can serve many agents, and a duplicated agent keeps its 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" }
  }
}
  • The contact's metadata is under contact, not at the root: map contact.metadata.plan.
  • entity is null for a call of a shared agent and number. Route on entity.externalId, your own identifier, rather than on the name.
  • qualificationCriteria is keyed by the names of the agent's information to collect, and its values arrive as text: a boolean is "true" or "false".
  • callerNumber is the customer's number, whatever the direction: on an outbound call it equals to. A call from a browser has "web-call".
  • duration is in seconds, the same figure as the call's billingSeconds.
  • contact is left out when the number matches no contact, and recordingUrl can be empty.
  • phone and qualificationCriteria match 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 2xx at 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/calls on 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 afterwards

Manage webhooks

Deleting a webhook stops the deliveries of every agent that sends to it.