lookupCustomer / searchRecords / createRecord / updateRecord / listRecent / deleteRecord); each driver translates those verbs to the provider’s native API.
Ten provider drivers ship today, all implementing the full interface. HubSpot is the production-proven driver (live on the Skyline tenant); the others are fully built and tested against the same contract. New providers are an additive driver file + a registry entry.
CRM is the system of record
Your conversation transcripts live in your CRM, not in Vorel. This is an architectural
commitment, not a marketing claim, and it is enforced in code via the CRM-as-SoR mechanism. For
the full per-class retention windows and the four architectural invariants, see Security → Data
retention.
- Mirrored to your CRM on insert. Every class-(c) write attempts a CRM-write via the configured driver (fire-and-forget on the dispatch hot path, so customer-facing latency stays voice-grade).
- Retained Vorel-side only until CRM-write success + an ADR-locked post-write window. For
messages(the transcript table itself), the window is 7 days post-insert and post-CRM-write success. Forconversations,customers,leadsit is 30 days. Forcasesit is 90 days. - Purged by a scheduled worker on the per-table window. The worker refuses to purge a class-(c) row whose CRM-write has not succeeded; the row stays until either a redrive resolves it or a 24-hour escalation window opens a
crm_sync_failureincident.
Supported providers
Custom webhook
Generic POST / GET adapter to any HTTP endpoint your team maintains. The fallback for any CRM we
don’t ship a native driver for. Vorel signs each request with a tenant-scoped HMAC.
HubSpot
OAuth 2.0; access tokens auto-refresh; refresh writes the new token back into
tenant_credentials so process restarts keep the cached token. Reads + writes contacts + deals- tickets.
Salesforce
OAuth 2.0; access tokens auto-refresh. Reads + writes leads + contacts + opportunities. Sandbox
and production endpoints both supported.
Zoho
OAuth 2.0 (Zoho’s per-DC token model); access tokens auto-refresh. Reads + writes leads +
contacts + deals.
Mindbody
Form-based credentials (site id + API key). Read + write client + booking records. The standard
salon / spa industry CRM. No deleteRecord; operators rotate manually at the Mindbody dashboard.
Athenahealth
OAuth 2.0 (
client_credentials flow); access tokens cached lazily. Reads + writes patient +
appointment records. Used by the clinic vertical pack.Tekmetric
Form-based credentials (shop id + API key). Read + write customer + repair-order records. The
standard auto-service shop CRM. No deleteRecord; operators rotate manually.
Odoo
Form-based credentials (instance URL + database + username + secret). JSON-RPC to Odoo’s
res.partner + crm.lead models. unlink hard-deletes (vs. HubSpot / Salesforce which
soft-archive).Toast
Partner client-credentials grant + per-tenant restaurant GUID. Reads + writes the loyalty
guest object. The standard restaurant-pack CRM.Generic REST API
A first-class configurable HTTP connector: base URL, auth type, and per-verb request templates.
For any CRM with a REST API we don’t ship a dedicated driver for. More structured than the custom
webhook bridge, with live schema introspection when the provider exposes it.
OpenTable is reserved as a registered provider slug but is partnership-only: no public
API. Tenants requesting OpenTable integration use the
custom_webhook driver against a
partner-side adapter.What the agent does with your CRM
Three things happen automatically, on every conversation:lookupCustomeron inbound. Before the agent engages, it queries your CRM by phone / email / external id. If your customer is already in your system asJohn Smith, VIP, last visited 2026-04-12, the agent uses that context (no re-asking name; greets appropriately).createRecordwhen a new lead qualifies. Tools likecrm_create_recordwrite the lead into your CRM with the per-tenant field mapping (your CRM’s “First Name” field, your CRM’s “Property Type” custom field), not generic field names.updateRecordas the agent learns more. The agent callscrm_update_recordincrementally, adding budget after the customer reveals it, attaching the booked viewing date, etc.
Per-tenant field mapping
Your CRM’s “Mobile Phone” custom field is not Vorel’scustomer.phone. Per-tenant field mapping
lives in a field-mapping “studio”: each override row maps a canonical Vorel key (e.g.
lead.budget_max) to your CRM’s field path (SF lead.UF_Budget_Max__c or HubSpot deal.budget_aed).
Your operator configures these during the kickoff call, and the studio can introspect your CRM’s
real schema (for providers that expose one) so the operator maps against your actual field names.
Mappings cover read, write, object-slug, and trigger-field kinds, so the translation layer applies
both when Vorel writes into your CRM and when it normalizes records read back out. A tenant with no
overrides resolves to per-provider defaults that reproduce each driver’s built-in field names
exactly.
Driver interface (what every provider implements)
| Verb | Returns | Notes |
|---|---|---|
testConnection() | { ok: true } | { ok: false, reason } | ”Test connection” button in operator console |
lookupCustomer({ phone, email, external_id }) | CustomerRecord | null | Phone is preferred match key |
searchRecords({ object, query, limit }) | { matches: [...] } | Used by FAQ-style “do I have insurance on file?” |
createRecord({ object, fields, idempotency_key }) | { external_id, record_url } | idempotency_key typically <conversation_id>:<turn_id> |
updateRecord({ object, external_id, fields }) | { updated: true, record_url } | Partial update; only changed fields |
listRecent({ object, since, limit }) | { records: [...] } | Used by analytics / digest workflows |
deleteRecord({ object, external_id }) | { deleted: true, hard_deleted: boolean } | Providers with no delete API throw delete_unsupported. Used by right-to-erasure |
Credential encryption
CRM credentials are encrypted at rest with AES-256-GCM (12-byte random IV, 16-byte auth tag per row). Two paths coexist, selected per-row at runtime:- Production (KMS envelope encryption): a KMS key-encryption-key wraps a per-row data-encryption-key; the wrapped key is stored alongside the ciphertext, and the key-management service never sees credential plaintext. Unwrapped keys are cached in-process briefly so webhook decrypts don’t hammer the service.
- Dev/CI: a single key derived from the
CRM_CREDENTIAL_ENCRYPTION_KEYenv var.
audit_log with the actor type
('tool' for agent calls, plus operator and system actors for console actions and background jobs)
and the originating request id + conversation id. The audit write is non-fatal: an audit failure
never blocks a decrypt.
OAuth refresh handling
OAuth drivers (HubSpot, Salesforce, Zoho, Athenahealth) refresh access tokens automatically when a 401 fires. The refreshed token gets re-encrypted and persisted back intotenant_credentials
(rotated_at updated, audit-log entry) so subsequent process starts pick up the cached token
rather than re-burning a refresh API call. Refresh failures audit-log + return auth_error to
the agent, which falls back gracefully.
Public API surface
Today, one CRM proxy endpoint is exposed on the public API:POST /v1/crm/create-record: same path the agent uses. Accepts{ object, fields, idempotency_key }; returns{ external_id, record_url }. Theidempotency_keyis the only field-level idempotency on the public API (the underlying CRM driver dedupes by it).
crm:write scope). Per-key rate limit (200 req/min) plus the global
per-(tenant, tool) rate limit (50 req/min); both fail-open on Redis blip.
Lookup / update / list-recent verbs are exposed today only through the agent-side tool routes
(/api/tools/crm_lookup_customer, etc.); these are JWT-authed, not API-key-authed. Promoting
them to the public API surface is a roadmap item; until then, integrate via the
custom_webhook driver going the other direction (your system pushes to Vorel via
POST /v1/leads) for read-style flows.
Right-to-erasure
When a customer requests deletion under PDPL Art. 17 / GDPR Art. 17, the operator-sidePOST /api/tenant/forget flow scrubs the customer’s data on Vorel and calls
deleteRecord against the configured CRM driver to scrub the provider-side record. Drivers
whose provider exposes no delete API throw delete_unsupported; the operator gets a flag that
they need to delete manually at the provider’s dashboard, and the audit trail records this
divergence.
Connecting your CRM during onboarding
Your operator runs through the connect step on the kickoff call (Step 5 of the Quickstart):- OAuth providers (HubSpot / Salesforce / Zoho / Athenahealth): your operator triggers the
OAuth flow from the admin console; you sign in on the provider’s OAuth page; tokens land
encrypted in
tenant_credentials. - Key / form-based providers (Mindbody / Toast / Tekmetric / Odoo / generic REST / custom webhook): your operator enters your API credentials directly into a form; encrypted at rest the same way.
testConnection() button) before stamping status='active'. If
the test fails, we troubleshoot together on the call rather than letting your tenant ship with a
broken CRM connection.
CRM-triggered outbound
The data flow runs both directions. Beyond Vorel writing into your CRM, a CRM-side event can trigger a Vorel outbound action. Inbound CRM webhooks (HubSpot / Salesforce / Mindbody / Toast ship signature verifiers) are normalized into events that can fire an operator-initiated outbound call, gated by the tenant’s trigger settings and TCPA consent gates. This closes the loop: a new lead in your CRM can prompt Vorel to reach out, not just the other way around.What’s NOT supported
- Multiple CRM providers per tenant. One provider per tenant. If your business needs to
write into both Salesforce + a custom warehouse, use the
custom_webhookor generic-REST driver against an internal fan-out service. - OpenTable as a native driver. Reserved as a registered slug but partnership-only; route it
through the
custom_webhookdriver against a partner-side adapter.
Related docs
- Quickstart: Step 5 covers the kickoff connect flow
- Security overview: encryption posture, audit-log policy
- API Reference:
/v1/crm/*endpoints - How it works: where CRM calls fit in the dispatch pipeline