Skip to main content

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.

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. 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:
  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 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)

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 }Throws delete_unsupported for Mindbody / Tekmetric. 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 per row.
  • Master key from CRM_CREDENTIAL_ENCRYPTION_KEY env var (Railway secret store).
  • The KMS-backed envelope-encryption path (per-row data-encryption-key + KMS-wrapped KEK) is scaffolded in lib/crm/credentials.ts but dormant — re-activates if/when we move back to AWS.
Every successful credential decrypt is audit-logged into 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 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 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.
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.

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_webhook driver 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.