Developers

REST API and webhooks.

Read and manage everything on your agency account, and push real-time call and booking events into any outside system. A Scale-plan feature, with API keys, scoped access, and signed webhooks. Every endpoint and event below lists a short "when you would use it", plus copyable curl, worked JSON, and named integration recipes, so you (or an AI assistant) can wire up an integration fast.

Get API keys
Bearer API key 13 signed events Live booking data

Plan

API keys, the REST API, and webhooks are a Scale plan feature. Call-center agencies get them on any plan, and agencies on a free trial are treated as Scale while trialing. Below that, requests return upgrade_required.

Base URL

Basehttps://urchin-app-bqb4i.ondigitalocean.app/api/v1

All paths below are relative to this base.

Authentication

Every request authenticates with an API key, created in the agency dashboard under Settings → API. The full key is shown once at creation; only a SHA-256 hash and a short prefix are stored, so a lost key is revoked and replaced, never recovered. Keys are agency-scoped.

Authorization: Bearer YOUR_API_KEY
# or, equivalently:
x-api-key: YOUR_API_KEY
read

Default. All GET endpoints.

read_write

POST, PATCH, PUT, DELETE. A write with a read key returns 403 insufficient_scope.

Your first request

curl
curl https://urchin-app-bqb4i.ondigitalocean.app/api/v1/account \
  -H "Authorization: Bearer YOUR_API_KEY"

Returns the authenticated agency: id, name, plan_type, subscription_status, and the key's scope.

Conventions

Errors

{ "error": { "type": "insufficient_scope", "message": "This API key is read-only." } }

Every error uses that envelope. Types you may see:

401authentication_errorMissing, invalid, or revoked key.
403insufficient_scopeWrite attempted with a read-only key.
403upgrade_requiredAccount is below the Scale plan.
403account_suspendedAgency account is suspended.
404not_foundNo such object, or not owned by your agency.
400invalid_requestBad or missing fields in the request.
429rate_limit_exceededOver 120 requests/min on this key.
501not_implementedEndpoint exists but is not available yet (e.g. DELETE /clients).
400/500api_errorA query or internal error.

Pagination

List endpoints take limit (1–100, default 25) and cursor. Pass the returned cursor back to page.

{ "data": [ ... ], "has_more": true, "next_cursor": "2026-10-20T14:03:00.000Z" }

Single objects return directly. Only allowlisted public fields are returned; internal IDs and secrets never leave the API. (Numbers, usage, analytics, and the event catalog return a plain { data } or object, not a cursor page.)

Rate limits

Each API key is limited to 120 requests per minute. Every response carries the current budget, and going over returns 429 rate_limit_exceeded with a Retry-After header.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1793114400

For a large nightly sync, page with the max limit=100 and, if you hit a 429, wait the window out using Retry-After rather than hammering.

Clients

GET/clientsList clients (paginated). Filter by status or industry.WhenAn agency pulls its whole client roster into its own CRM or billing dashboard each night, so a plumber it onboarded in VoiceAI shows up in its system with no manual entry. Filter ?status=active to skip clients still provisioning, or ?industry=hvac to sync one vertical at a time.
POST/clientsread_writeCreate and auto-provision a client (assistant + phone number). Returns 202 with a provisioning job id.WhenA new HVAC company signs up on the agency’s own website and this creates the client and provisions its AI number in the background, no logging into VoiceAI. The call returns a job id immediately; poll provisioning-status to show a live "setting up your receptionist" state instead of freezing the signup form.
GET/clients/:idOne client.WhenShow one client's live details, provisioned AI number, plan, and status inside the agency's own dashboard, so they never log into VoiceAI to check a single account. Good for a per-client detail page.
PATCH/clients/:idread_writeUpdate a whitelist of client fields (name, city, notification phone, post-call SMS, etc.).WhenA restoration client updates their business name or after-hours notification number in the agency's portal, and this pushes it back to VoiceAI so the records stay in sync. Provisioning-owned fields (the number, assistant ids, billing) stay locked.
DELETE/clients/:idread_writeNot available yet. Returns 501 not_implemented.WhenDeletion has to tear down the paid phone number, the assistant, and billing cleanly, so for now offboard a client from the dashboard. A write path for this is coming.
GET/clients/:id/provisioning-status/:jobIdPoll a provisioning job after a create.WhenDrive the onboarding screen right after a create: "finding a local number... building your receptionist... ready." Stop polling when status is done, and read the provisioned phone number from the result to show the new client the number to forward their line to.

Example: create a client

