Integration guide
How the okeep.voice copilot embeds into your CRM: one tenant row on our side, one adapter endpoint on yours, and a stream of live events for your UI.
Architecture
okeep.voice owns telephony (Twilio webhooks, call routing, recording), streaming
transcription and the extraction pipeline. It knows nothing about your product
except a tenant row: productKey, adapterBaseUrl and two secrets.
Everything product-specific happens through your adapter — a single HTTP endpoint
you implement. Your database, your UI, your customer relationships stay yours.
Quickstart
- Get a tenant. In early access we register it for you:
POST /v1/admin/tenantsreturnsadapterSecret(we → you) andserviceToken(you → us). Both are shown exactly once. - Implement the adapter endpoint. One route:
POST {adapterBaseUrl}/<action>, authenticated withAuthorization: Bearer <adapterSecret>. The 11 actions are listed below; unhandled optional actions may return404. - Wire live events to your staff UI. Four of the actions
(
call-ringing,call-started,call-draft,call-ended) are push notifications: publish them to your own realtime channel (WebSocket, Centrifugo, Pusher — your choice) so the badge and live form fill can render. - Read state back over the service API with
Authorization: Bearer <serviceToken>— line config, the calls journal, a call's current draft (for screens opened mid-call or reloaded). - Attach a phone number. A dedicated number from us, or forwarding from the number your customer already prints on their door.
Adapter actions
Requests are JSON; every body carries shopId — your identifier of the
end-business (workshop, clinic, salon) that owns the line.
Lookups — the service asks, you answer
| action | purpose | you return |
|---|---|---|
| shop-snapshot | Configuration snapshot: working hours, operators (id, phone, on-duty flags), answering mode. Cached on our side; we fall back to the last good snapshot if your API is down. | snapshot object |
| caller-info | Who is calling? Used for greeting and routing before pickup. | {known, name?, vehicle?, activeVisitId?} |
| verify-caller | May this caller hear order details? (phone-number match) | {verified} |
| visit-status | Current status of the caller's active order, for the bot to read out. | status payload |
Writes — the service creates records in your system
| action | purpose | semantics |
|---|---|---|
| create-booking-request | The request extracted from a finished booking call. | Idempotent by callRef — retries never duplicate. Returns {bookingId}; that id travels back to the UI in the final call-draft so the order your operator saves closes the request. |
| take-message | A message taken by the bot for the team. | idempotent by marker |
| notify-missed-call | Nobody answered and the caller hung up — alert the team. | best-effort |
Live events — push these to your staff UI
| action | when | key fields |
|---|---|---|
| call-ringing | Phones start ringing — before pickup. | callRef, operatorMemberIds[], callerPhone |
| call-started | An operator answered. | callRef, operatorMemberId, callerPhone |
| call-draft | A fresh extraction pass finished (roughly every 5 s of speech). | callRef, draft, final, bookingRequestId? |
| call-ended | The call finished — from any of several delivery paths, at-least-once. | callRef, operatorMemberIds[] |
The draft
Each call-draft carries a full snapshot of what the copilot
currently believes — not a delta. Your UI never has to merge partial updates.
{
"intent": "booking | price_question | status_check | cancellation | none",
"service": "Diagnostyka zawieszenia", // only when intent = booking
"preferredAt": "2026-08-06T14:00:00.000Z", // only when intent = booking
"complaint": "Coś stuka z przodu",
"plate": "KR 98765", // if the caller read it out
"vehicle": "toyota", // the caller's words about their car
"changed": ["service", "preferredAt"], // corrections: “not Tuesday — Thursday”
"notes": ["Prosi o telefon po 16"],
"endOfCall": "ongoing | agreed | declined | undecided"
}
- No fabrication:
service/preferredAtare gated onintent = booking— enforced in the extractor and re-checked at every trust boundary. - Corrections: a change of mind arrives as a new snapshot plus
changed[], so your UI can show “the client changed this” instead of silently overwriting. - Retraction: a call that ends as
cancellationretracts only what the copilot itself wrote — operator input is never touched. - Human approval: the copilot never writes into your order tables. It fills a form; a person saves it.
Service API (you → us)
| route | purpose |
|---|---|
| GET /v1/lines/:shopId/config | line status, number, limits, minutes used |
| PUT /v1/lines/:shopId/config | answering mode, schedule behaviour |
| POST /v1/lines/:shopId/pause | pause the line temporarily |
| GET /v1/lines/:shopId/calls | calls journal with transcripts and summaries |
| GET /v1/lines/:shopId/calls/:id/draft | current draft + final flag + endedAt — seeds screens opened mid-call or reloaded |
| POST /v1/lines/:shopId/calls/:id/handled | mark a call handled |
| POST /v1/lines/:shopId/calls/:id/visit-created | tell us an order was created from this call |
| GET /v1/lines/:shopId/calls/unhandled-count | badge counter |
| GET /v1/lines/:shopId/usage | bot minutes this month |
Delivery semantics you can rely on
- Telephony webhooks are at-least-once; every state-changing path on our side is a conditional update — replays are no-ops.
call-endedis sent from more than one place on purpose (action URL + status callback); your UI should treat it as idempotent.- Copilot pushes are best-effort: a failed UI event never fails the call.
For correctness after reloads, seed from
GET …/calls/:id/draft— it returnsnull-like 404 for unknown calls so your UI can honestly render “unknown” instead of pretending. - Your adapter being down degrades gracefully: cached snapshot → voicemail, never a dropped call.
What's next on the contract
Three typed packages ship today from a private registry (access comes with your
tenant): @okeep/voice-contract — the types in this guide;
@okeep/voice-node — an adapter kit that turns “implement 11 endpoints”
into “fill in a handlers object”, plus a service-API client;
@okeep/voice-react — useCallCopilot (the tri-state contract,
seed/live merging and every badge-lifetime safety timer from the production
implementation) and a themeable CallBadge, both transport-agnostic.
Per-field confidence/evidence and schema-based extraction for
non-automotive domains are in active development.