REST API v1

TalkTool API

Sync contacts from your CRM and pull call records, transcripts, AI summaries and recordings into your own systems. The API is JSON over HTTPS, scoped to your organization, and versioned with a response header.

Base URLhttps://www.usetalktool.com/api/v1
OpenAPI 3.1/api/v1/openapi.json
Current version2026-10-15
Quick start
Create a key in Dashboard → Integrations, then run curl -H "Authorization: Bearer tt_live_…" https://www.usetalktool.com/api/v1/contacts.
Authentication

Authenticate with a Bearer key

Create an API key in Dashboard → Integrations. Keys look like tt_live_… and are shown once, so copy it when it is created. Send it in the Authorization header on every request.

Request
curl https://www.usetalktool.com/api/v1/contacts \
  -H "Authorization: Bearer tt_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Keep it secret
A key can read your customers' contacts and call recordings. Store it in a secrets manager or environment variable, never in browser code, mobile apps or a public repository. If a key leaks, revoke it in Integrations immediately.

Scopes

Each key carries scopes. A request to an endpoint whose scope the key lacks returns 403 forbidden. New keys get the first three by default.

ScopeAllows
contacts:readList and retrieve contacts
contacts:writeCreate, update and delete contacts
calls:readList calls, retrieve a call, download recordings
webhooks:manageReserved for managing webhook subscriptions through the API. Today you manage endpoints in Dashboard → Integrations.

GET /languages works with any valid key, whatever its scopes.

Rate limits and errors

Rate limits and error format

Each key can make 300 requests per minute. Every successful response includes X-RateLimit-Limit and X-RateLimit-Remaining. Past the limit you get 429 rate_limited with a Retry-After header in seconds. Request bodies are capped at 64 KB (413).

Every response includes X-Request-Id and TalkTool-Api-Version. Quote the request id when you contact support. Errors always use the same envelope:

Error response (400)
{
  "error": {
    "code": "invalid_request",
    "message": "phone is not a valid phone number. Use E.164, e.g. +13105551234",
    "request_id": "6b0f1c6e-3a52-4a0e-9f0e-1d2c3b4a5f60",
    "details": { "field": "phone" }
  }
}
HTTPcodeMeaning
400invalid_requestMalformed JSON, a bad field or a bad query parameter. details.field names the field when there is one.
401unauthorizedMissing, malformed or revoked API key.
403forbiddenThe key does not have the scope this endpoint needs.
404not_foundNo such resource in your organization, or no recording for the call.
409conflictDuplicate phone or external_id (details.existing_id). Also used by request_in_progress.
409request_in_progressA request with the same Idempotency-Key is still running. Retry shortly.
413payload_too_largeBody exceeds 64 KB.
422idempotency_mismatchThe Idempotency-Key was already used with a different request.
429rate_limitedMore than 300 requests in a minute.
502recording_unavailableRecording service temporarily unavailable. Retry.
503temporarily_unavailableAccess logging is temporarily unavailable; retry with the same Idempotency-Key.
500internal_errorSomething went wrong on our side. Retry; for writes reuse the same Idempotency-Key.
Pagination

Cursor pagination

List endpoints return { data, next_cursor }, newest first. Use limit (1–100, default 50) to size the page. While next_cursor is not null, pass it back as cursor to fetch the next page. Treat cursors as opaque. A null next_cursor means you have reached the end.

curl "https://www.usetalktool.com/api/v1/calls?limit=50" \
  -H "Authorization: Bearer $TALKTOOL_API_KEY"

# {"data":[ ... ],"next_cursor":"NjZmMWMyYTliN2Q0ZTVhMWM4ZjAzYTEy"}

curl "https://www.usetalktool.com/api/v1/calls?limit=50&cursor=NjZmMWMyYTliN2Q0ZTVhMWM4ZjAzYTEy" \
  -H "Authorization: Bearer $TALKTOOL_API_KEY"
Idempotency

Safe retries with Idempotency-Key