curl
curl -X POST https://urchin-app-bqb4i.ondigitalocean.app/api/v1/clients \
  -H "Authorization: Bearer YOUR_READ_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Dana",
    "last_name": "Reyes",
    "email": "dana@brightplumbing.com",
    "phone": "+14045550199",
    "business_name": "Bright Plumbing",
    "industry": "plumbing",
    "business_city": "Atlanta",
    "business_state": "GA",
    "plan_type": "starter"
  }'
response
// 202 Accepted, provisioning runs in the background
{ "client_id": "9f3b...", "job_id": "prov_8c21...", "status": "processing" }

// Then poll:
// GET /clients/9f3b.../provisioning-status/prov_8c21...
// -> { "id": "...", "status": "done", "result": { "phone_number": "+1..." }, "created_at": "..." }

Receptionist & Knowledge base

GET/clients/:id/receptionistCurrent AI config: greeting, voice, human-handoff mode, transfer number, business hours.WhenShow a client's current greeting, voice, and after-hours transfer in the agency's own settings screen, so they can review how a restoration company's emergency line is set up without leaving their tool.
PATCH/clients/:id/receptionistread_writeUpdate the AI config. Takes effect on the next call, no rebuild.WhenA med spa wants a warmer greeting and calls sent to the front desk after 5pm; the agency changes it from their own panel and it lands on the very next call, no rebuild. Also how an agency bulk-applies one on-brand greeting across every client in a vertical.
GET/clients/:id/knowledge-baseThe client's knowledge base content.WhenExport what the AI knows about a plumbing client (hours, service area, pricing) so the agency can review or back it up before a big edit.
PUT/clients/:id/knowledge-baseread_writeReplace the knowledge base content.WhenSync a junk-removal client's pricing and service list from the agency's own source of truth, so when they change their dump fees in one place the AI starts quoting the new number. Writing it rebuilds the assistant knowledge file in the background, so it is heavier than a receptionist edit.

Calls

GET/callsList calls (paginated). Filter by client_id, since, until.WhenAn HVAC agency pulls every call each night so a missed-call lead becomes a morning follow-up task in their CRM. A restoration company syncs full transcripts into a warehouse to audit how the AI handled emergency water-damage calls. Walk pages with cursor and bound each run with ?since so you only fetch new calls.
GET/calls/:idOne call: outcome, captured customer details, summary, transcript, recording.WhenOpen one call's transcript, summary, and captured details in your app, for when a septic client asks "did the AI actually book that pumping job?" and the agency needs the full record. This fetch also resolves a fresh recording link.
GET/calls/:id/recordingA fresh, authenticated recording URL.WhenStored links expire, so call this the instant a user hits play, for example an agency QAing a salon's bookings by listening to the actual calls, or attaching a recording to a dispute.

Example: list and page

curl
curl https://urchin-app-bqb4i.ondigitalocean.app/api/v1/calls?limit=25 \
  -H "Authorization: Bearer YOUR_API_KEY"

# Next page: pass the cursor you got back
curl "https://urchin-app-bqb4i.ondigitalocean.app/api/v1/calls?limit=25&cursor=2026-10-20T14:03:00.000Z" \
  -H "Authorization: Bearer YOUR_API_KEY"
response
{
  "data": [ { "id": "c1a2...", "client_id": "9f3b...", "caller_phone": "+14045551212", "... ": "..." } ],
  "has_more": true,
  "next_cursor": "2026-10-20T14:03:00.000Z"
}

Appointments

GET/clients/:id/availabilityOpen slots for a client on a date. ?date=YYYY-MM-DD (&duration=30).WhenBefore booking, show a caller's real options, say a plumbing client's open 2-hour windows tomorrow, in your own flow or chatbot. Needs a connected calendar to read against; a client on no calendar returns 409 so you can fall back to just capturing the request.
GET/appointmentsList appointments. Filter by client_id, status, since, until.WhenPut a lawn-care client's upcoming jobs on an ops board, or reconcile what the AI booked against the crew's schedule every morning. Filter ?since/?until for a given week.
GET/appointments/:idOne appointment.WhenPull one booking's full details (customer, time, service, where it landed) when a client asks the agency to confirm a specific appointment.
POST/appointmentsread_writeCreate an appointment. Writes the real calendar event when the client has one connected.WhenBook from your own flow, not just the phone AI: a restoration company's website chatbot or the agency's intake form creates the appointment here. A Google-connected client gets a real calendar event (availability-checked); a client with no calendar gets a recorded booking so the call still succeeds. Fires appointment.booked so your reminders and CRM sync run either way.
PATCH/appointments/:idread_writeReschedule to a new date/time.WhenA customer calls back to move their HVAC tune-up to Thursday; reschedule here and the Google event moves with it (a record-only booking just updates). Fires appointment.rescheduled with the old time.
POST/appointments/:id/cancelread_writeCancel an appointment (kept for history).WhenA caller cancels their junk pickup; this frees the slot, removes the calendar event, and fires appointment.canceled so the agency's reminder sequence stops texting them. Optional body: reason.

