Skip to main content
Vorel’s CRM layer is a pluggable driver registry. Each tenant picks one provider; the agent calls a uniform interface (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.
The CRM-as-SoR architectural commitment classifies every Vorel-side row into a four-class taxonomy. Conversation transcripts, customer profiles, qualification state, appointments, and cases (the rows that hold actual customer content) are class (c). Class-(c) rows are:
  1. 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).
  2. 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. For conversations, customers, leads it is 30 days. For cases it is 90 days.
  3. 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_failure incident.
This means: at any point in time, the authoritative copy of a customer’s conversation transcript lives in your CRM, not in Vorel. Vorel-side state is a short-lived cache; the long-lived record lives where you read it from anyway. No competitor in the AI agent space ships this architecturally. A contract addendum from a competitor saying “we’ll keep your data per your retention policy” is a policy commitment that requires legal enforcement. Vorel’s commitment is enforced by a CI gate that refuses to merge a schema change that lacks a class declaration. There is no path to drift from the commitment without an ADR amendment visible in git history. Read the full data retention documentation for the four-class taxonomy, the per-class windows, and the four architectural invariants.

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:
  1. lookupCustomer on inbound. Before the agent engages, it queries your CRM by phone / email / external id. If your customer is already in your system as John Smith, VIP, last visited 2026-04-12, the agent uses that context (no re-asking name; greets appropriately).
  2. createRecord when a new lead qualifies. Tools like crm_create_record write 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.
  3. updateRecord as the agent learns more. The agent calls crm_update_record incrementally, adding budget after the customer reveals it, attaching the booked viewing date, etc.
The agent doesn’t speculate. If the CRM lookup returns nothing, the agent qualifies from scratch. If a CRM call fails (auth, rate limit, vendor outage), the agent falls back to “I’ll have someone follow up” rather than fabricating CRM data.

Per-tenant field mapping

Your CRM’s “Mobile Phone” custom field is not Vorel’s customer.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)

VerbReturnsNotes
testConnection(){ ok: true } | { ok: false, reason }”Test connection” button in operator console
lookupCustomer({ phone, email, external_id })CustomerRecord | nullPhone 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
All verbs are tenant-scoped via the driver’s bound credentials; the registry resolves and decrypts those credentials per call.

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_KEY env var.
The two paths coexist row-by-row, so a backfill can re-wrap dev-local rows under KMS with zero downtime. Every successful credential decrypt is audit-logged into 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 into tenant_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 }. The idempotency_key is the only field-level idempotency on the public API (the underlying CRM driver dedupes by it).
Authed via API key (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-side POST /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.
We test the connection live (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_webhook or 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_webhook driver against a partner-side adapter.