Kintara API
Last updated November 6, 2026
Read calls, transcripts, contacts, appointments and leads, create contacts and leads, and get signed webhooks when something happens. Agencies use one key for all their sub-accounts. The machine-readable OpenAPI 3.1 description is at /api/v1/openapi.json; import it into Postman, Insomnia or a code generator.
Quick start
Create a key in Settings → Developers (or, for an agency, on the agency portal’s API page), pick its scopes and copy it: it is shown only once. Then:
curl https://kintara-j2gv.vercel.app/api/v1/me \ -H "Authorization: Bearer $KINTARA_API_KEY"
All requests and responses are JSON over HTTPS. Base URL: https://kintara-j2gv.vercel.app/api/v1. Times are ISO 8601 in UTC, phone numbers E.164, money in cents.
Authentication and scopes
Send the key as a bearer token: Authorization: Bearer <key>. A missing, invalid or revoked key gets 401 with a WWW-Authenticate: Bearer header; a key without the scope an endpoint needs gets 403 insufficient_scope. Keys can be revoked at any time from the same page.
| Scope | Allows |
|---|---|
organizations:read | View sub-accounts |
calls:read | Read calls, transcripts and summaries |
contacts:read | Read contacts |
contacts:write | Create and update contacts |
appointments:read | Read appointments |
leads:read | Read leads |
leads:write | Create and update leads |
webhooks:manage | Manage webhook endpoints |
Sub-accounts (agency keys)
A key made inside a sub-account always acts on that organization. An agency key can act on any of the agency’s sub-accounts: name the one you mean with the Kintara-Organization header or the organization_id query parameter. List them with GET /organizations. Endpoints marked “Organization required” answer 400 organization_required when an agency key names none, and 403 for an organization outside the agency.
curl "https://kintara-j2gv.vercel.app/api/v1/calls?limit=10" \ -H "Authorization: Bearer $KINTARA_API_KEY" \ -H "Kintara-Organization: 0b6f2c1e-7d3a-4e8b-9c5f-1a2b3c4d5e6f"
Rate limits
Requests are counted per one-minute window: 600 per minute shared by an agency’s keys (the agency’s plan can raise it) and 120 per minute for a sub-account’s own keys. Every response carries:
X-RateLimit-Limit: requests allowed in the windowX-RateLimit-Remaining: requests left in itX-RateLimit-Reset: Unix time (seconds) the window resets
Over the limit you get 429 rate_limited with a Retry-After header in seconds; wait that long before retrying. GET /me shows your current limit.
Pagination
List endpoints return the newest first, 25 per page by default. Set limit (1 to 100) and pass the previous page’s next_cursor as cursor until has_more is false.
{
"data": [ ... ],
"has_more": true,
"next_cursor": "eyJ0IjoiMjAyNi0xMS0wNlQxNTowNDowNS4wMDBaIiwiaSI6Ii4uLiJ9"
}Versioning
The major version is part of the URL (/api/v1) and every response carries a Kintara-Api-Version header. Additive changes (new fields, endpoints, optional parameters and webhook event types) ship in place, so ignore fields you do not know. Breaking changes only ship as a new major version.
- A deprecated version answers with
Deprecation,SunsetandLinkheaders and stays available for at least 180 days after it is deprecated. GET /api/versionslists every version and its status: /api/versions.- An unknown version (for example
/api/v2/calls) returns404unsupported_api_version. - Every change is announced in the changelog.
Errors
Errors use the HTTP status and a JSON body with a stable code:
{
"error": {
"code": "invalid_parameter",
"message": "limit must be between 1 and 100.",
"details": { "param": "limit" }
}
}400invalid_request,invalid_parameter,organization_required401unauthorized: missing, invalid or revoked key403forbidden,insufficient_scope404not_found: no such record in this organization429rate_limited;500server_error(safe to retry)
Endpoints
Paths are relative to https://kintara-j2gv.vercel.app. Endpoints marked with an organization also take Kintara-Organization / organization_id.
Account
The API key itself.
/api/v1/meThe API key: its owner, scopes and rate limit
Scope: any valid keyOrganization optionalReturns 200
Organizations
Sub-accounts (an agency's clients).
/api/v1/organizationsList sub-accounts
An agency key lists its sub-accounts; a sub-account key gets its own organization.
Scope: organizations:readNo organizationReturns 200
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100 (default 25). |
cursor | query | string | `next_cursor` from the previous page. |
/api/v1/organizations/{id}Get a sub-account
Scope: organizations:readNo organizationReturns 200
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The organization id. |
Calls
Inbound and outbound phone calls, with transcripts.
/api/v1/callsList calls
Scope: calls:readOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100 (default 25). |
cursor | query | string | `next_cursor` from the previous page. |
since | query | string (date-time) | Only calls created at or after this time (ISO 8601). |
until | query | string (date-time) | Only calls created before this time (ISO 8601). |
status | query | string | Only calls with this status; comma-separate several.One of: queued, ringing, in_progress, completed, busy, no_answer, failed, canceled, voicemail |
/api/v1/calls/{id}Get a call with its transcript and extracted data
Scope: calls:readOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The call id. |
Contacts
People and businesses in the CRM.
/api/v1/contactsList contacts
Scope: contacts:readOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100 (default 25). |
cursor | query | string | `next_cursor` from the previous page. |
q | query | string | Search name, phone, email or company. |
phone | query | string | Exact phone match (normalized to E.164). |
email | query | string (email) | Exact email match. |
/api/v1/contactsCreate a contact (or match an existing one)
Returns 201 with a new contact, or 200 with the existing contact that has the same phone or email.
Scope: contacts:writeOrganization required (agency keys name it)Returns 200 / 201
JSON body
A new contact needs at least a name, phone or email.
| Field | Type | Notes |
|---|---|---|
first_name | string | |
last_name | string | |
company_name | string | |
phone | string | Phone number, ideally E.164 (+15551234567). |
email | string (email) | |
tags | string[] | |
notes | string | |
sms_opt_out | boolean | |
email_opt_out | boolean | |
call_opt_out | boolean |
/api/v1/contacts/{id}Get a contact
Scope: contacts:readOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The contact id. |
/api/v1/contacts/{id}Update a contact
Only the fields sent change; send null to clear a field.
Scope: contacts:writeOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The contact id. |
JSON body
A new contact needs at least a name, phone or email.
| Field | Type | Notes |
|---|---|---|
first_name | string | |
last_name | string | |
company_name | string | |
phone | string | Phone number, ideally E.164 (+15551234567). |
email | string (email) | |
tags | string[] | |
notes | string | |
sms_opt_out | boolean | |
email_opt_out | boolean | |
call_opt_out | boolean |
Appointments
Booked appointments.
/api/v1/appointmentsList appointments
Scope: appointments:readOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100 (default 25). |
cursor | query | string | `next_cursor` from the previous page. |
since | query | string (date-time) | Only appointments starting at or after this time (ISO 8601). |
until | query | string (date-time) | Only appointments starting before this time (ISO 8601). |
status | query | string | Only appointments with this status; comma-separate several.One of: scheduled, confirmed, completed, canceled, no_show, rescheduled |
Leads
Leads and their pipeline status.
/api/v1/leadsList leads
Scope: leads:readOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100 (default 25). |
cursor | query | string | `next_cursor` from the previous page. |
status | query | string | Only leads with this status; comma-separate several.One of: new, contacted, qualified, appointment_booked, won, lost, unresponsive |
source | query | string | Only leads with this source; comma-separate several.One of: inbound_call, web_form, meta_lead_ad, sms, email, referral, import, campaign, manual, other |
since | query | string (date-time) | Only leads created at or after this time (ISO 8601). |
until | query | string (date-time) | Only leads created before this time (ISO 8601). |
/api/v1/leadsCreate a lead
Matches or creates the contact, then runs the same follow-ups as a lead made in the app: activity, lead.created event, nurture workflows and CRM sync.
Scope: leads:writeOrganization required (agency keys name it)Returns 201
JSON body
The contact comes from `contact` or from the top-level first_name / last_name / company_name / phone / email; it is matched to an existing contact by phone or email. A lead needs a contact name, phone or email.
| Field | Type | Notes |
|---|---|---|
source | string | Default manualOne of: inbound_call, web_form, meta_lead_ad, sms, email, referral, import, campaign, manual, other |
source_detail | string | |
service_interest | string | |
estimated_value_cents | integer | |
notes | string | |
utm | object | |
contact | object | |
first_name | string | |
last_name | string | |
company_name | string | |
phone | string | |
email | string (email) |
Webhooks
Signed event delivery, REST hooks for Zapier and Make.
/api/v1/webhooksList webhook endpoints
The organization's endpoints, or the agency's own when an agency key names no organization.
Scope: webhooks:manageOrganization optionalReturns 200
/api/v1/webhooksSubscribe a URL to events
Also the Zapier / Make REST hook subscribe call. The response carries the signing secret, shown only once.
Scope: webhooks:manageOrganization optionalReturns 201
JSON body
`url` or `target_url` is required (https). Zapier / Make send `target_url` + `event_type`.
| Field | Type | Notes |
|---|---|---|
url | string (uri) | |
target_url | string (uri) | Alias of url (REST hooks). |
event_types | string[] | See event types |
event_type | string | One event type (REST hooks).See event types |
description | string | |
source | string | Default apiOne of: api, zapier, make |
/api/v1/webhooks/{id}Get a webhook endpoint
Scope: webhooks:manageOrganization optionalReturns 200
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The webhook endpoint id. |
/api/v1/webhooks/{id}Unsubscribe (delete a webhook endpoint)
Also the Zapier / Make REST hook unsubscribe call.
Scope: webhooks:manageOrganization optionalReturns 200
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The webhook endpoint id. |
/api/v1/eventsRecent events in the webhook payload shape
The polling and sample endpoint for Zapier / Make. With `sample=1` and no real events yet it returns realistic sample payloads.
Scope: webhooks:manageOrganization required (agency keys name it)Returns 200
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100 (default 25). |
cursor | query | string | `next_cursor` from the previous page. |
type | query | string | Only events with this type; comma-separate several.One of: call.completed, lead.created, appointment.booked, appointment.cancelled, message.received, integration.status_changed, integration.records.booking.confirmed, integration.records.booking.failed, integration.records.appointment.cancelled, integration.records.appointment.completed, integration.records.appointment.no_show, integration.records.patient.updated, intake.completed |
sample | query | string | 1 or true: return sample payloads when there are no real events yet.One of: 1, true |
/api/v1/event-typesEvent types a webhook can subscribe to
Scope: any valid keyNo organizationReturns 200
Webhooks
Subscribe an https URL with POST /webhooks (or in Settings → Developers). Kintara then POSTs each matching event to it as JSON:
{
"id": "8f0c5a8e-2b7e-4f6a-9d0e-3f1c2b4a5d6e",
"type": "call.completed",
"created_at": "2026-11-06T15:04:05.000Z",
"organization_id": "0b6f2c1e-7d3a-4e8b-9c5f-1a2b3c4d5e6f",
"data": {
"call_id": "c1a2b3d4-e5f6-4789-8abc-def012345678",
"status": "completed",
"duration_seconds": 184
}
}Every delivery carries these headers:
X-Kintara-Signature:v1=+ hex HMAC-SHA256 of"<timestamp>.<raw body>", keyed with the endpoint’s secretX-Kintara-Timestamp: Unix time (seconds) of signing; reject anything older than a few minutesX-Kintara-Event: the event typeX-Kintara-Delivery: the delivery id
Answer with any 2xx within 10 seconds. Other answers and timeouts are retried with growing delays, up to 8 attempts; delivery is at-least-once, so de-duplicate on the payload’s id, which stays the same across retries. Answering 410 Gone disables the endpoint, and an endpoint is disabled automatically after 20 failed deliveries in a row (re-enable it in Settings → Developers).
Verifying the signature (Node.js)
import crypto from "node:crypto";
import express from "express";
const app = express();
// Keep the raw body: the signature covers the exact bytes we sent.
app.post("/kintara/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const secret = process.env.KINTARA_WEBHOOK_SECRET; // from POST /api/v1/webhooks
const timestamp = req.get("X-Kintara-Timestamp") ?? "";
const signature = req.get("X-Kintara-Signature") ?? "";
const body = req.body.toString("utf8");
const expected =
"v1=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // 5 minutes
if (!valid || !fresh) return res.status(400).send("Invalid signature");
const event = JSON.parse(body); // { id, type, created_at, organization_id, data }
// Deliveries are at-least-once: skip event.id values you have already handled.
res.sendStatus(200); // answer quickly, do slow work in the background
});Event types
| Event | When |
|---|---|
call.completed | A phone call (inbound or outbound) has ended. |
lead.created | A new lead was captured. |
appointment.booked | An appointment was booked. |
appointment.cancelled | An appointment was cancelled. |
message.received | A text or other message was received. |
integration.status_changed | An integration connection changed status (for example needs reconnecting). |
integration.records.booking.confirmed | A booking in the connected practice system was confirmed. |
integration.records.booking.failed | A booking in the connected practice system failed. |
integration.records.appointment.cancelled | An appointment was cancelled in the connected practice system. |
integration.records.appointment.completed | An appointment was completed in the connected practice system. |
integration.records.appointment.no_show | A patient did not show up, per the connected practice system. |
integration.records.patient.updated | A patient was updated in the connected practice system. |
intake.completed | A patient completed an intake form. |
Leave event_types empty to receive every event. GET /event-types returns this list.
Zapier and Make (REST hooks)
The webhook endpoints follow the REST hooks pattern, so a Zapier or Make app can use instant triggers with an API key (scope webhooks:manage):
- Subscribe:
POST /api/v1/webhookswith{ "target_url": "...", "event_type": "lead.created", "source": "zapier" }. Store the returnedid. - Unsubscribe:
DELETE /api/v1/webhooks/{id}when the Zap or scenario is turned off. - Sample data:
GET /api/v1/events?type=lead.created&sample=1returns recent events in the exact webhook payload shape, or realistic samples when the account has none yet. It also works as a polling trigger. - Cleanup: if the hook URL answers
410 Gone, Kintara disables the subscription and stops sending.
curl -X POST https://kintara-j2gv.vercel.app/api/v1/webhooks \
-H "Authorization: Bearer $KINTARA_API_KEY" \
-H "Kintara-Organization: 0b6f2c1e-7d3a-4e8b-9c5f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{"target_url":"https://hooks.zapier.com/hooks/standard/123/abc","event_type":"lead.created","source":"zapier"}'Questions or a missing endpoint? Write to privacy@kintaragroup.com.