Appointments are created by the AI during calls and, now, directly through the API. How a write reaches a real calendar depends on what the client has connected. See Calendar booking.

Numbers · Usage · Analytics

GET/numbersProvisioned phone numbers across the agency's clients.WhenAudit which AI number every client runs on across the whole agency, and catch any client still stuck without a provisioned number.
GET/usageMinutes and records per client over a window or billing month.WhenBill your own clients from real minute usage: an agency pulls each HVAC client's monthly minutes to mark up and invoice. Pass ?billing_month=YYYY-MM for a clean monthly total, or ?since/?until for a custom window.
GET/analytics/callsCall aggregates: totals, minutes, spam, by status.WhenFeed call volume, spam counts, and outcomes into an agency reporting dashboard, so they can show a plumbing client "42 calls answered, 11 jobs booked" this month without exporting every call row.

Webhook management

GET/eventsThe event catalog (array of event-type strings).WhenLet an AI assistant or a dynamic settings screen discover the exact event names before subscribing, instead of hardcoding a list that could drift.
POST/webhooksread_writeSubscribe a URL to events. Returns the signing secret once.WhenWire VoiceAI into Zapier, Make, n8n, GoHighLevel, or your own endpoint, e.g. an agency subscribes call.completed to drop every lead into its GoHighLevel workflow. The response includes a whsec_ secret exactly once, store it then, it is never shown again.
GET/webhooksList subscriptions.WhenSee every subscription plus each endpoint's last delivery and last error, so an agency can tell at a glance whether its CRM sync is healthy.
GET/webhooks/:idOne subscription.WhenCheck a single subscription config and health.
PATCH/webhooks/:idread_writeUpdate URL, events, or status (active / disabled).WhenMove your endpoint to a new URL or change which events you get without re-creating the subscription or minting a new secret. Set status disabled to pause delivery during maintenance, then active again.
DELETE/webhooks/:idread_writeDelete a subscription.WhenTurn a subscription off for good when an agency drops an integration.
GET/webhooks/:id/deliveriesDelivery log: status, attempts, response code, stored payload.WhenDebug why your endpoint missed an event, for when a restoration company says "the booking never hit our CRM." Each row shows the attempt count, your endpoint's response code, and the exact payload sent.
POST/webhooks/:id/pingread_writeSend a test ping event to the endpoint.WhenFire a test event to confirm your endpoint and signature check work before going live, so the first real customer call is not also your first test. The ping has type "ping", not a catalog event.

Subscribe

curl
curl -X POST https://urchin-app-bqb4i.ondigitalocean.app/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_READ_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/hooks/voiceai",
    "events": ["call.completed", "appointment.booked"],
    "description": "CRM sync"
  }'
response
// 201 Created, secret is shown ONCE, store it now
{
  "id": "wh_7a9c...",
  "url": "https://yourapp.com/hooks/voiceai",
  "events": ["call.completed", "appointment.booked"],
  "status": "active",
  "description": "CRM sync",
  "created_at": "2026-10-27T15:12:00.000Z",
  "secret": "whsec_1f2e3d4c..."
}

Use "events": ["*"] to subscribe to everything. The secret is returned only on this create call.

Event catalog