Send an Idempotency-Key header (1–200 characters: letters, digits, - _ : .) on POST and PATCH requests. Repeating a request with the same key and body returns the original response with an Idempotent-Replayed: true header instead of running it twice. Reusing a key with a different body returns 422 idempotency_mismatch. If the first attempt failed, retrying under the same key runs it again.

curl -X POST https://www.usetalktool.com/api/v1/contacts \
  -H "Authorization: Bearer $TALKTOOL_API_KEY" \
  -H "Idempotency-Key: crm-sync-8841-v1" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Maria","phone":"+13105551234"}'
Contacts

Contacts

Contacts are the people you call. Use external_id to store your CRM record id, then look contacts up with GET /contacts?external_id=… instead of keeping a mapping table.

FieldNotes
first_nameRequired on create. Up to 100 characters.
last_nameOptional. Up to 100 characters.
phoneRequired on create. E.164 (+13105551234) or a US-style national number ((310) 555-1234). Stored and returned as E.164.
emailOptional. Must be a valid address.
languageA code like ES or EN_US, an English name like Spanish, a locale like es-US, or an ISO code like es. Stored and returned as the code. Defaults to EN_US on create. See Languages below.
external_idYour CRM record id. Up to 200 characters. Unique per organization.
notesOptional. Up to 5,000 characters.
typegeneral (default), allowed or blocked.

List contacts

GET /contacts · scope contacts:read. Query parameters: limit, cursor, phone, external_id, search (name, email or phone), updated_since (ISO 8601).

Request
curl "https://www.usetalktool.com/api/v1/contacts?external_id=crm_8841" \
  -H "Authorization: Bearer $TALKTOOL_API_KEY"
Response 200
{
  "data": [
    {
      "id": "66f1c2a9b7d4e5a1c8f03a12",
      "first_name": "Maria",
      "last_name": "Gonzalez",
      "phone": "+13105551234",
      "email": "maria@example.com",
      "language": "ES_US",
      "external_id": "crm_8841",
      "notes": "Prefers morning calls",
      "type": "general",
      "created_at": "2026-10-01T15:04:05.000Z",
      "updated_at": "2026-10-12T09:30:00.000Z"
    }
  ],
  "next_cursor": null
}

Create a contact

POST /contacts · scope contacts:write. Returns 201 with the contact.

Request
curl -X POST https://www.usetalktool.com/api/v1/contacts \
  -H "Authorization: Bearer $TALKTOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-8841-create" \
  -d '{
    "first_name": "Maria",
    "last_name": "Gonzalez",
    "phone": "(310) 555-1234",
    "language": "Spanish",
    "external_id": "crm_8841"
  }'
Response 201
{
  "data": {
    "id": "66f1c2a9b7d4e5a1c8f03a12",
    "first_name": "Maria",
    "last_name": "Gonzalez",
    "phone": "+13105551234",
    "email": "maria@example.com",
    "language": "ES_US",
    "external_id": "crm_8841",
    "notes": "Prefers morning calls",
    "type": "general",
    "created_at": "2026-10-01T15:04:05.000Z",
    "updated_at": "2026-10-12T09:30:00.000Z"
  }
}

If a contact with the same phone (or the same external_id) already exists you get 409 conflict and the id of the existing contact:

Response 409
{
  "error": {
    "code": "conflict",
    "message": "A contact with this phone already exists",
    "request_id": "6b0f1c6e-3a52-4a0e-9f0e-1d2c3b4a5f60",
    "details": { "existing_id": "66f1c2a9b7d4e5a1c8f03a12" }
  }
}

Upsert

Add ?upsert=true to update the existing contact instead of failing. It matches on phone first, then external_id, applies the fields you sent, and returns 200 with the updated contact. If there is no match it creates one and returns 201. This is the easiest way to keep a CRM in sync: run it on every record change.

curl -X POST "https://www.usetalktool.com/api/v1/contacts?upsert=true" \
  -H "Authorization: Bearer $TALKTOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Maria","phone":"+13105551234","language":"es-US"}'

Retrieve, update and delete

GET /contacts/{id} (contacts:read) returns one contact. PATCH /contacts/{id} (contacts:write) accepts any subset of the create fields; send null to clear last_name, email, notes, external_id or language. Changing to a phone or external_id another contact already uses returns 409.

