Browse the docs

Monitor and integrate

Connect n8n or Make

Most integrations need no code on your side: CallShift posts every call to an n8n or Make workflow, which writes it to your CRM, a sheet or your ticketing tool. This guide takes a workflow from its first node to a tested integration, then the other way round, from your tools to a campaign.

How it works

CallShift pushes, your workflow listens. When a call ends, CallShift posts a JSON body with everything about the call (summary, transcript, outcome, collected information, recording) to the address of your workflow. Your workflow has nothing to poll.

Build your workflow around these rules:

RuleValueSo your workflow
EventEnd of call onlyGets nothing when a call starts or a transfer begins
FormatPOST, Content-Type: application/jsonStarts with a webhook trigger that accepts POST
Answer time10 seconds at mostAnswers 200 at once and does the work afterwards
RetriesNoneCatches up on undelivered calls through the API on a schedule
SignatureNoneChecks a secret token in its address
AssignmentOne webhook per agentCan receive the calls of all your agents at one address

The after-call webhook guide has the details of the delivery.

Set it up in four steps

  1. Get the address of your workflow

    The production address of an n8n Webhook node, or the address of a Make custom webhook (the next two sections). It must be public and in HTTPS: CallShift posts from the cloud.

  2. Create the webhook

    In the dashboard, at app.callshift.ai/webhooks: a name (n8n CRM sync) and the address. Or through the API, with POST/v1/webhooks.

  3. Assign it to the agent

    On the agent's page, tab After-Call Automations: turn on End of call event, pick the webhook in Select Webhook endpoint, then Save. Or through the API, with PUT/v1/agents/{agentId}/webhook. Do it for each agent; a duplicated agent keeps the webhook of the agent it was copied from.

  4. Call the agent

    Call a number the agent answers, then check that the workflow received the call (Test it end to end).

In n8n

Start the workflow with a Webhook node:

SettingValue
HTTP MethodPOST
PathKeep the random one n8n suggests: it is hard to guess
RespondImmediately, so that CallShift gets its 200 within 10 seconds whatever the workflow does next
  • Give CallShift the production address, /webhook/..., and activate the workflow. The test address, /webhook-test/..., only listens while the editor waits for a test event, for one call.
  • Add ?token= and a long random value to the address, and put an If node right after the webhook: {{ $json.query.token }} must equal your token, and the workflow stops otherwise.
  • The call is under $json.body: {{ $json.body.callId }}, {{ $json.body.summary }}.
  • To build the workflow before the first real call, paste the example payload below as the Webhook node's pinned data (Pin data).

In Make

Start the scenario with the Webhooks app, Custom webhook trigger, and create a webhook: Make gives you a public HTTPS address at once.

  • The address is long and random: keep it secret, it plays the role of the token.
  • To let Make learn the structure of a call, click Redetermine data structure, then send it the example payload below, saved as callshift-payload.json:
Terminal
curl -X POST "https://hook.eu1.make.com/<your-webhook-address>" \
  -H "Content-Type: application/json" \
  -d @callshift-payload.json
  • Turn the scenario on, scheduled Immediately. Make answers 200 on receipt; if you add a Webhook response module, keep it before any slow module.
  • The fields are mapped as {{1.callId}}, {{1.contact.metadata.plan}}, where 1 is the number of the webhook module.

Make a self-hosted n8n reachable

A Make address and an n8n Cloud address work as they are. A self-hosted n8n often answers on an internal address that CallShift, in the cloud, cannot reach:

In the addressWhat goes wrongWhat to do
n8n.company.localA name only your network knowsPublish a public DNS name, such as a subdomain for webhooks
:5678n8n's own port, closed to the outsidePut n8n behind a reverse proxy on port 443
http://Not encrypted, while the payload carries transcripts and phone numbersHTTPS with a valid certificate
/webhook-test/Listens only during a test in the editorThe production address, /webhook/, with the workflow active

Three common ways to expose it, depending on your network policy: a reverse proxy you already run (nginx, Traefik, HAProxy, Caddy), an outbound tunnel that opens no inbound port (such as Cloudflare Tunnel), or n8n Cloud. Expose the /webhook/ path only, not the editor, and set n8n's WEBHOOK_URL variable to the public address. If your policy requires a list of source IP addresses, write to support@callshift.ai.

Check the address the way CallShift sees it: from a computer outside your network and without a VPN, a phone's connection sharing for instance. The name must resolve to a public IP address:

Terminal
nslookup n8n.example.com 8.8.8.8

And the address must answer a POST in under 10 seconds:

Terminal
curl -i -X POST "https://n8n.example.com/webhook/<path>?token=<your-token>" \
  -H "Content-Type: application/json" \
  -d '{"test": true}' \
  -w "\nTotal time: %{time_total} s\n"
ResultMeaningWhat to do
200, an execution in n8n, under 10 sReadyTest it end to end
Could not resolve hostThe name is not publicPublish the DNS name
Timeout, Connection refusedPort closed or firewallOpen 443 and route it to the reverse proxy
SSL certificate problemInvalid or self-signed certificateA recognised certificate, such as Let's Encrypt
404, webhook not registeredWorkflow inactive, or test addressActivate the workflow, use /webhook/
Over 10 sThe workflow answers at its endSet the Webhook node to respond Immediately

