Appearance
CRM Credentials
How Avoca wires per-team CRM credentials and how runtime call code resolves them. Same conceptual pattern across every CRM vertical; one notable inconsistency (AutoOps has no admin UI).
The pattern
Each CRM has its own credentials table (or pair of tables), all keyed on team_id. When a call comes in, the team_id flows from the phone number through the voice assistant to the runtime code; the runtime code reads from that CRM's credentials table to make API calls.
Resolution chain at call time:
Twilio dials phone number
↓ phone_numbers.team_id
team
↓ <crm>_credentials lookup (or <crm>_config / <crm>_api_keys, varies per CRM)
credential row
↓ api_key / tenant_id / client_id / client_secret (per-CRM shape)
CRM API callThe voice_assistant sits in the chain (phone → voice_assistant → team) but is NOT itself referenced in any credentials table. Multiple voice_assistants on the same team share the team's CRM credentials. One team cannot have multiple CRM client mappings on the same vertical.
Per-CRM table reference
| CRM | Table(s) | Per-team or per-enterprise? | Admin UI route |
|---|---|---|---|
| ServiceTitan | service_titan (per-team: tenant_id, client_id, client_secret, app_key) | per-team | /team/[teamId]/integrations/crm/service-titan/credentials |
| HouseCall Pro | house_call_pro_api_keys | per-team | /team/[teamId]/integrations/crm/hcp |
| Jobber | jobber_api_keys | per-team | /team/[teamId]/integrations/crm/jobber/credentials |
| Acculynx | acculynx_config | per-team | /team/[teamId]/integrations/crm/acculynx/credentials |
| AutoOps | autoops_credentials (per-enterprise: api_key) + autoops_team_configs (per-team: client_id, client_name, client_key, time_zone) | per-enterprise for the credential, per-team for the mapping | ❌ none (see Tech Debt below) |
AutoOps is structurally different in one way
AutoOps splits credentials and per-team mapping into TWO tables. Other CRMs collapse this into one table per team. The reason: AutoOps's API key is enterprise-scoped (one key serves all of EAS's 5+ shops), while the client_id differs per shop. Other CRMs typically issue one credential per shop.
Schema diff at a glance:
ServiceTitan / HCP / Jobber / Acculynx pattern (one table per team):
<crm>_credentials (team_id UNIQUE, api_key + per-CRM fields)
AutoOps pattern (two tables, enterprise + team):
autoops_credentials (enterprise_id UNIQUE, api_key)
autoops_team_configs (team_id UNIQUE, credential_id, client_id, ...)This shape is intentional. EAS's 5 shops all run under one cl_<...> enterprise in AutoOps's data model; the AutoOps API key authenticates calls to the enterprise tenant; the client_id parameter on each API call scopes the operation to a specific shop. Mirroring that structure means the Avoca-side data model has the same shape.
Encryption
All API keys are stored encrypted in the DB. The runtime helpers encryptApiKey() / decryptApiKey() from lib/api-keys/encryption.ts handle the round-trip via the WEBHOOK_API_KEY_ENCRYPTION_KEY env var. The admin UI for each CRM (where one exists) does the encryption transparently on save and decryption on display.
Tech debt: no admin UI for AutoOps
Every other CRM has a /team/[teamId]/integrations/crm/<vertical>/ settings page. AutoOps doesn't. Configuring AutoOps for a new team currently requires direct DB inserts (Kareem-driven for real EAS shops).
Filling this gap would be a /team/[teamId]/integrations/crm/autoops/ page mirroring the other CRM pages: a credentials page at the enterprise level for the api_key, plus a per-team page for the client_id / client_name / client_key / time_zone mapping.
Tracked in unknowns.md.
At call time
In-call tool handlers (e.g., autoOpsLookupCustomer, stCheckCustomer, etc.) receive the team_id via the Vapi tool-dispatch context. They call the CRM-specific lookup function (e.g., getAutoOpsConfig(teamId)) to get the credentials, then make the API call.
Post-call workflow stages (booking-autoops, cancellations-autoops, rescheduling-autoops, and the equivalent for other CRMs) receive the team_id from the end-of-call-report payload. Same lookup, same API call path.
Related
- Post-call Dispatch Chain, where CRM credentials get used at the post-call stage.
- Tool Calls, where CRM credentials get used in-call.
- AutoOps Integration, AutoOps-specific in-call + post-call patterns.
- HouseCall Pro, HCP-specific notes.
- Unknowns: CRM / AutoOps integration, open questions + tech debt.