PATCH
curl -X PATCH https://www.usetalktool.com/api/v1/contacts/66f1c2a9b7d4e5a1c8f03a12 \
  -H "Authorization: Bearer $TALKTOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"maria.g@example.com","notes":"Call after 2pm"}'

DELETE /contacts/{id} (contacts:write) soft-deletes the contact.

DELETE
curl -X DELETE https://www.usetalktool.com/api/v1/contacts/66f1c2a9b7d4e5a1c8f03a12 \
  -H "Authorization: Bearer $TALKTOOL_API_KEY"
Response 200
{ "data": { "id": "66f1c2a9b7d4e5a1c8f03a12", "deleted": true } }
Calls

Calls, transcripts and recordings

EndpointScopeDescription
GET /callscalls:readList calls. Filters: since, until (ISO 8601), direction (inbound, outbound, conference), status, contact_id, phone, plus limit and cursor.
GET /calls/{id}calls:readRetrieve one call. The id is the call SID (CA…).
GET /calls/{id}/recordingcalls:readRedirects to the recording audio.
List calls
curl "https://www.usetalktool.com/api/v1/calls?direction=inbound&since=2026-10-01T00:00:00Z&limit=25" \
  -H "Authorization: Bearer $TALKTOOL_API_KEY"
Retrieve a call
curl https://www.usetalktool.com/api/v1/calls/CA1f2e3d4c5b6a79880011223344556677 \
  -H "Authorization: Bearer $TALKTOOL_API_KEY"
Call object
{
  "data": {
    "id": "CA1f2e3d4c5b6a79880011223344556677",
    "direction": "outbound",
    "status": "completed",
    "type": "call",
    "from": "+13105550100",
    "to": "+13105551234",
    "number": "+13105550100",
    "contact": {
      "id": "66f1c2a9b7d4e5a1c8f03a12",
      "first_name": "Maria",
      "last_name": "Gonzalez",
      "external_id": "crm_8841"
    },
    "started_at": "2026-10-14T18:00:02.000Z",
    "answered_at": "2026-10-14T18:00:09.000Z",
    "ended_at": "2026-10-14T18:04:41.000Z",
    "duration_seconds": 279,
    "languages": { "agent": "EN_US", "customer": "ES_US" },
    "transcript_text": "Agent: Hello, this is Sam from Acme...",
    "summary": {
      "text": "Sam confirmed Maria's appointment for Friday.",
      "key_points": ["Appointment confirmed for Friday 10am"],
      "action_items": ["Send reminder text on Thursday"],
      "sentiment": "positive"
    },
    "recording": {
      "available": true,
      "url": "https://www.usetalktool.com/api/v1/calls/CA1f2e3d4c5b6a79880011223344556677/recording"
    },
    "created_at": "2026-10-14T18:00:02.000Z",
    "updated_at": "2026-10-14T18:05:10.000Z"
  }
}

direction is one of inbound, outbound, conference or unknown. languages are language codes (the same codes as the Languages endpoint). contact is null when the call is not linked to a contact. transcript_text and summary are null when none exists or when your organization has turned transcripts or summaries off.

Download a recording

GET /calls/{id}/recording responds with a 302 redirect to a signed audio URL that expires after about five minutes; the JSON body also contains { data: { url, expires_in } }. Follow the redirect with curl -L and send your key. Most recordings are MP3; some older ones are WAV. You get 404 when the call has no recording or recording is disabled for your organization. Do not store the signed URL, request a fresh one each time.

curl -L https://www.usetalktool.com/api/v1/calls/CA1f2e3d4c5b6a79880011223344556677/recording \
  -H "Authorization: Bearer $TALKTOOL_API_KEY" \
  -o call-recording.mp3
Languages

Languages

Every language in TalkTool has one code: uppercase letters, with an underscore before a regional variant, for example EN_US, ES_US, ES or PT_BR. The code is what the API stores and returns in language on contacts and in languages on calls. Send the code whenever you can. For convenience the API also accepts the English name, the locale (es-US) and the two-letter ISO code (es), case-insensitively, and converts them to the code. Anything else returns 400 invalid_request with details.field set to language.

