Receive tool and event webhooks
360 sends requests to your HTTPS endpoints in two ways:
- Tools. An agent calls your API before, during or after a call, to look something up or save something. You set the request up in the agent. This page shows what reaches your endpoint and how to answer. To add a tool in the app, see Give the agent tools.
- Event webhooks. 360 tells your system when a call starts or ends, when post-call analysis is ready, and when the balance or budget runs low. See Event webhooks.
Both requests come from 360’s servers, never from the caller’s phone or a browser.
Tool requests
Section titled “Tool requests”What your endpoint receives
Section titled “What your endpoint receives”The request uses the Method, URL, Headers and Query string you set in the tool. Before sending it, 360 fills in:
{{variables}}in the URL, the query string and text headers. In the URL, values are URL-encoded. If a variable has no value, the tool fails with “Missing a value for{{name}}.”- Credentials, in Credential headers and in
{{secret:…}}in the query string. Only 360’s server sees them; the AI never does. - Parameters that the AI fills in from the conversation, for tools that run during the call.
Only the parameters you declared are sent. A parameter you used in the URL or query string goes
there. The rest go:
- in the query string, for GET and DELETE;
- in the body, for POST, PUT and PATCH: as a JSON object with JSON, or as
application/x-www-form-urlencodedwith Form (urlencoded). In a form body, a value that isn’t text is sent as JSON text.
360 adds User-Agent: 360-VoiceBot-Tools/1.0 unless you set your own, and the matching
Content-Type when there’s a body.
For example, a POST tool change_delivery_date with the URL
https://api.acmeindia.example/orders/{{order_id}}/delivery, a Credential header X-Api-Key, and
the parameters order_id and new_date sends:
POST /orders/ORD-48213/delivery HTTP/1.1Host: api.acmeindia.exampleX-Api-Key: <your stored credential>Content-Type: application/jsonUser-Agent: 360-VoiceBot-Tools/1.0
{"new_date": "2026-10-30"}order_id went into the URL, so only new_date is in the body.
Tools that run Before the call or After the call get no parameters from the AI. Pass them
values through {{variables}} in the URL, query string or headers, for example
{{caller_number}}. A tool that runs after the call can also use values that tools saved during
the call.
How to answer
Section titled “How to answer”Answer with a 2xx status and, ideally, a short JSON body:
{ "order": { "id": "ORD-48213", "status": "out for delivery", "eta": "today, 6 pm" } }- Any
2xxis a success. The AI reads the body to decide what to say. - Any other status is a failure, for example “The server answered 404.” The agent tells the caller briefly and offers another way to help. It doesn’t read your error body.
- Keep it short. The AI reads the first 4,000 characters of the response. A response larger than 256 KB fails.
- JSON is read as JSON when the
Content-Typecontainsjson, or the body starts with{or[. Anything else is passed to the AI as text. - Save values for later with Save from the response, using a path such as
$.order.status. Saved values become{{variables}}for the instructions, the greeting and later tools.
Time limits
Section titled “Time limits”| When the tool runs | How long 360 waits |
|---|---|
| During the call | The tool’s Timeout (ms): 5,000 unless you change it, from 500 to 60,000. Redirects count towards it. The editor warns you above 8,000, because the caller waits in silence. |
| Before the call | All of the agent’s before-the-call tools run at the same time and must all finish within 2.5 seconds. If they don’t, the call starts without any of their values. |
| After the call | The tools run one after another once the caller has hung up. Nobody waits for them, and a failed one isn’t tried again. |
If your endpoint doesn’t answer in time, the tool fails with “No answer within 5000 ms.” (with its timeout), and the agent moves on.
What the agent says while it waits
Section titled “What the agent says while it waits”During the call, if a tool takes more than about a second, the agent fills the silence with a hold line: “One moment, please.” If the wait goes on, it says up to two more: “I’m saving that now.” and “Almost done, thanks for waiting.” Agents on an All in one model don’t say hold lines.
You can also turn on Thinking sounds in Voice tuning, for a soft typing sound while the agent works.
Network rules
Section titled “Network rules”To keep calls safe, 360 only calls endpoints that follow these rules:
- The URL starts with
https://and has no user name or password in it. Put credentials in a Credential header. - The host name resolves only to public internet addresses. If any of its addresses is private,
such as
10.x.x.xorlocalhost, the request is blocked. - At most 2 redirects, never from
httpstohttp. Credential headers aren’t sent on to a different host. After a301,302or303, a non-GET request becomes a GET without a body.
Check that a request comes from 360
Section titled “Check that a request comes from 360”360 doesn’t sign tool requests. To know a request is yours, require a secret your endpoint checks,
for example an Authorization: Bearer … or X-Api-Key header. In the tool, set the header’s
value type to Credential. A credential is encrypted, and nobody can see it again after you save it.
If your API echoes the credential back, 360 masks it as •••• before the AI or the test result sees
it.
Bookings sent to your tool
Section titled “Bookings sent to your tool”An appointment-booking agent can send each confirmed booking to one of your tools, under Data to collect → Send bookings to. See Send bookings to your own system. The AI never calls this tool itself: 360 calls it once the caller confirms. Each of the tool’s parameters gets a fixed value or one of these placeholders:
| Placeholder | Value |
|---|---|
{{booking.start_utc}} |
Start time in UTC, YYYY-MM-DDTHH:mm:ss. |
{{booking.start_local}} |
Start time in the caller’s timezone, without an offset. |
{{booking.timezone}} |
The timezone’s name, for example Asia/Kolkata. |
{{booking.duration_minutes}} |
The Appointment length (minutes). |
{{booking.name}}, {{booking.email}} |
What the caller gave. |
{{caller.number}}, {{called.number}} |
The caller’s number, and the number they called (phone calls only). |
{{call.sid}}, {{cti.record_id}} |
The call’s IDs from 360 CTI, when it sends them. |
All values are sent as text. A placeholder with no value leaves its parameter out. With Request
body set to Salesforce flow, the body is wrapped as {"inputs": [{…}]}.
360 counts the booking as failed if your endpoint answers with a status other than 2xx, or with
a body that has "success": false, "isSuccess": false, error or errors. If your answer has an
id, 360 keeps it as the booking’s ID in your system.
Event webhooks
Section titled “Event webhooks”You need a workspace role of Admin or Owner to manage webhooks.
Add an endpoint
Section titled “Add an endpoint”-
Open Integrations. Under Webhooks, click Add endpoint.
-
Enter the Endpoint URL. It must start with
https://and point at a public address. -
Under Events, tick the events you want. Call ended and Post-call analysis ready are ticked to start with.
-
Click Add endpoint, then copy the Signing secret (
whsec_…). You see it only once.
Click Send test to send a webhook.test event. Click the endpoint to see its recent
deliveries: Delivered, Retrying or Failed, with the status your endpoint answered.
Replay sends a delivery again, with the same event id. Use the switch to pause an endpoint.
An organisation can have up to 20 endpoints.
Events
Section titled “Events”| Event | In the app | Sent when | data |
|---|---|---|---|
call.started |
Call started | The agent picks up a call. | agentId, source, and dialedNumber (the number called, on calls to your numbers). |
call.ended |
Call ended | A call has ended and its transcript is saved. | jobId (the call’s ID), roomName, agentId, versionId, source (phone or test), durationSeconds, turns. |
analysis.ready |
Post-call analysis ready | The post-call analysis has finished. | callId, agentId, status, summary, outcome, outcomeLabel, sentiment (positive, neutral or negative), transferReason, fields, detail, analysedAt. |
transfer.completed |
Transfer completed | A phone call in which the agent tried to transfer the caller has ended. One event per attempt. | jobId, callerNumber, ok (whether it worked), mode, via. |
wallet.low |
Wallet balance low | The prepaid balance falls below your low-balance alert, or runs out. | level (low or empty), balance (amount, currency), lowBalanceThreshold. |
budget.reached |
Budget reached | Postpaid spending reaches 80% of the monthly budget, then all of it. Once each per month. | level (warning or limit), month, spent and limit (amount, currency), percentUsed, estimated. |
A call you start through the API sends one more call.started when 360 places it, with
callId, direction (outbound), agentId, versionId, environment, to and roomName.
Test calls from the app send call.started and call.ended too. In call.ended, source is
test: filter on it if you only want real calls.
What your endpoint receives
Section titled “What your endpoint receives”POST /hooks/360 HTTP/1.1Content-Type: application/jsonUser-Agent: 360VoiceBot-Webhooks/1X-VoiceBot-Event: call.endedX-VoiceBot-Delivery: 6c0e…X-VoiceBot-Signature: t=1793080071,v1=5d41402abc4b2a76b9719d911017c592…
{"id":"evt_…","type":"call.ended","createdAt":"2026-10-27T05:44:31+00:00","orgId":"<org-id>","data":{…}}Every event has the same envelope: id, type, createdAt, orgId and data. A retry or a
replay keeps the same id, so use it to ignore an event you’ve already handled.
Answer and retries
Section titled “Answer and retries”Answer with any 2xx status within 10 seconds. Do the slow work after you’ve answered.
Any other answer, a timeout or a redirect counts as a failure. 360 tries again after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours: 7 attempts in all. Then the delivery is marked Failed. You can Replay it later.
Check the signature
Section titled “Check the signature”X-VoiceBot-Signature holds a timestamp t and a signature v1. v1 is the HMAC-SHA256, in
hex, of <t>.<raw body>, keyed with your signing secret. Check it against the raw body, before
you parse the JSON, and reject requests older than 5 minutes.
import crypto from "node:crypto"
// rawBody: the request body exactly as received (a string or Buffer)export function isFrom360(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2))) const expected = crypto .createHmac("sha256", secret) .update(`${parts.t}.${rawBody}`) .digest("hex") const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300 return ( fresh && typeof parts.v1 === "string" && parts.v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)) )}import hashlibimport hmacimport time
def is_from_360(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) signed = parts["t"].encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts["t"])) < 300 return fresh and hmac.compare_digest(expected, parts.get("v1", ""))Rotate secret gives the endpoint a new signing secret. The old one stops working straight away, so update your endpoint at the same time.