call.startedA call begins (live event; carries a call_session_id, not a calls row id).
WhenFlash a live-call light on the agency wall board, or start a screen-pop so a restoration dispatcher sees a call coming in before they answer. Lightweight live signal, not enriched with call details.
call.completedA call ends. Enriched with the full call (see payloads).
WhenThe main real-time hook. The second an HVAC call ends: create a CRM activity from the summary, text the owner if urgency_level is high (a no-heat call in winter), and if the AI captured an appointment push it to a non-Google calendar, all from this one enriched event with no follow-up GET.
call.transferredThe call was transferred to the business line. Enriched like call.completed.
WhenTrack how often a plumbing client’s calls reach a live person and when, for staffing. Or alert the owner that a transferred call is ringing their cell so they answer knowing it is a real customer, not spam.
call.escalatedA call-center agent took over a live call (browser jump-in or their phone was rung).
WhenAlert a call-center manager the moment an agent jumps into a live call, for live-floor staffing and QA. Distinct from call.transferred, which is a dial out to the business line.
appointment.bookedAn appointment was created (by the AI on a call, or via POST /appointments).
WhenMirror the booking into the agency’s ops calendar, fire a confirmation text, and start a reminder sequence, e.g. a salon booking kicks off a 24-hour "see you tomorrow" SMS and a same-day reminder.
appointment.rescheduledAn appointment moved to a new time (via PATCH /appointments/:id). Carries previous_appointment_time.
WhenMove the booking everywhere you mirror it and re-send a confirmation for the new time. previous_appointment_time lets you clear the exact old slot, not guess which one moved.
appointment.canceledAn appointment was canceled (via POST /appointments/:id/cancel). Carries cancel_reason.
WhenFree the slot in your own system and stop the reminder sequence, so a canceled junk pickup never gets a "see you tomorrow" text. cancel_reason feeds your reporting on why jobs fall through.
client.provisionedA new client finished provisioning and is live (self-signup or agency-added).
WhenKick off the agency’s welcome sequence the moment a client goes live: send the "your receptionist is ready, here is your number to forward" email and create the onboarding checklist.
client.provisioning_failedProvisioning failed for a client.
WhenPage the agency if a setup breaks (no local number found, assistant build failed) so they fix it before the client ever notices their line is not answering.
client.trial_startedA free trial began (once per trial).
WhenTag the new member as "trial" in the agency’s CRM and start a trial-nurture sequence that shows the owner their first answered calls.
client.payment_succeededA payment went through. The only event that confirms money moved; is_first_payment marks the trial-to-paid conversion.
WhenMark the client as paying in the CRM the moment the first real charge clears, end trial messaging, and log revenue per client.
client.payment_failedA client's payment failed and will be retried.
WhenTrigger a dunning sequence from the agency’s billing tool the moment a card fails, before the account lapses and the AI stops answering.
client.subscription_activatedCheckout completed. Not a payment signal: a card-on-file trial activates without a charge.
WhenStart the onboarding checklist once the client has finished checkout. Use client.payment_succeeded, not this, to mark them paying.
client.trial_expiredA client trial expired.
WhenStart a win-back sequence the instant a trial lapses: "your receptionist is paused, reactivate to keep answering calls."
client.cancellation_scheduledA cancellation was requested for the end of the billing period.
WhenStart a save offer while the client is still live, before scheduled_for arrives.
client.cancellation_revertedA scheduled cancellation was undone.
WhenStop the save sequence and put the client back to active in the CRM.
client.canceledService ended (once per cancellation, with reason_code and the client’s stated reason).
WhenUpdate churn tracking, stop the agency’s own billing for that client, and trigger an exit survey to learn why they left.

Envelope

{ "id": "evt_uuid", "type": "call.completed", "created_at": "...", "data": { ... } }

Signature verification

Each delivery is signed. Three headers are sent:

X-VoiceAI-Signature: t=<unix_ts>,v1=<hex>
X-VoiceAI-Event:     call.completed
X-VoiceAI-Delivery:  <delivery_id>

where v1 = HMAC-SHA256(secret, t + "." + rawBody). Verify against the raw body before parsing JSON.

// Node, verify a delivery before trusting it
const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const p = Object.fromEntries(header.split(',').map(x => x.split('=')));
  const expected = crypto.createHmac('sha256', secret)
    .update(p.t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}
// Pass the exact raw request body, not the re-stringified JSON.

Deliveries are logged and retried on failure: up to 6 attempts with a 1 / 5 / 30 / 120 / 360 / 720 minute backoff and a 10-second timeout, so your endpoint should return a 2xx quickly and do slow work after. Stored payloads keep a short-lived recording_url; use recording_api_url or GET /calls/:id/recording for a fresh one.

Payloads

call.completed and call.transferred carry the whole call, so you can ingest from the webhook alone with no follow-up GET.

