OverviewAPI ReferenceResources

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

  1. Register your URL and events with POST /webhooks. The response contains the signing secret, which you see only once.
  2. Send a test event with POST /webhooks/test.
  3. 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 /webhooks with { "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

EventWhen it firesdata
conversation.createdThe desktop app saves a new call after it endsconversationId
transcript.readyThe desktop app saves a call's transcript. segmentCount is the number of segments in that save.conversationId, segmentCount
transcript.failedSaving the transcript failedconversationId, error
conversation.keypoints.readyKey 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.readyWith keypoints.ready, when the key points have sectionsconversationId, title, sectionCount
conversation.action_items.readyWith keypoints.ready, when there are action itemsconversationId, actionItemCount
conversation.feedback.readyFeedback is generated, in the app or by POST /feedbackconversationId, overallScore
conversation.sharedA call is shared, in the app or by POST /shareconversationId, shareUrl
conversation.updatedA call is renamed with PATCH /conversations/{id}conversationId, title
conversation.deletedA call is deleted, in the app or through the APIconversationId
webhook.testPOST /webhooks/testmessage

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"
}
HeaderValue
Content-Typeapplication/json
X-Convo-EventThe event name
X-Convo-SignatureThe 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 "", 200

Delivery

  • 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 /webhooks works on any paid plan, so you can still remove them.
Was this page helpful?