Developers / API reference

TimberSwitch API reference

The HTTP routes you can call today with a TimberSwitch API key: calls, numbers, voicemail, recordings, contacts, text conversations and the Zapier app routes. Machine-readable spec: /api/openapi/v1.json (OpenAPI 3.1).

This is a preview. The routes below work and are what our own apps use, but they predate the planned public v1 (see Changelog), so pagination and error shapes are not uniform yet. Each gap is called out where it applies.

Getting an API key

  1. Sign in and open Account > API keys.
  2. Name the key and pick its scopes. A key can only do what its scopes allow; a key with no scopes selected gets read-only profile and workspace access.
  3. Under Workspace, limit the key to one workspace. We recommend this for every integration: a bound key is refused (403) on any other workspace's routes. It is required for the zapier scope.
  4. Optionally set an expiry. Copy the key when it is shown: we store only a hash and cannot show it again. Keys look like bz_live_ followed by 32 characters.

The key acts as you. Routes that need a workspace owner or admin still check your role in that workspace, and a suspended member's key stops working there.

ScopeWhat it allows on these routes
read:userGET /api/v1/me
read:orgList and read your workspaces
read:telephonyRead numbers, extensions, calls, voicemail, recordings, contacts and texts
write:telephonyMark voicemail read or archived; create, update and import contacts
delete:dataDelete contacts
zapierOnly the /api/zapier/* routes (and /api/v1/me); the key must be bound to one workspace you own or administer

Authentication

Send the key on every request, in either header:

X-API-Key: bz_live_...
Authorization: Bearer bz_live_...

Keys in the query string are refused. Use HTTPS only. Find your workspace slug with GET /api/v1/orgs:

curl https://www.timberswitch.com/api/v1/orgs -H "X-API-Key: $TS_KEY"
curl "https://www.timberswitch.com/api/v1/orgs/your-slug/calls?limit=20" -H "X-API-Key: $TS_KEY"

Rate limits

Today there is one limit for API traffic: 100 requests per minute per user (fixed window). All of a user's keys and browser sessions share that budget. Over it, you get 429 Too Many Requests with an empty body and no Retry-After header, so back off for up to a minute and retry. Texts sent through the API also count against the workspace's daily texting caps.

Planned for v1: per-key limits by cost group, RateLimit-* headers and Retry-After.

Pagination

List routes return a plain JSON array, newest first, and take a limit query parameter that is clamped to a maximum (200 for calls, voicemail and conversations; 500 for contacts and messages). There is no cursor yet: you can read the most recent page only. For a full history, sync forward with webhooks and keep your own copy.

Planned for v1: ?limit=&cursor= with { "data": [...], "has_more": true, "next_cursor": "..." }.

Errors

Status codes are reliable; bodies are not uniform yet. What you will see today:

StatusMeaningBody
400Invalid input{ "error": "e164 must be in +1NNN format" }
401No valid keyempty
403Missing scope{ "error": "Insufficient scope", "required": "read:telephony" }
403Key bound to another workspace{ "error": "API key is not valid for this organization" }
403Needs a workspace owner or admin{ "error": "manager_only", "message": "..." }
403Not a member, or suspendedempty
404No such workspace or item, or it is in another workspaceempty
429Rate limitedempty

The Zapier routes always return { "error": "code", "message": "..." }, with codes such as insufficient_scope, key_not_bound, invalid_to and suppressed.

Planned for v1: one envelope, { "error": { "type", "code", "message", "param", "request_id" } }, on every error.

Webhooks and signing

Add an https endpoint in the app under Phone > Webhooks, with an event filter: *, a list such as call.completed,sms.received, or a prefix such as contact.*. Zapier subscribes its own endpoints through POST /api/zapier/hooks.

Every delivery is a POST with this body:

{ "id": "evt guid", "type": "call.completed", "created": 1759500000, "data": { ... } }
EventKey fields in data
call.completedcall_id, direction, from, to, from_name, status, started_at, answered_at, ended_at, duration_seconds, hangup_cause
call.missedSame as call.completed, for an inbound call nobody answered. Sent only to endpoints that name it exactly.
voicemail.receivedvoicemail_id, from, duration_seconds, audio_endpoint, transcript, received_at
sms.receivedmessage_id, thread_id, from, to_phone_number_id, body, media_urls, received_at, is_stop, is_help
contact.created, contact.updated, contact.deletedcontact_id, e164, first_name, last_name, company (plus email, tags, display_name, updated_at on create and update)

Verifying the signature

Each request carries two headers:

Timberswitch-Timestamp: 1759500000
Timberswitch-Signature: sha256=<hex HMAC-SHA256 of "{timestamp}.{raw body}" keyed with your endpoint's secret>

The secret (whsec_...) is shown when you create the endpoint. Compute the HMAC over the raw request bytes, before any JSON parsing, compare in constant time, and reject timestamps more than 5 minutes from your clock to stop replays. Answer with any 2xx within 10 seconds.

curl / shell (check a captured delivery):

# SECRET = your endpoint's whsec_... secret, TS = the Timberswitch-Timestamp header,
# body.json = the raw request body exactly as received.
{ printf '%s.' "$TS"; cat body.json; } \
  | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.*= /sha256=/'
# The output must equal the Timberswitch-Signature header.

Node.js

const crypto = require('crypto');

// rawBody: the request body as a string or Buffer, before JSON.parse
// (in Express: express.raw({ type: 'application/json' })).
function verifyTimberswitch(rawBody, headers, secret) {
  const ts = headers['timberswitch-timestamp'];
  const sig = headers['timberswitch-signature'] || '';
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(ts + '.').update(rawBody).digest('hex');
  const a = Buffer.from(sig), b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

C#

using System.Security.Cryptography;
using System.Text;

// rawBody: the request body exactly as received (read it before deserializing).
static bool VerifyTimberswitch(string rawBody, string timestamp, string signature, string secret)
{
    if (!long.TryParse(timestamp, out var ts)
        || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return false;
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(timestamp + "." + rawBody));
    var expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature ?? ""));
}

Retries: a delivery that fails or gets a non-2xx response is retried with exponential backoff (10 s, 20 s, 40 s and so on), up to 8 attempts in total, then dropped. The id is the same on every retry of an event, so use it to ignore duplicates.

Planned for v1: Standard Webhooks signing (webhook-id, webhook-timestamp, webhook-signature) alongside today's headers, secret rotation, and a delivery log you can replay from.

Reference

Base URL https://www.timberswitch.com. Version: v1 (preview, 2026-10-04).

Account

Who is calling, and which workspaces they can reach.

GET /api/v1/me

Who am I

The signed-in user and, for an API key, the key's id, name and workspace binding. A good first call to check a key works.

Scope: read:user or zapier · Who: the caller (no workspace role needed)

ResponseBodyWhen
200MeResponseOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs

List my workspaces

Every workspace the key's user belongs to. Use the slug in the /api/v1/orgs/{slug}/... routes. A key bound to one workspace still lists all of the user's workspaces, but only works inside its own.

Scope: read:org · Who: the caller (no workspace role needed)

ResponseBodyWhen
200WorkspaceSummary[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}

Get a workspace

One workspace and the caller's role in it.

Scope: read:org · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
ResponseBodyWhen
200WorkspaceDetailOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

Numbers and extensions

The workspace's phone numbers and extensions (read only).

GET /api/v1/orgs/{slug}/numbers

List phone numbers

The workspace's phone numbers. Buying and releasing numbers is done in the app, not over the API.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
ResponseBodyWhen
200PhoneNumberDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}/extensions

List extensions

The workspace's extensions.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
ResponseBodyWhen
200ExtensionDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

Calls

The call log and per-number caller context.

GET /api/v1/orgs/{slug}/caller-context

Caller context for a number

Everything the workspace has on one outside number in a single call: the saved contact, the last 5 calls, the last 3 voicemails and the latest text. Built for screen pops. `e164` is required.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
e164querystringyes
ResponseBodyWhen
200CallerContextDtoOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
400ApiErrorBodyThe request is invalid.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}/calls

List recent calls

Newest first. `limit` defaults to 50 and is clamped to 1-200. There is no cursor or date filter yet.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
limitqueryinteger (int32)no
ResponseBodyWhen
200CallDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

Voicemail

Voicemails with transcripts, read/archive state and audio.

GET /api/v1/orgs/{slug}/voicemails

List voicemails

Newest first, with the transcript when it is ready. `limit` defaults to 50 and is clamped to 1-200. There is no cursor yet.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
includeArchivedquerybooleanno
limitqueryinteger (int32)no
ResponseBodyWhen
200VoicemailDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/v1/orgs/{slug}/voicemails/{id}/read

Mark a voicemail read

Marks the voicemail read (audited).

Scope: write:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
204No Content
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/v1/orgs/{slug}/voicemails/{id}/archive

Archive a voicemail

Hides the voicemail from the default list (includeArchived=true still returns it).

Scope: write:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
204No Content
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}/voicemails/{id}/audio

Get voicemail audio

Redirects (302) to a presigned audio url that expires in 15 minutes, or streams audio/mpeg. Every fetch is written to the workspace audit log. Don't store the redirect target; ask again when you need it.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
302Found
200OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

Recordings

Call-recording audio.

GET /api/v1/orgs/{slug}/recordings/{id}/audio

Get call-recording audio

Redirects (302) to a presigned audio url that expires in 15 minutes, or streams audio/mpeg. Every fetch is written to the workspace audit log. There is no recording list endpoint yet.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
302Found
200OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

Contacts

The shared address book.

GET /api/v1/orgs/{slug}/contacts

List or search contacts

`search` matches name, company, email or number. `limit` defaults to 100 and is clamped to 1-500. There is no cursor yet.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
searchquerystringno
limitqueryinteger (int32)no
ResponseBodyWhen
200ContactDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/v1/orgs/{slug}/contacts

Create or update a contact by number

Upsert by E.164: posting a number that already exists updates that contact instead of failing, so retries are safe. Any member may add a new contact; overwriting an existing one needs an owner or admin. Fires `contact.created` or `contact.updated`.

Scope: write:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes

Body: ContactWriteRequest (application/json)

ResponseBodyWhen
200ContactDtoOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
400ApiErrorBodyThe request is invalid.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}/contacts/{id}

Get a contact

One contact by id.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
200ContactDtoOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
PATCH /api/v1/orgs/{slug}/contacts/{id}

Update a contact

Partial update by id (PATCH and PUT behave the same). Fields left null keep their value; an empty string clears a field. The number cannot be changed.

Scope: write:telephony · Who: a workspace owner or admin

ParameterInTypeRequired
slugpathstringyes
idpathstringyes

Body: ContactWriteRequest (application/json)

ResponseBodyWhen
200ContactDtoOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member, or is not an owner or admin (manager_only).
400ApiErrorBodyThe request is invalid.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
PUT /api/v1/orgs/{slug}/contacts/{id}

Update a contact

Partial update by id (PATCH and PUT behave the same). Fields left null keep their value; an empty string clears a field. The number cannot be changed.

Scope: write:telephony · Who: a workspace owner or admin

ParameterInTypeRequired
slugpathstringyes
idpathstringyes

Body: ContactWriteRequest (application/json)

ResponseBodyWhen
200ContactDtoOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member, or is not an owner or admin (manager_only).
400ApiErrorBodyThe request is invalid.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
DELETE /api/v1/orgs/{slug}/contacts/{id}

Delete a contact

Removes the contact. Fires `contact.deleted`.

Scope: delete:data · Who: a workspace owner or admin

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
204No Content
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member, or is not an owner or admin (manager_only).
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/v1/orgs/{slug}/contacts/import

Import contacts from CSV

Multipart upload of one CSV file (5 MB max). Rows upsert by number. Returns counts and a per-row error list.

Scope: write:telephony · Who: a workspace owner or admin

ParameterInTypeRequired
slugpathstringyes

Body: object (multipart/form-data)

ResponseBodyWhen
200ContactImportResultOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member, or is not an owner or admin (manager_only).
400ApiErrorBodyThe request is invalid.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

SMS

Text conversations and their messages (read only; see the Zapier send-sms action for sending).

GET /api/v1/orgs/{slug}/sms/threads

List text conversations

Most recent first. `limit` defaults to 50 and is clamped to 1-200. There is no cursor yet.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
limitqueryinteger (int32)no
ResponseBodyWhen
200SmsThreadDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}/sms/threads/{id}

Get a text conversation

One conversation by id.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
ResponseBodyWhen
200SmsThreadDtoOK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/v1/orgs/{slug}/sms/threads/{id}/messages

List messages in a conversation

`limit` defaults to 100 and is clamped to 1-500. There is no cursor yet.

Scope: read:telephony · Who: any non-suspended member of the workspace

ParameterInTypeRequired
slugpathstringyes
idpathstringyes
limitqueryinteger (int32)no
ResponseBodyWhen
200SmsMessageDto[]OK
401No valid API key or session.
403ApiErrorBodyThe key lacks the scope, is bound to a different workspace, the caller is not a (non-suspended) member.
404The workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.

Zapier app

The routes the TimberSwitch Zapier app uses. The workspace comes from the key, not the URL.

POST /api/zapier/hooks

Subscribe a REST hook

Creates a webhook subscription for one trigger (call_completed, missed_call, voicemail_received, sms_received). target_url must be an https Zapier hook url. The response carries the signing secret once. At most 50 per workspace (429 after that).

Scope: zapier · Who: an X-API-Key bound to one workspace whose owner is an owner or admin there

Body: ZapierSubscribeRequest (application/json)

ResponseBodyWhen
201ZapierSubscribeResponseCreated
401ZapierErrorBodyNo valid API key or session.
403ZapierErrorBodyThe key lacks the zapier scope, is not bound to one workspace, or its owner is not an owner or admin there.
400ZapierErrorBodyThe request is invalid.
429Rate limited. The body is empty and no Retry-After header is sent today.
DELETE /api/zapier/hooks/{id}

Unsubscribe a REST hook

Removes a Zapier subscription of the key's workspace.

Scope: zapier · Who: an X-API-Key bound to one workspace whose owner is an owner or admin there

ParameterInTypeRequired
idpathstringyes
ResponseBodyWhen
200ZapierUnsubscribeResponseOK
401ZapierErrorBodyNo valid API key or session.
403ZapierErrorBodyThe key lacks the zapier scope, is not bound to one workspace, or its owner is not an owner or admin there.
404ZapierErrorBodyThe workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
GET /api/zapier/triggers/{trigger}

List recent items for a trigger

Newest first, in the same shape the webhook delivers (its data object). ai_message_taken is polled here; the others are samples for Zap setup. `limit` defaults to 10.

Scope: zapier · Who: an X-API-Key bound to one workspace whose owner is an owner or admin there

ParameterInTypeRequired
triggerpathstringyes
limitqueryinteger (int32)no
ResponseBodyWhen
200object[]OK
401ZapierErrorBodyNo valid API key or session.
403ZapierErrorBodyThe key lacks the zapier scope, is not bound to one workspace, or its owner is not an owner or admin there.
404ZapierErrorBodyThe workspace or the item does not exist, or is not in this workspace.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/zapier/actions/send-sms

Send a text

Sends an SMS from one of the workspace's numbers (from_number is required when there are several). Send an idempotency_key: a retry with the same key returns the first result with duplicate=true and sends nothing. Texting must be on for the workspace; STOP replies, daily caps and US/Canada-only rules apply. 1600 characters max.

Scope: zapier · Who: an X-API-Key bound to one workspace whose owner is an owner or admin there

Body: ZapierSendSmsRequest (application/json)

ResponseBodyWhen
200ZapierSendSmsResponseOK
409ZapierErrorBodyConflict
422ZapierErrorBodyUnprocessable Entity
502ZapierErrorBodyBad Gateway
401ZapierErrorBodyNo valid API key or session.
403ZapierErrorBodyThe key lacks the zapier scope, is not bound to one workspace, or its owner is not an owner or admin there.
400ZapierErrorBodyThe request is invalid.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/zapier/actions/block-number

Block a number

Adds a number to the workspace's call block list (upsert, so a retry does not add a second row).

Scope: zapier · Who: an X-API-Key bound to one workspace whose owner is an owner or admin there

Body: ZapierBlockRequest (application/json)

ResponseBodyWhen
200ZapierBlockResponseOK
401ZapierErrorBodyNo valid API key or session.
403ZapierErrorBodyThe key lacks the zapier scope, is not bound to one workspace, or its owner is not an owner or admin there.
400ZapierErrorBodyThe request is invalid.
429Rate limited. The body is empty and no Retry-After header is sent today.
POST /api/zapier/actions/create-contact

Create or update a contact

Upsert by number; created=false means an existing contact was updated.

Scope: zapier · Who: an X-API-Key bound to one workspace whose owner is an owner or admin there

Body: ZapierContactRequest (application/json)

ResponseBodyWhen
200ZapierContactResponseOK
401ZapierErrorBodyNo valid API key or session.
403ZapierErrorBodyThe key lacks the zapier scope, is not bound to one workspace, or its owner is not an owner or admin there.
400ZapierErrorBodyThe request is invalid.
429Rate limited. The body is empty and no Retry-After header is sent today.

Schemas

JSON field names are camelCase on the /api/v1 routes and snake_case on the Zapier routes and in webhook payloads. Unknown fields may be added at any time; ignore them.

ApiErrorBody - Error body. Today's routes use more than one shape; every field is optional. See /developers/api#errors.
FieldTypeNotes
errorstring | nullA short code or message, for example "Insufficient scope" or "manager_only".
messagestring | nullA sentence for people, when present.
requiredstring | nullThe scope the key is missing, on a scope error.
CallDto - A call in the workspace's call log.
FieldTypeNotes
idstring
directionstringinbound or outbound.
statusstring
fromstring
tostring
fromNamestring | null
fromCitystring | null
fromStatestring | null
fromCountrystring | null
startedAtstring (date-time)
answeredAtstring (date-time) | null
endedAtstring (date-time) | null
durationSecondsinteger (int32)
CallerContextCall
FieldTypeNotes
idstring
directionstring
statusstring
startedAtstring (date-time)
durationSecondsinteger (int32)
CallerContextContact
FieldTypeNotes
idstring
firstNamestring | null
lastNamestring | null
companystring | null
emailstring | null
notesstring | null
tagsstring | null
isVerifiedboolean
displayNamestring
CallerContextDto - Everything the workspace knows about one outside number.
FieldTypeNotes
e164string
contactany | null | CallerContextContact
recentCallsCallerContextCall[]
voicemailsCallerContextVoicemail[]
smsany | null | CallerContextSms
CallerContextSms
FieldTypeNotes
threadIdstring
unreadinteger (int32)
lastany | null | CallerContextSmsMessage
CallerContextSmsMessage
FieldTypeNotes
directionstring
bodystring
sentAtstring (date-time)
CallerContextVoicemail
FieldTypeNotes
idstring
receivedAtstring (date-time)
durationSecondsinteger (int32)
transcriptstring | null
ContactDto - An address-book contact.
FieldTypeNotes
idstring
e164string
firstNamestring | null
lastNamestring | null
companystring | null
emailstring | null
notesstring | null
tagsstring | nullComma-separated tags.
isVerifiedboolean
displayNamestring
createdAtstring (date-time)
updatedAtstring (date-time)
ContactImportResult - Result of a CSV contact import.
FieldTypeNotes
createdinteger (int32)
updatedinteger (int32)
skippedinteger (int32)
errorsstring[]
ContactWriteRequest
FieldTypeNotes
e164string
firstNamestring | null
lastNamestring | null
companystring | null
emailstring | null
notesstring | null
tagsstring | null
ExtensionDto - An extension (a person or a ring target inside the workspace).
FieldTypeNotes
idstring
numberstringThe dialable extension, for example 101.
namestring
userIdstring | nullThe user who owns the extension, if any.
ringWebRTCbooleanWhether browser and app softphones ring.
ringTimeoutSecondsinteger (int32)
forwardToE164string | null
MeResponse - The caller: the signed-in user, and the API key when one was used.
FieldTypeNotes
userIdstring
emailstring | null
namestring | null
rolestring | null
authMethodstring"api_key" for a key, "cookie" for a browser session.
apiKeyIdstring | null
apiKeyNamestring | null
apiKeyOrganizationIdstring | nullThe workspace id the key is bound to, or null for an unbound key.
PhoneNumberDto - A phone number owned by the workspace.
FieldTypeNotes
idstring
e164stringE.164, for example +15551234567.
labelstring
numberTypestringlocal or toll-free.
isActiveboolean
createdAtstring (date-time)
SmsMessageDto - One SMS or MMS message in a thread.
FieldTypeNotes
idstring
directionstringinbound or outbound.
bodystring
mediaUrlsstring | nullJSON array of media URLs, as a string, for MMS.
statusstring
failureReasonstring | null
sentAtstring (date-time)
deliveredAtstring (date-time) | null
SmsThreadDto - An SMS conversation with one outside number.
FieldTypeNotes
idstring
contactE164string
contactNamestring | null
phoneNumberIdstring
ourE164string | nullThe workspace number the conversation is on.
unreadCountinteger (int32)
lastMessageAtstring (date-time)
isArchivedboolean
createdAtstring (date-time)
VoicemailDto - A voicemail left for the workspace.
FieldTypeNotes
idstring
fromE164string
audioUrlstringThe authenticated audio route for this voicemail. Send your API key to fetch it.
durationSecondsinteger (int32)
transcriptstring | null
transcriptStatusstringpending, processing, complete, empty, skipped or failed.
readAtstring (date-time) | null
isArchivedboolean
createdAtstring (date-time)
WorkspaceDetail - A workspace and the caller's role in it.
FieldTypeNotes
idstring
slugstring
namestring
planstring
logoUrlstring | null
trialEndsAtstring (date-time) | null
rolestringowner, admin or member.
WorkspaceSummary - A workspace (organization) the caller belongs to.
FieldTypeNotes
idstring
slugstringUse this in /api/v1/orgs/{slug}/... paths.
namestring
planstring
logoUrlstring | null
trialEndsAtstring (date-time) | null
ZapierBlockRequest
FieldTypeNotes
numberstring | null
reasonstring | null
ZapierBlockResponse
FieldTypeNotes
idstring
numberstring
reasonstring | null
expires_atstring (date-time) | null
ZapierContactRequest
FieldTypeNotes
numberstring | null
first_namestring | null
last_namestring | null
companystring | null
emailstring | null
notesstring | null
tagsstring | null
ZapierContactResponse
FieldTypeNotes
idstring
numberstring
first_namestring | null
last_namestring | null
companystring | null
emailstring | null
display_namestring
createdboolean
ZapierErrorBody - Zapier error body.
FieldTypeNotes
errorstring
messagestring
ZapierSendSmsRequest
FieldTypeNotes
tostring | null
bodystring | null
from_numberstring | null
idempotency_keystring | null
ZapierSendSmsResponse
FieldTypeNotes
idstring
statusstring
tostring
thread_idstring
sent_atstring (date-time)
duplicatebooleantrue when this idempotency_key was already sent; nothing new went out.
ZapierSubscribeRequest
FieldTypeNotes
target_urlstring | null
triggerstring | null
ZapierSubscribeResponse - A REST-hook subscription.
FieldTypeNotes
idstring
triggerstring
eventstring | null
signing_secretstringShown once. Verify the Timberswitch-Signature header of each delivery with it.
ZapierUnsubscribeResponse
FieldTypeNotes
idstring
deletedboolean

Changelog

2026-10-04
First published spec (/api/openapi/v1.json) and this reference. Response bodies are now typed projections; the field names did not change. Two fields changed: voicemail audioUrl is always our authenticated audio route, and SMS messages no longer include the carrier's internal message id.
Next: public v1
A versioned /v1 surface with keys bound to one workspace, resource scopes (calls:read, messages:send and so on), cursor pagination, one error envelope, Idempotency-Key, per-key rate-limit headers, sending texts and managing webhook endpoints over the API, and Standard Webhooks signing. The routes on this page keep working through the change, with notice before anything is retired.