Vorel’s CRM layer is a pluggable driver registry. Each tenant picks one provider; the agent calls a uniform interface (Documentation Index
Fetch the complete documentation index at: https://docs.vorel.ai/llms.txt
Use this file to discover all available pages before exploring further.
lookupCustomer / searchRecords / createRecord / updateRecord / listRecent / deleteRecord); each driver translates those verbs to the provider’s native API.
8 providers ship today. New providers are an additive driver file + a registry entry.
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 + API key). XML-RPC to Odoo’s
res.partner + crm.lead models. unlink hard-deletes (vs. HubSpot / Salesforce which
soft-archive).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 tenant_crm_field_mapping: each row maps a Vorel slot (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.
The mapping is one-way today — Vorel writes into your CRM with your field names. Reading
arbitrary CRM custom fields back into Vorel slots is a Phase A2 follow-up.
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 } | Throws delete_unsupported for Mindbody / Tekmetric. Used by right-to-erasure |
Credential encryption
CRM credentials are encrypted at rest with AES-256-GCM:- 12-byte random IV per row.
- Master key from
CRM_CREDENTIAL_ENCRYPTION_KEYenv var (Railway secret store). - The KMS-backed envelope-encryption path (per-row data-encryption-key + KMS-wrapped KEK) is
scaffolded in
lib/crm/credentials.tsbut dormant — re-activates if/when we move back to AWS.
audit_log with the actor type
('tool' for agent calls, 'platform_operator' for operator-console actions, 'system' for
background jobs) and the originating request id + conversation id. This closes the forensic gap
surfaced in the 2026-05-01 drill 1 walkthrough.
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
without a delete API (Mindbody, Tekmetric) throw delete_unsupported — the operator gets a
flag that they need to rotate 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. - Form-based providers (Mindbody / Tekmetric / Odoo / 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.
What’s NOT supported
- Reading arbitrary CRM custom fields back into Vorel slots. Field mapping is write-direction today (Vorel writes named fields into your CRM). The reverse direction is a Phase A2 follow-up.
- Multiple CRM providers per tenant. One provider per tenant. If your business needs to
write into both Salesforce + a custom warehouse, use the
custom_webhookdriver against an internal fan-out service. - CRM-side webhooks back into Vorel. Today the data flow is one-way (Vorel → CRM). Listening for “CRM record updated” events to refresh Vorel-side caches is on the roadmap.
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