Spanish has two codes
ES_US is Spanish for the United States and Mexico, which is what most North American teams want. ES is Spanish for Spain. Plain "Spanish", es-US, es-MX and es-419 all resolve to ES_US. The bare code es, es-ES and "Spanish (Spain)" resolve to ES. If your CRM only stores "Spanish", send ES_US explicitly rather than relying on the alias. Any other Spanish region code (es-CO, es-AR, es-PR…) also resolves to ES_US.

GET /languages returns the full list with every spelling each code accepts, so you can build a mapping from your CRM's language field once.

Response 200 (truncated)
{
  "data": [
    {
      "code": "ES_US",
      "label": "Spanish (US & MX)",
      "locale": "es-US",
      "also_accepted": ["Spanish (US & MX)", "es-US", "spanish", "es-MX", "es-419", "mexican spanish"]
    },
    {
      "code": "ES",
      "label": "Spanish (Spain)",
      "locale": "es-ES",
      "also_accepted": ["Spanish (Spain)", "es-ES", "castilian", "european spanish", "es"]
    }
  ]
}

All language codes

CodeLanguageLocaleAlso accepted
AFAfrikaansaf-ZAafrikaans, af
ARArabicar-SAarabic, ar
AR_EGArabic (Egyptian)ar-EGegyptian-arabic, arabic-egypt, egyptian arabic
BEBelarusianbe-BYbelarusian, be
BNBengalibn-BDbengali, bn
BGBulgarianbg-BGbulgarian, bg
CACatalanca-EScatalan, ca
ZH_CNChinese (Simplified)zh-CNmandarin, chinese, mandarin-chinese, simplified-chinese, zh-hans, zh-cn
ZH_TWChinese (Traditional)zh-TWchinese-traditional, traditional-chinese, taiwanese, zh-hant, zh-tw
HRCroatianhr-HRcroatian, hr
CSCzechcs-CZczech, cs
DADanishda-DKdanish, da
NLDutchnl-NLdutch, nl
EN_AUEnglish (Australia)en-AUaustralian-english, english-australia
EN_INEnglish (India)en-INindian-english, english-india
EN_IEEnglish (Ireland)en-IEirish-english, english-ireland
EN_NZEnglish (New Zealand)en-NZnew-zealand-english, english-new-zealand, kiwi-english
EN_ZAEnglish (South Africa)en-ZAsouth-african-english, english-south-africa
EN_GBEnglish (UK)en-GBbritish-english, english-uk, uk-english
EN_USEnglish (US)en-USenglish, american-english, english-us, us-english, en-ca, american english, us english, en
ETEstonianet-EEestonian, et
FIFinnishfi-FIfinnish, fi
FRFrenchfr-FRfrench, fr-fr, fr
FR_CAFrench (Canada)fr-CAcanadian-french, french-canada, quebec-french, canadian french
GLGaliciangl-ESgalician, gl
DEGermande-DEgerman, de-de, de
DE_CHGerman (Swiss)de-CHswiss-german, german-swiss, swiss german
ELGreekel-GRgreek, el
HEHebrewhe-ILhebrew, he
HIHindihi-INhindi, hi
HUHungarianhu-HUhungarian, hu
IDIndonesianid-IDindonesian, id
ITItalianit-ITitalian, it
JAJapaneseja-JPjapanese, ja
KNKannadakn-INkannada, kn
KOKoreanko-KRkorean, ko
LVLatvianlv-LVlatvian, lv
LTLithuanianlt-LTlithuanian, lt
MKMacedonianmk-MKmacedonian, mk
MSMalayms-MYmalay, ms
MRMarathimr-INmarathi, mr
NBNorwegian Bokmalno-NOnorwegian, norwegian-bokmal, no
FAPersian (Farsi)fa-IRpersian, farsi, fa
PLPolishpl-PLpolish, pl
PT_BRPortuguese (Brazil)pt-BRbrazilian-portuguese, portuguese-brazil, brazilian portuguese
PTPortuguese (Portugal)pt-PTportuguese, european-portuguese, pt-pt, pt
RORomanianro-ROromanian, ro
RURussianru-RUrussian, ru
SRSerbiansr-RSserbian, sr
SKSlovaksk-SKslovak, sk
SLSloveniansl-SIslovenian, sl
ESSpanish (Spain)es-ESspanish-spain, european-spanish, castilian, es-es, spain spanish, castilian spanish, european spanish, es
ES_USSpanish (US & MX)es-USspanish, spanish-us, american-spanish, es-us, es-mx, es-419, mexican spanish, latin american spanish, us spanish
SVSwedishsv-SEswedish, sv
TLTagalog / Filipinotl-PHtagalog, filipino, fil, tl
TATamilta-INtamil, ta
TETelugute-INtelugu, te
THThaith-THthai, th
TRTurkishtr-TRturkish, tr
UKUkrainianuk-UAukrainian, uk
URUrduur-PKurdu, ur
VIVietnamesevi-VNvietnamese, vi
Webhooks

