#Integration guide for AI coding agents
You are integrating calling into a CRM using the Voice Infrastructure Platform. Work through this checklist in order. Each step names the endpoint(s), the recipe with tested code, and how to verify the step before moving on. Everything here also works in the sandbox (mock carrier + simulated phone network), so build and test there first.
Machine-readable references:
- OpenAPI 3.1:
https://tvh-voice-api.gigaleads.nl/openapi.yaml(every endpoint with examples;webhookssection = all events) - Docs index for LLMs:
https://tvh-voice-api.gigaleads.nl/llms.txt, everything in one file:https://tvh-voice-api.gigaleads.nl/llms-full.txt - TypeScript SDK:
@voice/sdk(typed client generated from the OpenAPI document +verifyWebhook) - Browser softphone:
@voice/softphone(SIP.js wrapper)
#0. Decisions to ask the human about first
Do not guess these; ask, then record the answers in your project notes.
- Base URL and API key. Production and sandbox base URLs. Which scopes the key needs
(default:
calls:*,agents:*,numbers:read,routing:*,webhooks:*,events:read,recordings:read). The key lives in server-side config (env var), never in browser code. - Agent mapping. Which CRM entity is an agent (user id?) → used as
agent_ref. - Who answers inbound calls. One agent, a team (ring group), voicemail, or "the CRM decides per call" (webhook-decides, 2-second answer window with a fallback rule).
- Caller ID. Which number each user calls from (per user, per team, or one company number).
- Recording. Off / all / inbound / outbound; consent announcement on or off; retention days. This is a legal decision (GDPR, local consent law) — the human must decide.
- Softphone location. Embedded browser softphone (recommended), desk phones/apps (long-lived SIP password), or both.
- Which carrier (production only) and who creates its account and API key. The platform never needs the human's carrier password, only an API key/credentials for a dedicated key.
- Destinations and limits. Allowed destination countries, max concurrent calls, calls per hour/day, spend-guard minutes (toll-fraud protection — the customer pays the carrier).
#1. Authenticate
- Header on every request:
Authorization: Bearer vk_.... - Errors always look like
{ "error": { "code": "...", "message": "...", "request_id": "..." } }; branch oncode(stable; see/docs/errors.md), not on the message. - Retry
POSTrequests safely with anIdempotency-Keyheader. - Recipe:
/docs/recipes/01-authenticate.md. Verify:GET /v1/workspacereturns 200.
#2. Connect a carrier (usually done once by the human in the dashboard)
GET /v1/carrier-types→ types, settings schema, credential fields, default capabilities.POST /v1/carrierswithtype,settings,credentials. The platform validates the credentials at the carrier and provisions everything (SIP connection/trunk, origination, outbound profile).- Wait for
health.state == "healthy"(or thecarrier.health.changedevent). - Read
capabilitiesand hide CRM features the carrier does not support (e.g. number search for generic SIP trunks). Unsupported calls returnCAPABILITY_NOT_SUPPORTED. - Recipe:
/docs/recipes/02-connect-carrier.md. Sandbox: usetype: "mock".
#3. Create agents and embed the softphone
- For each CRM user:
POST /v1/agents { agent_ref }(treat409 CONFLICTas "already exists"). - Your backend exposes an endpoint (e.g.
GET /crm/softphone-config) that callsPOST /v1/agents/{agent_ref}/softphone-configand returns the JSON to the logged-in user's browser. - In the browser:
new Softphone({ getConfig: () => fetch('/crm/softphone-config').then(r => r.json()) }), thenstart(). The widget refreshes the short-lived password itself viagetConfig. - Verify: the agent shows
registration.state == "registered"(andagent.registeredarrives). - Recipe:
/docs/recipes/03-agents-and-softphone.md, widget reference:/docs/softphone.md.
#4. Numbers and regulatory documents
- If
capabilities.supports_ordering: search (GET /v1/available-numbers), checkrequires_requirement_group, collect requirements (GET /v1/regulatory-requirements), upload documents (POST /v1/documents, base64), create + submit a requirement group, order (POST /v1/number-orders). - Status follows the carrier:
number.order.updated/number.requirements.updatedevents. A rejected group can be corrected (PATCH) and resubmitted. - If the carrier has no number API: add existing numbers with
POST /v1/numbers. - Recipe:
/docs/recipes/04-numbers.md.
#5. Click-to-call
POST /v1/calls { agent, to, caller_id, external_ref }→ the agent's phone rings first, thentois dialed via the carrier ofcaller_id, then both are connected.- Show call state from webhooks/stream:
call.initiated → call.ringing → call.answered → call.ended. - Controls:
/hangup,/hold,/unhold,/dtmf,/transfer(blind or attended). - The browser softphone receives the agent leg as an incoming call with header
X-Call-Id(=call_id): auto-answer it in the UI when it matches the call the user just started. - Recipe:
/docs/recipes/05-click-to-call.md.
#6. Webhooks (store call activity in the CRM)
POST /v1/webhook-endpoints { url, event_types }→ keep the returnedsecret.- Verify every request with the raw body (
verifyWebhook(secret, rawBody, headers)from the SDK, or any Standard Webhooks library). Respond 2xx within 10 s; process asynchronously. - Deduplicate on the event
id(=webhook-idheader): deliveries are at-least-once and can be replayed. call.endedcarries the full CDR (status, durations, hangup cause);recording.ready→ fetch a signed URL withGET /v1/calls/{id}/recording(valid 15 min; do not store the URL, store the call id).- Recipe:
/docs/recipes/06-webhooks.md, catalog with JSON Schemas:/docs/events.md.
#7. Inbound routing
- Create ring groups / inbound rules and attach a rule to each number (
PATCH /v1/numbers/{id}). - Webhook-decides: answer the synchronous
call.incomingrequest within 2 s with{ "route": { "type": "agent", "agent_ref": "user-123" } }(orring_group,voicemail,reject); otherwise the fallback rule is used. - Recipe:
/docs/recipes/07-inbound-routing.md.
#8. Call logs and recordings
GET /v1/calls(filters, cursor pagination),GET /v1/calls/{id}(with event timeline),GET /v1/calls/export.csv, recordings via signed URLs,DELETE /v1/calls/{id}/recordingfor erasure.- Recipe:
/docs/recipes/08-call-logs.md.
#9. Go-live test (run it, report the results to the human)
- Sandbox: steps 1–8 pass against the sandbox (
docker compose upin the platform repo, see/docs/sandbox.md). - Production carrier connected and healthy; at least one active number.
- Agent registers from the CRM in a real browser (microphone allowed).
- Click-to-call to a real mobile phone: both hear each other;
call.endedstored in the CRM. - Call the number from a mobile phone: the configured agent/group rings; answer; hang up.
- Webhook signature verification rejects a modified body (send one manually).
- If recording is on:
recording.readyreceived, playback works, consent message heard. - Limits: a call to a non-allowed country is refused with
DESTINATION_NOT_ALLOWED.
#10. Switching carriers later
Configuration only — no CRM change:
- Connect the new carrier (
POST /v1/carriers); both can be active at the same time. - Move numbers (port them outside the platform, or order new numbers at the new carrier). Outbound calls always use the carrier of the chosen caller ID, so you migrate per number.
- Optionally set
failover_carrier_idon the old carrier during the migration. - Revoke the old carrier (
POST /v1/carriers/{id}/revoke). Never reference carrier/provider names in CRM code: usecarrier_idandcapabilitiesonly. Recipe:/docs/recipes/09-switch-carriers.md.