Webhooks
Get notified instantly when a call completes via webhook events.
Twenty2 sends a webhook event to your server every time a call ends. This is the most reliable way to capture call outcomes, recordings, transcripts, and output variables — without polling the API.
Setup
Go to Integrations
Navigate to Profile → Integrations in your Twenty2 dashboard
Enter your endpoint URL
Find the Webhook URL field and enter your publicly accessible HTTPS endpoint
Save
Click Save — Twenty2 will immediately start sending events to your URL
Twenty2 sends a POST request to your URL every time a call ends. Make sure your endpoint returns a 200 response within 5 seconds.
Events
| Event | Description |
|---|---|
call.failed | Call did not connect or was unanswered |
call.completed | Call connected and completed successfully |
Example Payload
{
"event": "call.failed",
"event_id": "evt_dd640228-c82f-494f-b0ff-4ef3db3aaf3b",
"timestamp": "2026-05-22T07:18:39.684Z",
"workspace_id": "69bcb7e7cf2a02ecc7a28e17",
"assistant_id": "6a02dc5d92b9cbce7962a2b5",
"assistant_name": "Varsha",
"call": {
"call_id": "6a100328ba090039ac8610a2",
"direction": "outbound",
"status": "failed",
"from_number": "7971441943",
"to_number": "9871532305",
"started_at": "2026-05-22T07:18:30.092Z",
"answered_at": null,
"ended_at": "2026-05-22T07:18:39.684Z",
"duration_seconds": 0,
"billed_duration_seconds": 0,
"call_cost_inr": 0,
"disconnection_reason": "unanswered"
},
"campaign": {
"campaign_id": "6a100328ba090039ac8610a0",
"campaign_name": "web test"
},
"contact": {
"phone_number": "9871532305",
"input_variables": {
"customerName": "Lakshay",
"productInCart": "Shoes"
}
},
"output_variables": {},
"recording": {
"available": false,
"url": null
},
"transcript": {
"available": false
}
}
Payload Reference
Root fields
| Field | Type | Description |
|---|---|---|
event | string | Event type — call.completed or call.failed |
event_id | string | Unique identifier for this webhook delivery |
timestamp | string | Time the event was fired (ISO 8601) |
workspace_id | string | Your Twenty2 workspace ID |
assistant_id | string | ID of the assistant that handled the call |
assistant_name | string | Name of the assistant |
call object
| Field | Type | Description |
|---|---|---|
call_id | string | Unique identifier for the call |
direction | string | Always outbound for Phase 1 |
status | string | Final call status — completed or failed |
from_number | string | Number the call was made from |
to_number | string | Number the call was made to |
started_at | string | Call start time (ISO 8601) |
answered_at | string | Time call was answered — null if unanswered |
ended_at | string | Call end time (ISO 8601) |
duration_seconds | number | Total call duration in seconds |
billed_duration_seconds | number | Billable duration in seconds |
call_cost_inr | number | Call cost in INR |
disconnection_reason | string | Reason call ended — e.g. unanswered, completed |
campaign object
| Field | Type | Description |
|---|---|---|
campaign_id | string | ID of the campaign — null for standalone calls |
campaign_name | string | Name of the campaign |
contact object
| Field | Type | Description |
|---|---|---|
phone_number | string | Contact's phone number |
input_variables | object | Key-value pairs passed into the call at trigger time |
output_variables object
Key-value pairs captured by the assistant during the call. Empty {} if the call did not connect or no variables were captured.
recording object
| Field | Type | Description |
|---|---|---|
available | boolean | true if a recording is available for this call |
url | string | URL to download the recording — null if not available |
transcript object
| Field | Type | Description |
|---|---|---|
available | boolean | true if a transcript is available for this call |
Delivery & Retries
If your endpoint fails to return a 200, Twenty2 retries the webhook:
| Attempt | Timing |
|---|---|
| 1st | Immediately after call ends |
| 2nd | 5 minutes later |
| 3rd | 30 minutes later |
After 3 failed attempts the webhook is dropped. Make sure your endpoint is stable and responds within 5 seconds. Use the Call History API to recover any missed data.
Signature Verification
Every webhook request includes an X-Twenty2-Signature header so you can verify it genuinely came from Twenty2. The signature is an HMAC-SHA256 hash of the raw request body signed with your webhook secret.
const crypto = require("crypto");
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const expectedHeader = `sha256=${expected}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedHeader)
);
}
app.post("/webhook", (req, res) => {
const signature = req.headers["x-twenty2-signature"];
const isValid = verifyWebhook(req.rawBody, signature, process.env.WEBHOOK_SECRET);
if (!isValid) return res.status(401).send("Invalid signature");
const event = req.body;
console.log(`Event: ${event.event}, Call: ${event.call.call_id}`);
res.status(200).send("OK");
});
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Twenty2-Signature")
is_valid = verify_webhook(
request.get_data(),
signature,
os.environ["WEBHOOK_SECRET"]
)
if not is_valid:
return "Invalid signature", 401
event = request.get_json()
print(f"Event: {event['event']}, Call: {event['call']['call_id']}")
return "OK", 200
Always use timingSafeEqual or equivalent for signature comparison — never plain string equality. This prevents timing attacks.
Last updated 1 day ago
Built with Documentation.AI