Webhooks

Webhooks push call events to your server as they happen, so you do not have to poll GET /calls. TalkTool sends an HTTPS POST with a signed JSON body to every endpoint subscribed to the event.

Create an endpoint

Open Dashboard → Integrations → Webhooks and choose Add endpoint. Enter an https:// URL, pick the events you want and save. TalkTool shows the signing secret once: store it in a secrets manager. You can send a test event, rotate the secret, edit the subscription or review recent deliveries from the same page. Only organization admins can change endpoints; every member can view them. An organization can have up to 20 endpoints.

Events

EventFires whenNotes
call.ringingA call starts ringing.Inbound calls fire when the call reaches your number; outbound calls when the far end starts ringing.
call.answeredSomeone answers the call.answered_at is set.
call.completedA call that was answered ends.Includes duration_seconds. It can arrive before call.recording.ready, and the transcript and summary may still be null.
call.missedA call ends and nobody answered.Sent instead of call.completed for unanswered calls.
call.recording.readyThe recording has been processed.recording.available is true and recording.url works with GET /calls/{id}/recording. Only sent when recording is enabled for the organization.

Events for one call are not guaranteed to arrive in order, because each is delivered and retried on its own. Use the timestamps inside data.call if order matters.

Delivery format

Every delivery is a JSON object with the event id, type, api_version, created_at and a data.call object that is identical to what GET /calls/{id} returns at the moment of the event.

Request headers
POST /talktool/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: TalkTool-Webhooks/1.0
TalkTool-Event-Id: evt_5b0f1c6e-3a52-4a0e-9f0e-1d2c3b4a5f60
TalkTool-Event-Type: call.completed
TalkTool-Delivery-Attempt: 1
TalkTool-Signature: t=1760465082,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
HeaderValue
TalkTool-Event-IdThe event id, the same as id in the body. Stable across retries.
TalkTool-Event-TypeThe event type, e.g. call.completed.
TalkTool-Delivery-Attempt1 for the first attempt, up to 7.
TalkTool-Signaturet=<unix seconds>,v1=<hex HMAC>. See below.
User-AgentTalkTool-Webhooks/1.0
Example body (call.completed)
{
  "id": "evt_5b0f1c6e-3a52-4a0e-9f0e-1d2c3b4a5f60",
  "type": "call.completed",
  "api_version": "2026-10-15",
  "created_at": "2026-10-14T18:04:42.000Z",
  "data": {
    "call": {
      "id": "CA1f2e3d4c5b6a79880011223344556677",
      "direction": "outbound",
      "status": "completed",
      "type": "call",
      "from": "+13105550100",
      "to": "+13105551234",
      "number": "+13105550100",
      "contact": {
        "id": "66f1c2a9b7d4e5a1c8f03a12",
        "first_name": "Maria",
        "last_name": "Gonzalez",
        "external_id": "crm_8841"
      },
      "started_at": "2026-10-14T18:00:02.000Z",
      "answered_at": "2026-10-14T18:00:09.000Z",
      "ended_at": "2026-10-14T18:04:41.000Z",
      "duration_seconds": 279,
      "languages": { "agent": "EN_US", "customer": "ES_US" },
      "transcript_text": null,
      "summary": null,
      "recording": { "available": false, "url": null },
      "created_at": "2026-10-14T18:00:02.000Z",
      "updated_at": "2026-10-14T18:04:41.000Z"
    }
  }
}

