Webhooks
Get a signed POST at your URL when a call is saved, summarised, scored, shared, renamed or deleted. Professional and Enterprise.
A webhook sends a signed JSON POST to your URL for each event you subscribe to.
- Professional plans can have 3 webhooks and Enterprise 999.
- A delivery tells you that something happened. Read the data itself from the API, such as
GET /conversations/{id}/keypoints. - Reply with a 2xx within 10 seconds and do the work afterwards. Convo ignores the response.
Setup
- Register your URL and events with
POST /webhooks. The response contains the signing secret, which you see only once. - Send a test event with
POST /webhooks/test. - Verify the signature of every request.
Register a URL
curl -X POST https://www.itsconvo.com/api/v1/webhooks \
-H "Authorization: Bearer $CONVO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/convo", "events": ["conversation.keypoints.ready", "conversation.deleted"] }'const res = await fetch("https://www.itsconvo.com/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CONVO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/webhooks/convo",
events: ["conversation.keypoints.ready", "conversation.deleted"],
}),
});
const { data } = await res.json();
// data.secret is shown only in this response{
"data": {
"id": "8d3f6a52-1c4e-4b7a-9e2d-5f0b8c1a7e34",
"url": "https://example.com/webhooks/convo",
"events": ["conversation.keypoints.ready", "conversation.deleted"],
"secret": "whsec_3c9f0e1a7b2d4c6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c",
"createdAt": "2026-03-18T15:00:00.000Z"
}
}- The URL must use HTTPS. Localhost and private network addresses are refused with
400. - List, test and delete webhooks with the webhook endpoints.
- Webhooks can't be changed. To change the URL or events, delete the webhook (
DELETE /webhookswith{ "id": "..." }as the body) and register a new one.
Send a test event
curl -X POST https://www.itsconvo.com/api/v1/webhooks/test \
-H "Authorization: Bearer $CONVO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "webhookId": "8d3f6a52-1c4e-4b7a-9e2d-5f0b8c1a7e34" }'{ "data": { "webhookId": "8d3f6a52-1c4e-4b7a-9e2d-5f0b8c1a7e34", "delivered": true, "statusCode": 200 } }delivered is true whenever your server answered, so check statusCode. If your server doesn't answer within 10 seconds, you get delivered: false. The test is sent even when the webhook isn't subscribed to webhook.test.
Events
| Event | When it fires | data |
|---|---|---|
conversation.created | The desktop app saves a new call after it ends | conversationId |
transcript.ready | The desktop app saves a call's transcript. segmentCount is the number of segments in that save. | conversationId, segmentCount |
transcript.failed | Saving the transcript failed | conversationId, error |
conversation.keypoints.ready | Key points are generated, by the app after a call or by POST /keypoints. Returning saved key points doesn't fire it. | conversationId, title |
conversation.summary.ready | With keypoints.ready, when the key points have sections | conversationId, title, sectionCount |
conversation.action_items.ready | With keypoints.ready, when there are action items | conversationId, actionItemCount |
conversation.feedback.ready | Feedback is generated, in the app or by POST /feedback | conversationId, overallScore |
conversation.shared | A call is shared, in the app or by POST /share | conversationId, shareUrl |
conversation.updated | A call is renamed with PATCH /conversations/{id} | conversationId, title |
conversation.deleted | A call is deleted, in the app or through the API | conversationId |
webhook.test | POST /webhooks/test | message |
Each request looks like this:
{
"event": "conversation.keypoints.ready",
"data": { "conversationId": "session_1773842400000", "title": "Acme pilot pricing call" },
"timestamp": "2026-03-18T15:00:12.000Z"
}| Header | Value |
|---|---|
Content-Type | application/json |
X-Convo-Event | The event name |
X-Convo-Signature | The hex HMAC-SHA256 of the raw body, keyed with your secret |
Verify the signature
Use the whole secret as the key, including whsec_. Compute the HMAC over the raw body before you parse it, and compare in constant time. The signature covers the body's timestamp, so you can reject old requests by checking it.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/webhooks/convo", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.CONVO_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const received = req.get("X-Convo-Signature") ?? "";
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
res.sendStatus(200);
const { event, data } = JSON.parse(req.body);
// handle the event
});
app.listen(3000);import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/webhooks/convo")
def convo_webhook():
secret = os.environ["CONVO_WEBHOOK_SECRET"].encode()
expected = hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Convo-Signature", "")):
abort(401)
payload = request.get_json()
# handle payload["event"] and payload["data"]
return "", 200Delivery
- Convo sends each event once. It doesn't retry and keeps no delivery log, so a delivery that fails or times out is lost. To catch up, list recent calls with
GET /conversations. - Deliveries don't count against your rate limit.
- Webhooks keep sending after a downgrade below Professional.
DELETE /webhooksworks on any paid plan, so you can still remove them.
Was this page helpful?