The fields of a call

An example of what your workflow receives. The values are made up: check the structure on your first real call.

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" }
  }
}
FieldTypeWhenWhat it holds
callIdUUIDAlwaysThe call's id: the key to remove duplicates and to catch up
typetextAlwaysinbound, outbound or webcall
statustextAlwaysCOMPLETED for a call that ended normally
createdAt, startedAt, endedAtISO 8601 date, UTCcreatedAt always; the other two null when unknownWhen the call was created, started and ended
durationintegerWhen the call has its datesSeconds, the same figure as the call's billingSeconds
from, totextUsuallyOutbound: your line, then the customer. Inbound: the customer, then your line
callerNumbertextAlwaysThe customer's number, whatever the direction; "web-call" for a call from a browser
campaignIdUUID or nullCampaign callsThe campaign that placed the call
endedReasontextWhen the call has oneWhy it ended: see Ended reasons
isReached, isSignificant, isSilent, isSuccessfulbooleanOnce the call is analysedThe outcome: see Call analysis
summarytextOnce the call is analysedThe summary, empty when nobody spoke
transcripttext or nullWhen something was saidOne line per turn, starting with User: or Assistant:
recordingUrlURL or nullAgents on the current voice engineA direct link to the recording; can be empty
agent{id, name}AlwaysThe agent of the call
phone{id, number} or nullAlwaysYour CallShift line
entity{id, externalId, name} or nullAlwaysThe call's entity, null when shared: see Entities
qualificationCriteriaobjectOnce the call is analysedOne key per criterion of the agent, its value as text
contactobjectWhen the number matches a contactid, phoneNumber, firstName, lastName, email, information, metadata

A field with no value is left out of the body: map it as optional.

Four mapping traps

  • The criteria's keys are their exact names, accents and capitals included, and each agent has its own. Write them down from the agent's settings, and treat a name as frozen once a workflow reads it: renaming it empties the mapping without an error.
  • The criteria's values are text. A yes or no arrives as "true" or "false": compare it with the text, never with a boolean. In n8n: {{ $json.body.qualificationCriteria["Visit booked"] === "true" }}; in Make, a filter with the text operator Equal to true.
  • The metadata is under the contact: contact.metadata.plan, not metadata.plan. A wrong path gives an empty value, without an error.
  • The dates are in UTC. Convert them to your time zone before writing them anywhere a person reads them. In n8n: {{ DateTime.fromISO($json.body.endedAt).setZone("Europe/Paris").toFormat("yyyy-MM-dd HH:mm") }}; in Make, formatDate with the time zone.

Test it end to end

No simulator is needed: a real call to a number the agent answers sends the webhook, a few seconds after hanging up (the time to write the summary and extract the information). Before calling, check that the address answers (the checks above), that the workflow is active on its production address, and that the webhook is assigned to the agent.

Call to makeWhat the workflow should get
A complete conversation, up to the agent's objectiveisSuccessful true, a clear summary, every expected criterion
A call the agent should transfer, if it transfersendedReason assistant-forwarded-call
Hang up after two secondsisSignificant false, an almost empty transcript: the workflow must not fail on it

Then check:

  • callId is there, and different on each call;
  • callerNumber is the number you called from;
  • transcript and summary match the conversation;
  • every criterion your workflow reads is there, with the name you mapped;
  • recordingUrl opens the recording;
  • the dates show in your time zone in your tools.
SymptomLikely causeWhat to do
No execution in n8n or MakeAddress unreachable, or workflow inactiveRun the address checks above again
Still nothingWebhook not assigned to this agent, or not savedCheck the agent's After-Call Automations tab, then write to support@callshift.ai with the time of the call
An execution that failsA mapping on a wrong pathCompare it with the raw body the execution received

Every delivery, answered or failed, also shows in the logs of the dashboard, with your workflow's answer.

Catch up on undelivered calls

A delivery that fails is not sent again. A second workflow, on a schedule (every 30 minutes, every hour), reads the calls back through the API and adds the ones your destination does not have:

  1. a Schedule Trigger node in n8n, or a scheduled scenario in Make;
  2. an HTTP request to GET/v1/calls, filtered by agent and by period (from and to, full ISO dates on the creation time), with your API key in an Authorization: Bearer header stored in the tool's credentials, and the pages read up to pageCount (see Pagination);
  3. a check of each callId against what your destination already holds, to add only the missing ones;
  4. for the full transcript of a call, GET/v1/calls/{callId}.
Terminal
curl "https://api.callshift.ai/v1/calls?agentId=$AGENT_ID&from=2026-10-30T08:00:00Z&to=2026-10-30T09:00:00Z&page=1&limit=100" \
  -H "Authorization: Bearer $CALLSHIFT_API_KEY"

Create a key for the workflow alone at app.callshift.ai/api-keys, and keep it in the tool's credentials, never in clear in a workflow you share.

Send contacts into a campaign