call.completed
{
  "id": "evt_6b1f9c2a-...",
  "type": "call.completed",
  "created_at": "2026-10-27T15:42:11.904Z",
  "data": {
    "id": "a7c4e0d2-...",
    "client_id": "9f3b1d88-...",
    "caller_phone": "+14045551212",
    "customer_name": "Marcus Hale",
    "customer_phone": "+14045551212",
    "customer_email": "marcus.hale@gmail.com",
    "customer_address": "812 Oak Ridge Dr, Atlanta, GA",
    "service_requested": "Water heater leaking, wants someone out today",
    "urgency_level": "high",
    "sentiment": "anxious",
    "appointment_booked": true,
    "appointment_time": "2026-10-27T21:00:00.000Z",
    "ai_summary": "Caller has an active leak. Booked an emergency visit for 5pm.",
    "summary": "Emergency water heater call, same-day visit booked.",
    "transcript": "AI: Thanks for calling Bright Plumbing...",
    "recording_url": "https://storage.vapi.ai/....mp3?token=... (short-lived)",
    "recording_api_url": "https://urchin-app-bqb4i.ondigitalocean.app/api/v1/calls/a7c4e0d2-.../recording",
    "ended_reason": "customer-ended-call",
    "transfer_status": null,
    "is_spam": false,
    "spam_reason": null,
    "call_language": "en",
    "started_at": "2026-10-27T15:40:02.000Z",
    "ended_at": "2026-10-27T15:42:10.000Z"
  }
}

appointment.booked

Fires on any create, whether from the AI on a call or POST /appointments. appointment.rescheduled and appointment.canceled carry the same shape (plus previous_appointment_time / cancel_reason). google_event_id and calendar_link are null for a record-only appointment.

appointment.booked
{
  "id": "evt_2d8a7f10-...",
  "type": "appointment.booked",
  "created_at": "2026-10-27T15:42:09.511Z",
  "data": {
    "id": "appt_5e9b...",
    "client_id": "9f3b1d88-...",
    "business_name": "Bright Plumbing",
    "customer_name": "Marcus Hale",
    "customer_phone": "+14045551212",
    "appointment_time": "2026-10-27T21:00:00.000Z",
    "end_time": "2026-10-27T22:00:00.000Z",
    "duration_minutes": 60,
    "timezone": "America/New_York",
    "service_type": "Emergency water heater",
    "staff_name": null,
    "notes": "Active leak, caller will be home.",
    "status": "confirmed",
    "booking_source": "ai_receptionist",
    "google_event_id": "abc123def456",
    "google_calendar_id": "primary",
    "calendar_link": "https://calendar.google.com/event?eid=...",
    "created_at": "2026-10-27T15:42:09.000Z"
  }
}

Integration recipes

Schedule appointments via the API

availability, create, reschedule, cancel

1. Create a read_write key.
2. GET /clients/:id/availability?date=YYYY-MM-DD to show open slots.
3. POST /appointments with client_id, customer_name, date, time (+ service_type,
   duration_minutes). Google-connected client -> real calendar event; no calendar
   connected -> record-only appointment. Fires appointment.booked.
4. PATCH /appointments/:id to move it (appointment.rescheduled);
   POST /appointments/:id/cancel to cancel (appointment.canceled).

Sync calls to your CRM

GET /calls, scheduled pull

1. Create a read key.
2. On a schedule (hourly/nightly), GET /calls?since=<last run>&limit=100
3. Walk pages with next_cursor until has_more is false.
4. Upsert each call by its id into your CRM; store ended_at as the sync mark.
   Tip: the row already carries customer_name/phone/email, service_requested,
   summary and transcript, so no per-call follow-up is needed.

Real-time alerts via call.completed

Webhook, for Zapier / Make / your own endpoint

1. Create a read_write key.
2. POST /webhooks with events ["call.completed"] and your endpoint URL.
   Store the whsec_ secret from the response.
3. On each delivery: verify the signature against the raw body, then act, e.g.
   - create a CRM activity from data.summary + data.service_requested
   - text the owner if data.urgency_level is "high" or "emergency"
   - if data.appointment_booked is true, push it to your calendar
4. Return 2xx fast. Non-2xx is retried with backoff (up to 6 attempts).

Book into a not-yet-native calendar

interim: call.completed, capture then create

Interim, until native writes for your calendar land via the aggregator:
1. Subscribe a webhook to call.completed.
2. On each event where data.appointment_booked is true, read:
   data.appointment_time, customer_name, customer_phone, service_requested.
3. Create the event in your calendar (Microsoft Graph, Acuity, Calendly,
   Cal.com, iCloud) from those fields.
Caveat: the AI did not check that calendar's live availability, so treat it
as a requested slot, not a double-book-safe confirmed one.

Onboard clients via the API

POST /clients, then poll provisioning

1. Create a read_write key.
2. On your own signup form submit: POST /clients with the business details.
3. Take the job_id from the 202 response and poll
   GET /clients/:id/provisioning-status/:jobId every few seconds.
