Skip to content

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.

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-urlencoded with 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.1
Host: api.acmeindia.example
X-Api-Key: <your stored credential>
Content-Type: application/json
User-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.

Answer with a 2xx status and, ideally, a short JSON body:

{ "order": { "id": "ORD-48213", "status": "out for delivery", "eta": "today, 6 pm" } }
  • Any 2xx is 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-Type contains json, 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.
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.

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.

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.x or localhost, the request is blocked.
  • At most 2 redirects, never from https to http. Credential headers aren’t sent on to a different host. After a 301, 302 or 303, a non-GET request becomes a GET without a body.

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.

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.

You need a workspace role of Admin or Owner to manage webhooks.

  1. Open Integrations. Under Webhooks, click Add endpoint.

  2. Enter the Endpoint URL. It must start with https:// and point at a public address.

  3. Under Events, tick the events you want. Call ended and Post-call analysis ready are ticked to start with.

  4. 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.

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.

POST /hooks/360 HTTP/1.1
Content-Type: application/json
User-Agent: 360VoiceBot-Webhooks/1
X-VoiceBot-Event: call.ended
X-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 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.

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))
)
}

Rotate secret gives the endpoint a new signing secret. The old one stops working straight away, so update your endpoint at the same time.