Webhooks & Integrations
Connect CallTone to your CRM, communication tools, and custom systems. Use the REST API to programmatically manage resources and receive real-time webhook notifications when events occur.
REST API Overview
The CallTone API is a RESTful JSON API that lets you programmatically manage agents, campaigns, contacts, calls, and more. All API requests are made over HTTPS and require authentication via an API key.
Base URL
https://api.calltone.ai/v1All endpoints are versioned. The current version is v1. When breaking changes are introduced, a new version will be released and the previous version will continue to work for at least 12 months.
Authentication
Every API request must include your API key in the Authorization header using the Bearer scheme:
GET /v1/agents HTTP/1.1 Host: api.calltone.ai Authorization: Bearer ct_live_abc123def456... Content-Type: application/json
API keys start with ct_live_ for production keys and ct_test_ for test keys. Test keys work in a sandbox environment where no real calls are placed.
Rate Limits
The API enforces rate limits to ensure fair usage. Current limits are:
- 100 requests per minute for standard endpoints (agents, contacts, campaigns).
- 10 requests per minute for call initiation endpoints.
- 1,000 requests per minute for read-only endpoints (analytics, call logs).
Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset) are included in every response. If you exceed the limit, the API returns a 429 Too Many Requests response.
Creating API Keys
API keys are managed from your CallTone dashboard under Settings > API Keys.
- Navigate to Settings > API Keys.
- Click Create API Key.
- Enter a descriptive name for the key (e.g., "Zapier Integration", "CRM Sync", or "Slack Notifications").
- Choose the environment: Production (for live calls and data) or Test (for sandbox testing).
- Click Create. The full key is displayed once. Copy it immediately and store it securely. You will not be able to view the full key again.
You can create multiple keys for different integrations. Each key is independently revocable, so if one is compromised, you can revoke it without affecting other integrations. See the Team Management documentation for API key security best practices.
Webhook Events
Webhooks allow CallTone to push real-time notifications to your server when specific events occur. Instead of polling the API for updates, you register a webhook URL and CallTone sends an HTTP POST request to that URL whenever an event fires.
Configuring Webhooks
To set up a webhook, go to Settings > Webhooks and click Add Webhook. Provide:
- URL — the HTTPS endpoint on your server that will receive the webhook payloads.
- Events — select which event types you want to receive. You can subscribe to all events or pick specific ones.
- Secret — optionally provide a signing secret, or let CallTone generate one for you. This is used for signature verification.
Available Events
CallTone currently supports the following webhook event types:
call.started
Fired when an outbound or inbound call begins. The payload includes the call ID, agent ID, contact information, campaign ID (if part of a campaign), phone numbers (from and to), and the timestamp when the call connected.
call.ended
Fired when a call completes, regardless of the outcome. This is the most data-rich event. The payload includes:
- Call ID, agent ID, and contact information.
- Duration — total call duration in seconds.
- Outcome — the agent-determined result (e.g., "interested", "not interested", "voicemail", "appointment booked", "callback requested", "dnc requested").
- Transcript — the full call transcript as an array of speaker-labeled turns.
- Summary — an AI-generated summary of the conversation.
- Sentiment — the overall sentiment score of the call (positive, neutral, negative).
- Recording URL — a signed URL to the call recording (if recording was enabled), valid for 24 hours.
appointment.booked
Fired when a voice agent successfully books an appointment during a call. The payload includes the appointment ID, contact information, scheduled date and time, duration, any notes captured by the agent, and the associated call ID.
campaign.completed
Fired when all contacts in a campaign have been called (or skipped due to DNC, invalid numbers, etc.) and the campaign status changes to "completed". The payload includes the campaign ID, name, start and end timestamps, total contacts, calls made, calls completed, and a breakdown of outcomes.
contact.updated
Fired whenever a contact record is modified. This includes changes to contact fields (name, email, phone), lead score updates, consent status changes, and DNC additions. The payload includes the contact ID, the fields that changed (with old and new values), and the source of the change (e.g., "api", "dashboard", "call_outcome").
Webhook Payload Format
All webhook payloads are sent as JSON via HTTP POST. Every payload follows a consistent envelope structure:
{
"id": "evt_a1b2c3d4e5f6",
"type": "call.ended",
"created_at": "2026-02-13T14:30:00.000Z",
"api_version": "v1",
"data": {
"call_id": "call_xyz789",
"agent_id": "agent_abc123",
"contact": {
"id": "contact_def456",
"name": "Jane Smith",
"phone": "+12125551234",
"email": "[email protected]"
},
"campaign_id": "campaign_ghi789",
"duration": 142,
"outcome": "appointment_booked",
"summary": "Discussed premium plan options. Jane was interested and booked a demo for Thursday at 2 PM.",
"sentiment": "positive",
"transcript": [
{ "speaker": "agent", "text": "Hello, this is an AI assistant calling on behalf of Acme Corp..." },
{ "speaker": "contact", "text": "Hi, yes I was expecting this call..." }
],
"recording_url": "https://api.calltone.ai/v1/recordings/rec_abc123?token=signed_token"
}
}Envelope Fields
id— a unique identifier for this event, prefixed withevt_. Use this for deduplication.type— the event type (e.g., "call.ended", "appointment.booked").created_at— ISO 8601 timestamp of when the event was generated.api_version— the API version used to generate the payload.data— the event-specific payload. The shape varies by event type as described above.
Signature Verification
To verify that a webhook request genuinely came from CallTone (and was not spoofed or tampered with), every webhook request includes a signature in the X-CallTone-Signature header. The signature is computed using HMAC-SHA256.
How It Works
- CallTone takes the raw JSON request body (as a UTF-8 string).
- It computes the HMAC-SHA256 of the body using your webhook signing secret as the key.
- The resulting hex digest is sent in the
X-CallTone-Signatureheader. - On your server, you compute the same HMAC-SHA256 and compare it to the header value. If they match, the request is authentic.
Verification Example (Node.js)
import crypto from "crypto";
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(body, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
// In your webhook handler:
app.post("/webhooks/calltone", (req, res) => {
const signature = req.headers["x-calltone-signature"] as string;
const rawBody = req.body; // must be the raw string, not parsed JSON
if (!verifyWebhookSignature(rawBody, signature, process.env.CALLTONE_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
// Process the event...
res.status(200).json({ received: true });
});Important: Always use a constant-time comparison function (like crypto.timingSafeEqual) to compare signatures. Simple string equality (===) is vulnerable to timing attacks.
Retry Policy
CallTone expects your webhook endpoint to respond with a 2xx status code within 10 seconds to acknowledge receipt. If your endpoint fails to respond, returns a non-2xx status code, or times out, CallTone will retry the delivery.
Retry Schedule
CallTone uses exponential backoff with the following retry schedule:
- Attempt 1 — immediately (the initial delivery).
- Attempt 2 — 1 minute after the first failure.
- Attempt 3 — 5 minutes after the second failure.
After 3 failed attempts, the delivery is marked as failed. You can view failed deliveries and manually retry them from the Settings > Webhooks page. The event ID remains the same across retries, so use it for deduplication in case a delivery succeeds but the acknowledgment is lost.
Best Practices for Webhook Handlers
- Respond quickly. Return a 200 response as soon as you receive the payload. Process the event asynchronously (e.g., push it to a queue) rather than performing slow operations in the request handler.
- Be idempotent. Use the event
idfield to deduplicate. If you receive the same event ID twice, skip processing the duplicate. - Handle all event types gracefully. If you receive an event type you do not recognize, respond with 200 and ignore it. This prevents failures when new event types are added.
Integration Examples
Zapier
CallTone integrates with Zapier through webhooks. To connect:
- Create a new Zap in Zapier and choose Webhooks by Zapier as the trigger.
- Select Catch Hook as the trigger event.
- Zapier will provide a webhook URL. Copy it.
- In CallTone, go to Settings > Webhooks > Add Webhook and paste the Zapier URL as the endpoint.
- Select the events you want to send to Zapier (e.g., call.ended, appointment.booked).
- Back in Zapier, test the trigger by making a test call in CallTone. Once Zapier receives the sample payload, you can map fields to any of Zapier's 5,000+ app integrations.
Common Zapier workflows include: sending appointment details to Google Calendar, creating deals in HubSpot when a call outcome is "interested", or posting call summaries to a Slack channel.
Slack Notifications
To receive real-time call notifications in Slack:
- Create a Slack Incoming Webhook in your Slack workspace settings and note the webhook URL.
- Set up a lightweight webhook handler on your server (or use a serverless function) that receives CallTone webhooks and forwards a formatted message to the Slack webhook URL.
Here is a Node.js example that posts call summaries to Slack:
import express from "express";
const app = express();
app.use(express.text({ type: "application/json" }));
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;
app.post("/webhooks/calltone", async (req, res) => {
// Verify signature first (see verification example above)
const event = JSON.parse(req.body);
if (event.type === "call.ended") {
const { data } = event;
const message = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: [
"*Call Completed* :phone:",
`*Contact:* ${data.contact.name} (${data.contact.phone})`,
`*Duration:* ${Math.floor(data.duration / 60)}m ${data.duration % 60}s`,
`*Outcome:* ${data.outcome}`,
`*Summary:* ${data.summary}`,
].join("\n"),
},
},
],
};
await fetch(SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
}
res.status(200).json({ received: true });
});
app.listen(3000);CRM Sync
Use the call.ended and contact.updated webhooks to keep your CRM in sync with CallTone. A typical integration works as follows:
- When
call.endedfires, look up the contact in your CRM by phone number or email. - Create or update the contact record with the call outcome, summary, and any notes from the transcript.
- If the outcome is "appointment_booked", create a corresponding calendar event or task in the CRM.
- If the outcome is "interested", update the lead stage or score in the CRM to reflect the new status.
- Log the call as an activity on the contact record for your sales team to review.
This pattern works with any CRM that has a REST API, including HubSpot, Salesforce, Pipedrive, and Close.
Complete Webhook Handler Example (Node.js)
Here is a complete example of a webhook handler that verifies signatures, processes events, and handles errors:
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: Use express.text() to get the raw body for signature verification
app.use(express.text({ type: "application/json" }));
const WEBHOOK_SECRET = process.env.CALLTONE_WEBHOOK_SECRET!;
function verifySignature(body: string, signature: string): boolean {
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(body, "utf8")
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false;
}
}
// Track processed events for deduplication
const processedEvents = new Set<string>();
app.post("/webhooks/calltone", async (req, res) => {
// 1. Verify the signature
const signature = req.headers["x-calltone-signature"] as string;
if (!signature || !verifySignature(req.body, signature)) {
console.error("Webhook signature verification failed");
return res.status(401).json({ error: "Invalid signature" });
}
// 2. Parse the event
const event = JSON.parse(req.body);
// 3. Deduplicate
if (processedEvents.has(event.id)) {
return res.status(200).json({ received: true, deduplicated: true });
}
processedEvents.add(event.id);
// 4. Respond immediately, then process asynchronously
res.status(200).json({ received: true });
// 5. Handle the event
try {
switch (event.type) {
case "call.started":
console.log(`Call started: ${event.data.call_id}`);
// Update your system: mark contact as "in progress"
break;
case "call.ended":
console.log(`Call ended: ${event.data.call_id}, outcome: ${event.data.outcome}`);
// Sync to CRM, update lead score, notify team
break;
case "appointment.booked":
console.log(`Appointment booked: ${event.data.appointment_id}`);
// Create calendar event, send confirmation email
break;
case "campaign.completed":
console.log(`Campaign completed: ${event.data.campaign_id}`);
// Generate report, notify campaign manager
break;
case "contact.updated":
console.log(`Contact updated: ${event.data.contact_id}`);
// Sync changes to CRM
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
} catch (error) {
console.error(`Error processing event ${event.id}:`, error);
// In production, send this to your error tracking service
}
});
app.listen(3000, () => {
console.log("Webhook handler listening on port 3000");
});API Error Responses
When an API request fails, the response includes a consistent error format:
{
"error": {
"code": "invalid_request",
"message": "The 'phone' field must be in E.164 format (e.g., +12125551234).",
"param": "phone",
"status": 400
}
}Common error codes include:
401 Unauthorized— missing or invalid API key.403 Forbidden— the API key does not have permission for this action.404 Not Found— the requested resource does not exist.422 Unprocessable Entity— validation error in the request body.429 Too Many Requests— rate limit exceeded.500 Internal Server Error— an unexpected error on our end. Retry with exponential backoff.
Next Steps
Explore related documentation:
- Team Management — create and manage API keys, configure organization settings.
- Compliance — use the API to sync DNC lists and consent records.
- Creating Agents — manage agents programmatically via the API.
- Campaigns — launch and monitor campaigns through the API.