4. When status is "done", show the provisioned number from result and tell
   the client to forward their existing line to it.

Calendar booking

An appointment write (create, reschedule, cancel) reaches a real calendar based on what the client has connected. The behavior is the same whether the booking comes from the AI on a call or from the API.

Google Calendar (live)

Full calendar write. Create checks live availability, reschedule patches the event, cancel removes it. The matching event fires with the event link.

Outlook · M365 · Acuity · Calendly · other (coming)

Native writes to these are being added through a calendar aggregator. The API is already provider-agnostic, so these plug in with no change to your integration.

No calendar connected

A write creates a record-only appointment (a real row, no calendar event), so the API still succeeds. Availability returns 409 calendar_not_connected.

A record-only appointment was not checked against any live calendar, so treat it as a requested slot, not a double-book-safe confirmed one, until the client connects a calendar.

Quick reference

A compact, copyable map of the whole API, handy to paste into an AI assistant you are building an integration with.

VoiceAI Connect Developer API, v1
base_url: https://urchin-app-bqb4i.ondigitalocean.app/api/v1
auth: Authorization: Bearer <api_key>   (or x-api-key: <api_key>)
plan: Scale only (call-center agencies any plan; trialing = Scale); else 403 upgrade_required
scopes: read (GET), read_write (POST/PATCH/PUT/DELETE)
rate_limit: 120 req/min per key; headers X-RateLimit-Limit/Remaining/Reset; 429 rate_limit_exceeded
pagination: ?limit=1-100 (default 25) & ?cursor=<next_cursor>; list -> { data, has_more, next_cursor }
errors: { error: { type, message } }; types: authentication_error(401), insufficient_scope(403),
        upgrade_required(403), account_suspended(403), not_found(404), invalid_request(400),
        rate_limit_exceeded(429), not_implemented(501), api_error(400/500)

endpoints:
  GET    /account
  GET    /clients            POST /clients(rw)
  GET    /clients/:id        PATCH /clients/:id(rw)    DELETE /clients/:id(rw -> 501 not_implemented)
  GET    /clients/:id/provisioning-status/:jobId
  GET    /clients/:id/receptionist     PATCH /clients/:id/receptionist(rw)
  GET    /clients/:id/knowledge-base   PUT   /clients/:id/knowledge-base(rw)
  GET    /calls   GET /calls/:id   GET /calls/:id/recording
  GET    /clients/:id/availability?date=YYYY-MM-DD
  GET    /appointments   GET /appointments/:id
  POST   /appointments(rw)   PATCH /appointments/:id(rw)   POST /appointments/:id/cancel(rw)
  GET    /numbers   GET /usage   GET /analytics/calls
  GET    /events
  POST   /webhooks(rw)   GET /webhooks   GET /webhooks/:id
  PATCH  /webhooks/:id(rw)   DELETE /webhooks/:id(rw)
  GET    /webhooks/:id/deliveries   POST /webhooks/:id/ping(rw)   POST /webhooks/:id/test(rw)

events: call.started, call.completed, call.transferred, call.escalated,
        appointment.booked, appointment.rescheduled, appointment.canceled,
        client.provisioned, client.provisioning_failed, client.payment_failed,
        client.subscription_activated, client.trial_expired, client.canceled,
        client.trial_started, client.payment_succeeded,
        client.cancellation_scheduled, client.cancellation_reverted
client_events: every client.* event carries data.client_id and data.client (owner, plan, trial, payment, cancellation)
webhook_signature: header X-VoiceAI-Signature: t=<unix>,v1=<hex>; v1 = HMAC-SHA256(secret, t + "." + rawBody)
webhook_headers_also: X-VoiceAI-Event, X-VoiceAI-Delivery
webhook_retries: up to 6 attempts, backoff 1/5/30/120/360/720 min, 10s timeout, needs 2xx
calendar: appointment writes hit the client's connected calendar (Google live now; Outlook/Acuity/Calendly via aggregator coming). No calendar connected = record-only appointment.
docs: https://www.myvoiceaiconnect.com/developers

Roadmap

Not available yet:

  • Native calendar writes for Microsoft 365 / Outlook, Cal.com, Calendly, Acuity (in progress via a calendar aggregator; the appointment API already supports them without a change on your side).
  • A call.followup_required event for calls that need a human (voicemail, callback requested, unanswered, complaint, abandoned).
  • Client deletion via the API (today DELETE /clients returns 501; offboard from the dashboard).