The other way round: your workflow adds contacts to a campaign, which calls them. Send them together, in one request to POST/v1/campaigns/{campaignId}/contacts, rather than one request per contact.

  • Normalise the numbers to E.164 (+33612345678) before sending them: CallShift matches contacts by number and does not normalise them for you.
  • Leave out an email you are not sure of. An email that is present but empty or malformed refuses the whole request with a 400; keep doubtful values in metadata, which takes any JSON.
  • An active campaign dials at once. Send a test with one or two internal numbers first.

In n8n, a Code node can prepare each contact, here for French numbers:

n8n Code node
// Mode "Run Once for Each Item": one contact per item, French numbers to E.164.
let phone = String($json.phone ?? "").replace(/[^\d+]/g, "");
if (phone.startsWith("00")) phone = "+" + phone.slice(2);
else if (phone.startsWith("0")) phone = "+33" + phone.slice(1);

const email = String($json.email ?? "").trim();
const validEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);

return {
  json: {
    phoneNumber: phone,
    firstName: $json.firstName,
    lastName: $json.lastName,
    ...(validEmail ? { email } : {}),
    metadata: { source: "n8n", ...(email && !validEmail ? { rawEmail: email } : {}) },
  },
};

Then an Aggregate node gathers them into one contacts list, and an HTTP Request node posts it:

cURL
curl -X POST "https://api.callshift.ai/v1/campaigns/$CAMPAIGN_ID/contacts" \
  -H "Authorization: Bearer $CALLSHIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phoneNumber": "+33612345678", "firstName": "Ada", "metadata": { "source": "n8n" } },
      { "phoneNumber": "+33687654321", "firstName": "Louis", "email": "louis@example.com", "metadata": { "source": "n8n" } }
    ]
  }'

Let an AI assistant build the workflow

Three prompts to paste into Claude, ChatGPT or Gemini. Each one carries what the assistant needs to know about CallShift: replace what is between brackets.

Build the receiving workflow

Prompt to copy
You are an expert in [n8n / Make]. Help me build a workflow that receives the end-of-call webhooks of the CallShift voice platform and sends them to [TARGET: our CRM, a Google Sheet, our ticketing tool].

CallShift's constraints:
- One JSON POST per ended call, sent once, never retried, with a 10-second timeout.
- No signature: the address carries a ?token= parameter the workflow must check first, rejecting the call when it is missing or wrong.
- Answer 200 at once and do the work afterwards.
- Remove duplicates on callId.

The body: at the root callId, type, status, createdAt, startedAt, endedAt (ISO 8601 in UTC), duration (seconds), from, to, callerNumber, campaignId, endedReason, isReached, isSignificant, isSilent, isSuccessful, summary, transcript, recordingUrl, agent {id, name}, phone {id, number}, entity {id, externalId, name} or null. qualificationCriteria is a flat object whose keys are the exact names of the agent's criteria and whose values are text ("true" or "false" for a yes or no). contact, absent when the number matches no contact, holds id, phoneNumber, firstName, lastName, email, information and metadata. A field with no value is left out. transcript is one block of text, one line per turn starting with "User:" or "Assistant:".

Here is a body my workflow received: [PASTE THE RAW JSON]

Give me the nodes or modules in order, the settings of each, the exact expression of each mapped field, the conversion of the dates to [TIME ZONE], and how to handle very short calls (isSignificant false).

Make a self-hosted n8n reachable

Prompt to copy
You are a network engineer. Our n8n instance answers on an internal address, [INTERNAL ADDRESS, such as http://n8n.company.local:5678]. A SaaS platform in the cloud (CallShift) must send it HTTPS POST requests from the Internet, on the path /webhook/<id>. It sends each request once and expects an answer within 10 seconds.

Our context: [FIREWALL, DMZ, EXISTING REVERSE PROXY, HOW N8N IS HOSTED (Docker, VM), CONSTRAINTS OF OUR IT TEAM].

Suggest the simplest safe way to expose ONLY the /webhook/ path of n8n (not the editor), in HTTPS on port 443 with a valid certificate. Compare a reverse proxy in a DMZ, an outbound tunnel and n8n Cloud. Give the exact configuration of the option you recommend, the n8n environment variables to set (WEBHOOK_URL in particular), and the nslookup and curl commands to check it from outside our network.

Catch up on undelivered calls

Prompt to copy
You are an expert in [n8n / Make]. The CallShift end-of-call webhook is never retried: when our endpoint is down, the call is lost for the webhook. Build a scheduled workflow, every [30 MINUTES / HOUR], that:
1. Calls GET https://api.callshift.ai/v1/calls with the header Authorization: Bearer <API key stored in the tool's credentials>, filtered by agentId and by period (from and to, full ISO 8601 dates, on the creation time).
2. Reads every page: the query takes page (from 1) and limit (up to 100), and the response gives data and pageCount.
3. Compares the callId of each call with those already in [TARGET] and adds only the missing ones.
4. For one call, GET https://api.callshift.ai/v1/calls/{callId} returns the full detail, transcript included.

Give me the nodes or modules, their settings and the exact expressions.