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.

Early access. The API below is what runs in production behind okeep.auto today. It is v1 and may still change in breaking ways — write to hello@okeep.io for a sandbox tenant and we will version the contract with you.

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

  1. Get a tenant. In early access we register it for you: POST /v1/admin/tenants returns adapterSecret (we → you) and serviceToken (you → us). Both are shown exactly once.
  2. Implement the adapter endpoint. One route: POST {adapterBaseUrl}/<action>, authenticated with Authorization: Bearer <adapterSecret>. The 11 actions are listed below; unhandled optional actions may return 404.
  3. 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.
  4. 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).
  5. 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

actionpurposeyou return
shop-snapshotConfiguration 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-infoWho is calling? Used for greeting and routing before pickup.{known, name?, vehicle?, activeVisitId?}
verify-callerMay this caller hear order details? (phone-number match){verified}
visit-statusCurrent status of the caller's active order, for the bot to read out.status payload

Writes — the service creates records in your system

actionpurposesemantics
create-booking-requestThe 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-messageA message taken by the bot for the team.idempotent by marker
notify-missed-callNobody answered and the caller hung up — alert the team.best-effort

Live events — push these to your staff UI

actionwhenkey fields
call-ringingPhones start ringing — before pickup.callRef, operatorMemberIds[], callerPhone
call-startedAn operator answered.callRef, operatorMemberId, callerPhone
call-draftA fresh extraction pass finished (roughly every 5 s of speech).callRef, draft, final, bookingRequestId?
call-endedThe 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"
}

Service API (you → us)

routepurpose
GET /v1/lines/:shopId/configline status, number, limits, minutes used
PUT /v1/lines/:shopId/configanswering mode, schedule behaviour
POST /v1/lines/:shopId/pausepause the line temporarily
GET /v1/lines/:shopId/callscalls journal with transcripts and summaries
GET /v1/lines/:shopId/calls/:id/draftcurrent draft + final flag + endedAt — seeds screens opened mid-call or reloaded
POST /v1/lines/:shopId/calls/:id/handledmark a call handled
POST /v1/lines/:shopId/calls/:id/visit-createdtell us an order was created from this call
GET /v1/lines/:shopId/calls/unhandled-countbadge counter
GET /v1/lines/:shopId/usagebot minutes this month

Delivery semantics you can rely on

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-reactuseCallCopilot (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.

Want a sandbox? Write to hello@okeep.io — we'll set up a tenant, a test number and walk your team through the first integration.