Verify the signature

Each endpoint has its own signing secret. TalkTool computes an HMAC-SHA256 over the timestamp, a dot and the exact raw request body, and sends it in TalkTool-Signature as t=<unix seconds>,v1=<hex>. To verify: read the raw body, recompute the HMAC with your secret, compare in constant time, and reject timestamps more than five minutes old to stop replays.

Use the raw body
Compute the signature over the bytes you received, before JSON parsing. A parsed and re-serialized body will not match.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TALKTOOL_WEBHOOK_SECRET; // whsec_...

function verify(rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split(/=(.*)/s).slice(0, 2))
  );
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Use the RAW body. Parsing and re-serializing JSON changes the bytes.
app.post("/talktool/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("TalkTool-Signature") || "")) {
    return res.status(400).send("bad signature");
  }
  res.sendStatus(200); // acknowledge first, work later
  const event = JSON.parse(raw);
  queue.add(event); // dedupe on event.id inside your worker
});
Python (Flask)
import hashlib
import hmac
import os
import time

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["TALKTOOL_WEBHOOK_SECRET"].encode()  # whsec_...


def verify(raw_body: bytes, header: str, tolerance_sec: int = 300) -> bool:
    try:
        parts = dict(p.strip().split("=", 1) for p in header.split(","))
        timestamp = parts["t"]
        signature = parts["v1"]
        if abs(time.time() - int(timestamp)) > tolerance_sec:
            return False
    except (KeyError, ValueError):
        return False
    expected = hmac.new(
        SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/talktool/webhooks")
def talktool_webhook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    if not verify(raw, request.headers.get("TalkTool-Signature", "")):
        return "bad signature", 400
    enqueue(request.get_json())  # dedupe on event["id"] in your worker
    return "", 200

Retries and failures

Any 2xx response counts as delivered. Anything else, a timeout or a connection error is a failure and is retried with the same event id on this schedule:

AttemptSent
1Immediately
21 minute after the first failure
35 minutes later
430 minutes later
52 hours later
66 hours later
724 hours later, then we stop

If an endpoint fails 20 deliveries in a row, TalkTool disables it and shows the reason in Dashboard → Integrations. Fix the problem and switch it back on under Edit. Delivery history is kept for 30 days.

Handling events well

  • Respond 2xx within 5 seconds. Acknowledge first, then do the real work asynchronously in a queue. Slow responses are treated as failures and retried.
  • Be idempotent. Retries, and rare duplicate sends, reuse the same event id. Store the ids you have processed and skip repeats.
  • Do not rely on order. call.recording.ready can land before or after call.completed.
  • Fetch when in doubt. GET /calls/{id} always returns the current state of a call.
  • Rotate on suspicion. If a secret leaks, rotate it from Integrations. The old secret stops working immediately.

API questions

Can I use the API without an organization?
No. API keys belong to an organization, and every request only sees that organization's contacts and calls. Create or join an organization in TalkTool, then create the key from its Integrations page.
Is there a sandbox?
Not today. There is a single production environment. To test safely, create a key with only the scopes you need, use a dedicated test contact, and set an external_id you can recognize. Contacts you delete are soft-deleted and disappear from the API.
How do I rotate a key?
Create a second key in Dashboard → Integrations, deploy it to your integration, confirm requests succeed, then revoke the old key. Revoked keys stop working immediately and return 401 unauthorized.
Are webhooks available?
Yes. Add an endpoint in Dashboard → Integrations → Webhooks to receive signed call.ringing, call.answered, call.completed, call.missed and call.recording.ready events. See the Webhooks section above for payloads, signature verification and retries.
Which API version am I on?
Every response carries a TalkTool-Api-Version header. The current version is 2026-10-15.