> ## 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.

# CRM integration

> Driver implementations covering the major CRMs in the MENA SMB market. Real reads + writes via a uniform driver interface; per-tenant credentials encrypted at rest.

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

<Note>
  **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](/security/data-retention).
</Note>

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](/security/data-retention) for the four-class taxonomy, the per-class windows, and the four architectural invariants.

## Supported providers

<CardGroup cols={2}>
  <Card title="Custom webhook" icon="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.
  </Card>

  <Card title="HubSpot" icon="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.
  </Card>

  <Card title="Salesforce" icon="salesforce">
    OAuth 2.0; access tokens auto-refresh. Reads + writes leads + contacts + opportunities. Sandbox
    and production endpoints both supported.
  </Card>

  <Card title="Zoho" icon="briefcase">
    OAuth 2.0 (Zoho's per-DC token model); access tokens auto-refresh. Reads + writes leads +
    contacts + deals.
  </Card>

  <Card title="Mindbody" icon="scissors">
    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.
  </Card>

  <Card title="Athenahealth" icon="stethoscope">
    OAuth 2.0 (`client_credentials` flow); access tokens cached lazily. Reads + writes patient +
    appointment records. Used by the clinic vertical pack.
  </Card>

  <Card title="Tekmetric" icon="car">
    Form-based credentials (shop id + API key). Read + write customer + repair-order records. The
    standard auto-service shop CRM. No deleteRecord; operators rotate manually.
  </Card>

  <Card title="Odoo" icon="briefcase">
    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).
  </Card>

  <Card title="Toast" icon="utensils">
    Partner client-credentials grant + per-tenant restaurant GUID. Reads + writes the loyalty
    `guest` object. The standard restaurant-pack CRM.
  </Card>

  <Card title="Generic REST API" icon="plug">
    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.
  </Card>
</CardGroup>

<Note>
  **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.
</Note>

## 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)

| 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 |

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](/getting-started/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.

## Related docs

* [Quickstart](/getting-started/quickstart): Step 5 covers the kickoff connect flow
* [Security overview](/security/overview): encryption posture, audit-log policy
* [API Reference](/api-reference/introduction): `/v1/crm/*` endpoints
* [How it works](/getting-started/how-it-works): where CRM calls fit in the dispatch pipeline

{/* verified-against: apps/web/src/lib/crm/index.ts instantiateDriver switch (10 driver classes) */}

{/* verified-against: apps/web/src/lib/crm/index.ts audit of every decrypt (non-fatal) */}

{/* verified-against: apps/web/src/lib/crm/mapping/* — field-mapping studio: read/write/object/trigger kinds, per-provider default catalogs, live introspection */}

{/* verified-against: apps/web/src/app/api/v1/crm/create-record/route.ts (idempotency_key body field) */}
