Aventora CRM — complete guide
Single reference for aventora-crm (Aventora CRM): deployment, upgrade, provisioning/SSO, industry presets, workspace color themes, Sales Cockpit, TIPS sync, and troubleshooting.
Audience: operators, integrators, client backend developers, sales ops.
Laravel custom integration (no plugin): Laravel Integration — provisioning, SSO, embedded navigation, avatar.
Related platform docs: API Security Model, Platform API Keys, CRM MCP, Engagement Hub MCP, Outbound Email Templates, Gmail IMAP Setup
Table of contents
- Concepts
- Environment variables
- Deployment and upgrade
- Admin CLI commands
- Provisioning and SSO
- Embedded page mode (host-driven navigation)
- Laravel (custom integration): Laravel Integration
- Workspaces, users, and industry presets
- Sales Cockpit
- Campaign landing pages
- Demo workspace provisioning
- TIPS Services sync
- Laravel plugin
- Contacts and workspace API keys
- MCP (Cursor / Claude)
- Webhooks
- Troubleshooting
Concepts
| Concept | CRM meaning | API surface |
|---|---|---|
| User | Person who can log in to a workspace | POST /auth/provision/* on apex CRM_API_URL |
| Contact | person record in the People module | POST /rest/people on workspace host with workspace API key |
Provisioning and SSO use the apex server. Contact writes use https://{subdomain}.{crm-host}. Do not send /rest/people with the provisioning secret.
Environment variables
aventora-crm (server)
| Variable | Required | Description |
|---|---|---|
PROVISIONING_SECRET | Yes | Bearer token for /auth/provision/* (server-side only) |
APP_VERSION | Yes | Valid semver (e.g. 1.20.0); drives workspace migrations. Never leave empty. |
FRONTEND_URL | Multi-workspace | Apex URL, e.g. https://crm.aventora.ai |
SERVER_URL | Yes | API base; usually same as FRONTEND_URL |
IS_MULTIWORKSPACE_ENABLED | Production | true for {subdomain}.crm.aventora.ai |
AVENTORA_BASE_URL | Hub integration | Engagement Hub API base URL |
AVENTORA_WEBHOOK_SECRET | Recommended | Validates X-Aventora-Webhook-Secret on inbound webhooks |
AVENTORA_HUB_AUTO_PROVISION_ENABLED | Optional | When true, enqueue Hub/domain provisioning after CRM workspace or member provisioning |
AVENTORA_HUB_PROVISION_CALLBACK_URL | With auto-provision | Assistant URL, e.g. http://hub:8010/internal/crm-hub-provision |
AVENTORA_HUB_PROVISION_CALLBACK_SECRET | With auto-provision | Shared Bearer secret with Assistant (CRM_HUB_PROVISION_CALLBACK_SECRET) |
DEFAULT_PHONE_COUNTRY_CODE | Optional | ISO 3166-1 alpha-2 (e.g. US) used for new/empty phone inputs when workspace Settings → Data Model → Phones → Default Country Code is unset. Calling code is derived via libphonenumber. Unset keeps prior behavior (no default). |
SHOULD_PREFILL_STANDARD_OBJECTS | Optional | Default false. When true, workspace activation seeds Twenty demo CRM records (sample companies, people, opportunities, Quick Lead workflow, My First Dashboard). Leave unset/false for empty provisioned workspaces. When unset/false, Docker startup also runs workspace:remove-standard-objects-prefill to soft-delete those fixed seed UUIDs from existing workspaces. |
WHITE_LABEL | Optional | Partner display name for UI labels, API toasts/errors, and transactional emails. Unset or blank → Aventora (product name {name} CRM). Example: WHITE_LABEL=Acme → “Acme” / “Acme CRM”. Logos and website URLs are unchanged. Pass through Docker Compose from .env. Existing workspaces keep previously seeded role/workflow/tab names in the DB until recreated or manually renamed. |
USE_2FA | Optional | Default true. When true, workspace admins (and platform admins) must enable and enter 2FA at login. When false, CRM does not enforce admin 2FA enrollment or verification (workspace Settings → Security 2FA toggle is also ignored at login). Compose passes this into server/worker (${USE_2FA:-true}). |
Per workspace (not env): AVENTORA_API_KEY in Settings → Applications → Aventora Phone. AVENTORA_PERSON_WALL_ENABLED in Settings → Applications → Aventora (empty = inherit; false = opt out when PERSON_WALL=true).
Person wall (optional, server)
| Variable | Default | Description |
|---|---|---|
PERSON_WALL | false | Kill switch; when true, members see only their own Person records unless workspace opts out |
PERSON_WALL_WORKSPACE_IDS | empty | Comma-separated workspace UUID allowlist (empty = all workspaces) |
PERSON_WALL_WORKSPACE_ID | empty | Deprecated single-UUID allowlist |
PERSON_WALL_EXCLUDE_SYSTEM_RECORDS | false | Hide integration-created persons from scoped members |
PERSON_WALL_SYSTEM_ACTOR_NAME | empty | Display name for system-created person createdBy. When empty, uses WHITE_LABEL (or Aventora). |
PERSON_WALL_DEBUG | false | Log PersonWall scope decisions |
Rollout: workspace:sync-aventora-variables, then workspace:backfill-person-created-by. See Person wall (optional, server).
Calling app (integrator / admin / domain-chatbot)
| Variable | Required | Description |
|---|---|---|
CRM_API_URL | Yes | Apex URL for /auth/provision/* |
CRM_PROVISIONING_SECRET | Yes | Must match CRM PROVISIONING_SECRET |
CRM_PUBLIC_BASE_URL | Contacts / URLs | Defaults to CRM_API_URL |
CRM_WORKSPACE_API_KEY | Contact writes | Workspace-scoped token for /rest/* |
Laravel uses AVENTORA_CRM_URL and AVENTORA_CRM_PROVISIONING_SECRET.
Host-controlled Compose env files (.env next to docker-compose.yml) must stay off Git. On the Linux VPS restrict the file to the deployment user, for example sudo chmod 600 .env. Typical documented path: /opt/aventora/crm/.env.
TIPS (optional, server + worker)
| Variable | Required | Default | Description |
|---|---|---|---|
TIPS_CLIENT_ID | For sync | — | OAuth client id |
TIPS_CLIENT_SECRET | For sync | — | OAuth secret |
TIPS_API_BASE_URL | No | https://tipsadvisors.tipservices.ca/api | Include /api |
TIPS_SYNC_WORKSPACE_ID | For sync | — | Single workspace UUID |
TIPS_SYNC_ENABLED | No | false | Enable scheduled cron |
TIPS_SYNC_CRON_PATTERN | No | 0 * * * * | BullMQ cron (hourly) |
Deployment and upgrade
Production (Docker)
Containers run migrations and upgrade on startup unless DISABLE_DB_MIGRATIONS=true:
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod workspace:sync-aventora-sales-person-fields
yarn command:prod workspace:ensure-aventora-user-role-permissions
yarn command:prod workspace:backfill-provisioned-workspace-admins
yarn command:prod workspace:remove-standard-objects-prefill
yarn command:prod cache:flush
Manual upgrade after pulling a new image:
cd /path/to/compose # directory with docker-compose.yml and .env
docker compose pull
docker compose up -d --force-recreate server worker
# Or run explicitly:
docker compose exec server sh -c \
'yarn command:prod cache:flush && yarn command:prod upgrade && yarn command:prod cache:flush'
docker compose exec server yarn command:prod workspace:sync-aventora-variables
Health check:
curl -sf https://crm.example.com/healthz
Keep the worker container running for BullMQ (TIPS cron, outbound jobs).
Development
npx nx run twenty-server:platform:upgrade
Equivalent manual steps: build → cache:flush → upgrade → workspace:sync-aventora-sales-person-fields → workspace:remove-standard-objects-prefill → cache:flush → workspace:sync-aventora-variables.
What upgrade does
- Pending TypeORM core migrations
- Workspace-level data migrations for current
APP_VERSIONminor - Updates
core.workspace.versionper workspace
Check workspace versions:
SELECT id, version, "displayName", "activationStatus"
FROM core.workspace
ORDER BY version NULLS FIRST;
New deploy checklist
- Update
.env(APP_VERSION, URLs, secrets) docker compose up -d(server runs migrations + upgrade + cron register)- Confirm
curl …/healthzand worker Up workspace:sync-aventora-variables- Existing workspaces with an industry preset:
workspace:reapply-industry-preset(seeds new[preset:…]workflows and cockpit rules without changing preset id/profile) - Review Settings → Workflows and Sales Cockpit → Automation Suggestions; activate templates you want live
Further deployment paths are available to customers under agreement. Contact sales@aventora.ai.
Admin CLI commands
Run in production:
docker compose exec server yarn command:prod <command> [options]
Dev:
npx nx run twenty-server:command -- <command> [options]
Quick index
| Command | Purpose |
|---|---|
upgrade | Primary migrator — run on every deploy |
cache:flush | Flush Redis; run around upgrade |
workspace:sync-aventora-variables | Ensure Aventora app vars on all workspaces (non-destructive) |
workspace:sync-aventora-sales-person-fields | Sales Cockpit Person fields (auto after upgrade in Docker) |
workspace:remove-standard-objects-prefill | Soft-delete Twenty demo seed rows (fixed UUIDs only) from active/suspended workspaces; skipped when SHOULD_PREFILL_STANDARD_OBJECTS=true unless --force. Auto on Docker startup. Use --destroy for permanent removal. |
workspace:seed-aventora-sales-cockpit-demo | Demo signals on existing people |
workspace:apply-industry-preset | Apply preset to existing workspace (first time or with overrides) |
workspace:reapply-industry-preset | Re-apply preset from each workspace’s stored industryPreset / industryProfile |
tips:bootstrap-fields | TIPS fields + hub role + RLS |
tips:sync | One-off TIPS inbound sync |
cron:register:all | Register background crons (incl. TIPS) |
workspace:backfill-person-created-by | Backfill person.createdBy for PERSON_WALL readiness |
workspace:backfill-provisioned-workspace-admins | Promote the Laravel/TIPS workspace owner to Admin (auto on Docker startup) |
Demo sales signals
After upgrade and person-field sync:
docker compose exec server yarn command:prod workspace:seed-aventora-sales-cockpit-demo
Idempotent: adds demo signals on existing people in active workspaces. Does not create or delete CRM records.
Local dev (destructive full reset + demo signals):
npx nx database:reset twenty-server
Industry preset on existing workspace
Re-apply after upgrade (uses each workspace’s stored preset; no UUID or preset flags required):
docker compose exec server yarn command:prod workspace:reapply-industry-preset
Optional single workspace: add -w <workspace-uuid>. Idempotent: appliers only add missing [preset:…] workflows, rules, fields, etc.
First-time apply or change preset (requires workspace id; use --force if already applied):
docker compose exec server yarn command:prod workspace:apply-industry-preset \
-w <workspace-uuid> \
--industry-preset insurance \
--industry-profile general_insurance_advisor
Check state:
SELECT id, "displayName", "industryPreset", "industryProfile", "presetAppliedAt"
FROM core.workspace WHERE id = '<uuid>';
Provisioning and SSO
All routes: Authorization: Bearer <PROVISIONING_SECRET> on CRM_API_URL.
| Method | Path | Purpose |
|---|---|---|
POST | /auth/provision/resolve | Find or create workspace + user; optional customSettings. Returns twentyApiKey when a new workspace is created |
POST | /auth/provision/rotate-api-key | Revoke Partner Provisioning keys and mint a new Admin JWT (twentyApiKey) |
POST | /auth/provision/login-token | SSO token + workspaceUrl; optional page, avatar, customSettings |
POST | /auth/provision/person-ownership-context | Resolve workspace member + Person Wall scope for Hub contact creates |
POST | /auth/provision/user | Add user to existing workspace |
POST | /auth/provision/move-user | Move user membership from one workspace to another (access only) |
POST | /auth/provision/workspace | Create or reuse named workspace (supports industryPreset) |
GET | /auth/provision/workspace?subdomain= or ?displayName= | Lookup workspace |
GET | /auth/provision/workspaces?activationStatus=ACTIVE | List workspaces with hubConnected, adminEmail, assignedDomain |
POST | /auth/provision/link-workspace | Wire Hub↔CRM; returns twentyApiKey |
POST | /auth/provision/demo-tenant | Demo workspace + API key + Phone wiring |
POST | /auth/provision/sync-cockpit-action-mappings | Push Sales Cockpit mappings |
GET | /auth/provision/industry-preset-catalog | Preset list for UIs |
DELETE | /auth/provision/workspace?subdomain= | Hard-delete workspace (demo teardown) |
SSO sequence
All provisioning calls must run on the backend. The browser only receives the final workspaceUrl.
Resolve
POST /auth/provision/resolve
{
"email": "agent@example.com",
"firstName": "Alex",
"lastName": "Agent",
"tenantSubdomain": "acme",
"tenantDisplayName": "Acme Inc",
"avatar": "https://cdn.example.com/avatars/user.png",
"customSettings": {
"engagementInitiatorPhone": "6473710396"
}
}
tenantSubdomain optional — omit for personal workspace mode.
Optional avatar — same rules as login-token.
Optional customSettings — per-user CRM settings bag; see Custom settings (customSettings).
Response includes wasCreated: { workspace, user }. When wasCreated.workspace is true, the response also includes twentyApiKey — a long-lived Admin-role workspace API key (shown once). Store it server-side and use it for later /rest/* and /mcp calls on https://{subdomain}.{crm-host}. Repeat resolve for an existing workspace omits twentyApiKey.
{
"workspaceId": "...",
"subdomain": "acme",
"userId": "...",
"wasCreated": { "workspace": true, "user": true },
"twentyApiKey": "<jwt>"
}
Never return twentyApiKey to the browser. If the key was not stored, call POST /auth/provision/rotate-api-key (same PROVISIONING_SECRET as resolve). That invalidates existing Partner Provisioning keys and returns a new twentyApiKey (shown once). Do not use link-workspace or demo-tenant for this — those mint Hub/demo keys with different names.
Rotate partner API key
POST /auth/provision/rotate-api-key
{
"workspaceId": "<uuid>",
"workspaceSubdomain": "acme"
}
One of workspaceId or workspaceSubdomain is required. 404 if the workspace is missing; 400 if neither identifier is sent.
{
"workspaceId": "<uuid>",
"subdomain": "acme",
"twentyApiKey": "<jwt>",
"revokedKeyCount": 1
}
revokedKeyCount is 0 when the workspace never received a partner key (still mints a new one). Keys created in CRM Settings → API keys, plus Engagement Hub CRM Sync and Demo Phone Sync, are left intact. Store twentyApiKey server-side; never send it to the browser.
Login token
POST /auth/provision/login-token
{
"workspaceId": "...",
"email": "agent@example.com",
"firstName": "Alex",
"lastName": "Agent",
"page": "/objects/people",
"avatar": "https://cdn.example.com/avatars/user.png",
"customSettings": {
"engagementInitiatorPhone": "+15551234567"
}
}
page is optional — see Embedded page mode.
avatar is optional — a publicly accessible http or https image URL. CRM downloads the image, stores it as the workspace member profile picture, and uses it in the user profile and anywhere the member avatar is shown. Invalid URLs return HTTP 400. If the URL is unreachable or not an image, provisioning continues without an avatar (logged server-side).
customSettings is optional — see Custom settings (customSettings). The top-level engagementInitiatorPhone field is still accepted on this endpoint for backward compatibility; when both are sent, the top-level value wins.
Creates user + membership if missing. Redirect browser to workspaceUrl.
The returned workspaceUrl always includes aventoraSso=1 (CRM hides Log out for SSO sessions). When page is set, the URL also includes returnToPath. Pass optional embedded: true to add aventoraEmbedded=1 (hides left nav — use for iframe embeds only, not new-tab deep links).
Custom settings (customSettings)
Optional JSON object on user provisioning endpoints: resolve, login-token, user, and move-user. Integrators (Laravel, custom HTTP clients) can send per-user CRM values before CRM adds first-class API fields. Unknown keys are ignored (debug-logged server-side).
| Key | Type | CRM behavior |
|---|---|---|
engagementInitiatorPhone | string | Sets userWorkspace.aventoraEngagementInitiatorPhone when empty (North American 10-digit normalization). Users can also set this in Settings → Profile → Engagement callback number. |
Example:
{
"workspaceId": "...",
"email": "agent@example.com",
"customSettings": {
"engagementInitiatorPhone": "6473710396"
}
}
On login-token, engagementInitiatorPhone may still be sent as a top-level field (legacy). Prefer customSettings for new integrations.
Person ownership context (Hub contact creates)
Engagement Hub attributes CRM people to the initiating user by calling this endpoint before POST /rest/people.
POST /auth/provision/person-ownership-context
{
"workspaceId": "...",
"email": "agent@example.com",
"engagementInitiatorPhone": "+15551234567"
}
At least one of email or engagementInitiatorPhone is required. Phone matches userWorkspace.aventoraEngagementInitiatorPhone (same normalization as login-token provisioning). If the user is not yet a workspace member, CRM provisions them with the Aventora User role (same as extra SSO users; the workspace owner is Admin).
Response:
{
"workspaceMemberId": "...",
"displayName": "Jane Agent",
"personWallScopeFieldName": "tipsAgentId",
"personWallScopeValue": 42
}
personWallScopeFieldName / personWallScopeValue are included when Person Wall is enabled and PERSON_WALL_FIELD_NAME includes a legacy numeric field (e.g. TIPS tipsAgentId or tipsAgentId,createdBy). Hub copies both createdBy.workspaceMemberId and the scope field onto new people.
Engagement Hub env (same values as domain-chatbot provisioning):
| Variable | Description |
|---|---|
CRM_API_URL | Apex URL for /auth/provision/person-ownership-context |
CRM_PROVISIONING_SECRET | Must match CRM PROVISIONING_SECRET |
Domains must have crm_workspace_id in domain account_settings (set when CRM is enabled in aventora-admin).
Bulk SMS / calls: Person ownership is optional for CRM sync. Engagements, transcripts, and contact create/update still run via the workspace API key even when ownership lookup returns 404. Bulk rows often only carry user_phone_number (default broker phone) without a domain-chatbot user_id. Hub falls back to the Engagement Hub account email for ownership resolution when initiator email cannot be resolved from domain users. Phone-only lookup still requires userWorkspace.aventoraEngagementInitiatorPhone in CRM (set during SSO / login-token provisioning with engagementInitiatorPhone).
Embedded page mode (host-driven navigation)
Use this when your app (not CRM) owns navigation — e.g. a sidebar or submenu in aventora-admin that opens CRM in an iframe or new window.
Partner iframe embeds require CRM env FRAME_ANCESTORS (CSP allowlist). Staging leaves it unset so framing is denied. See Laravel Integration — Embedded navigation.
| Mode | Parameters | CRM behavior |
|---|---|---|
| Full CRM | No page | Normal CRM with left navigation and mobile bottom nav |
| Embedded page | page (+ embedded: true on direct API) | Left nav and mobile bottom nav hidden; user lands on the requested screen |
Laravel GET /crm/sso?page=... always sends embedded: true unless you pass embedded=0. Direct POST /auth/provision/login-token must include "embedded": true with page to hide nav (page alone only deep-links — used for Hub Call Log new-tab links that keep navigation).
In embedded mode:
- Log out is hidden in CRM (user exits via your app)
- Navigation is your responsibility — each submenu item should trigger a new SSO request with a different
page - The user may still follow in-page links (e.g. open a record); CRM nav stays hidden for that session
How to call SSO with page
Laravel host app (authenticated):
GET /crm/sso?page=/objects/people
| Query param | Required | Description |
|---|---|---|
tenant | No | CRM workspace subdomain. Omit for the user’s personal workspace. |
page | No | Full internal CRM path (see Available pages). Omit for full CRM. |
avatar | No | Public http/https image URL for the user’s CRM profile picture. Omit to leave unchanged. |
Direct provisioning API (server-to-server):
POST /auth/provision/login-token
{
"workspaceId": "uuid",
"email": "user@example.com",
"page": "/objects/people",
"embedded": true,
"avatar": "https://cdn.example.com/avatars/user.png"
}
The same optional avatar field is supported on POST /auth/provision/resolve and POST /auth/provision/user.
SSO response (Laravel GET /crm/sso)
{
"url": "https://{subdomain}.crm.example.com/verify?loginToken=...&aventoraSso=1&returnToPath=%2Fobjects%2Fpeople&aventoraEmbedded=1",
"loginToken": "...",
"expiresAt": "2026-...",
"workspaceId": "...",
"subdomain": "...",
"userId": "...",
"page": "/objects/people"
}
| Field | Description |
|---|---|
url | Open this URL in iframe, popup, or redirect |
loginToken | Short-lived token (also inside url) |
expiresAt | Token expiry (ISO timestamp) |
workspaceId | CRM workspace ID |
subdomain | Workspace subdomain |
userId | CRM user ID |
page | Echo of requested page (only when provided) |
Your frontend should open url — no extra client-side URL building is required. This JSON does not include twentyApiKey. On first workspace create the plugin fires CrmWorkspaceProvisioned so the Laravel backend can persist the key. After a rotate, listen for CrmWorkspaceApiKeyRotated the same way.
page parameter rules
- Format: full internal CRM path, starting with
/- Valid:
/cockpit,/objects/people,/settings/profile - Invalid:
cockpit,https://...,//evil.com
- Valid:
- Validation: invalid paths return HTTP 400 from
login-token - URL encoding: when passing
pageas a query param, encode it:page=%2Fobjects%2Fpeople - Permissions: a valid path may still show an empty or restricted view if the user lacks CRM permissions
- Custom objects: use
/objects/{pluralName}where{pluralName}is the object’s API plural name in that workspace
Blocked paths (return 400)
Do not use auth, onboarding, or sign-up routes:
/welcome,/verify,/verify-email/create/*,/invite-team,/plan-required,/book-call*/reset-password/*/(root alone)
Available page values
Main app screens
| Screen | page value | Notes |
|---|---|---|
| Sales Cockpit | /cockpit | Default home when Sales Cockpit is enabled |
| People (list) | /objects/people | Standard CRM object |
| Companies (list) | /objects/companies | Standard CRM object |
| Opportunities (list) | /objects/opportunities | Standard CRM object |
| Tasks (list) | /objects/tasks | Standard CRM object |
| Notes (list) | /objects/notes | Standard CRM object |
| Dashboards (list) | /objects/dashboards | If enabled in workspace |
| Workflows (list) | /objects/workflows | If enabled in workspace |
Record detail pages (optional deep links)
Pattern: /object/{singularName}/{recordId}
| Screen | page value |
|---|---|
| Person record | /object/person/{uuid} |
| Company record | /object/company/{uuid} |
| Opportunity record | /object/opportunity/{uuid} |
Replace {uuid} with the CRM record ID.
Settings screens (prefix /settings/)
In embedded mode (page + embedded: true / aventoraEmbedded=1), CRM hides all navigation including the settings sidebar. There is no /settings hub page — link each screen from your host app with its own page path (e.g. Profile, Accounts, Experience as separate menu items). See Laravel integration — Settings in embedded mode.
| Settings area | page value |
|---|---|
| Profile | /settings/profile |
| Experience (theme/locale) | /settings/experience |
| Connected accounts | /settings/accounts |
| Account emails | /settings/accounts/emails |
| Account calendars | /settings/accounts/calendars |
| Workspace general | /settings/general |
| Data model | /settings/objects |
| Members | /settings/members |
| Roles | /settings/roles |
| Domains | /settings/domains |
| Billing | /settings/billing |
| APIs & Webhooks | /settings/api-webhooks |
| Apps | /settings/applications |
| AI | /settings/ai |
| Security | /settings/security |
| Admin panel | /settings/admin-panel |
| Updates | /settings/updates |
Settings sub-pages with IDs (roles, API keys, etc.) follow the same pattern, e.g. /settings/roles/{roleId}.
Custom workspace objects
/objects/{objectPluralName}
/object/{objectSingularName}/{recordId}
Example: custom object plural deals → /objects/deals
Recommended host app pattern
| Your app menu | SSO call |
|---|---|
| CRM Home (full) | GET /crm/sso |
| Sales Cockpit | GET /crm/sso?page=/cockpit |
| People | GET /crm/sso?page=/objects/people |
| Companies | GET /crm/sso?page=/objects/companies |
| Opportunities | GET /crm/sso?page=/objects/opportunities |
| Profile | GET /crm/sso?page=/settings/profile |
| Experience | GET /crm/sso?page=/settings/experience |
| Connected accounts | GET /crm/sso?page=/settings/accounts |
Each click: (1) call /crm/sso with the chosen page, (2) open returned url. Switching sections = new SSO call, not in-CRM navigation.
Embedded vs full CRM
| Feature | Without page | With page |
|---|---|---|
| Left navigation | Visible | Hidden |
| Mobile bottom nav | Visible | Hidden |
| Log out in CRM | Hidden (SSO session) | Hidden (SSO session) |
| Landing screen | CRM default home | Your page path |
| Who navigates | User (CRM nav) | Your app (submenu + SSO) |
Verify embedded SSO
GET /crm/sso→ full CRM with navigationGET /crm/sso?page=/cockpit→ Cockpit only, no navGET /crm/sso?page=/objects/people→ People list, no navGET /crm/sso?page=/welcome→ 400 (invalid path)- Switch submenu → new SSO call with different
page→ correct screen each time
Existing workspace shortcut
When crm_workspace_id is already stored (aventora-admin after CRM enable), skip resolve and call login-token only with that workspaceId.
Add user explicitly
POST /auth/provision/user with workspaceId + email. For SSO, login-token alone is usually enough.
Provision workspace with preset
POST /auth/provision/workspace
{
"adminEmail": "admin@example.com",
"subdomain": "acme",
"displayName": "Acme Insurance",
"industryPreset": "insurance",
"industryProfile": "general_insurance_advisor"
}
Idempotent: if subdomain exists, adds/finds admin user and returns existing workspace.
Workspaces, users, and industry presets
When is a workspace created?
| Flow | Who picks workspace | Creates workspace? |
|---|---|---|
resolve + tenantSubdomain | Client passes subdomain | Yes, if subdomain new |
resolve without tenantSubdomain | CRM per email | Yes, personal workspace if none exists |
login-token with workspaceId | Caller (stored ID) | Never |
provision/workspace or demo-tenant | Explicit API call | Yes, if subdomain new |
TIPS tips:sync | TIPS_SYNC_WORKSPACE_ID env | Never |
| aventora-admin SSO | crm_workspace_id in domain settings | Never at login time |
Resolve decision tree
User already has a workspace membership:
- Returns that workspace (no create)
- If requested
tenantSubdomaindiffers →400
New user + tenantSubdomain provided:
- Subdomain exists → join workspace, create user if needed
- Subdomain missing → create shared workspace + user as admin
New user + no tenantSubdomain:
- Personal workspace for email exists → reuse
- Else → create personal workspace + user
Hard rules:
- One workspace per user through provisioning (multi-membership →
400) - Cannot move user to another workspace via SSO (
400) — usemove-userinstead - User exists with no membership →
400(orphan; manual cleanup)
Workspace owner vs extra users
On workspace create (resolve, provision/workspace, TIPS agent workspace), CRM assigns the standard Admin role to both AVENTORA_PLATFORM_ADMIN_EMAIL and the partner owner (adminEmail / TIPS agent). Extra users added later stay Aventora User (no Settings All Access). Existing tenants: workspace:backfill-provisioned-workspace-admins.
Admins must enable and use 2FA at login unless the CRM server sets USE_2FA=false.
Move user between workspaces
POST /auth/provision/move-user moves a user's login membership from workspace A to workspace B. It does not migrate CRM records (People, engagements, cockpit history, etc.) — those stay in the source workspace.
Requirements:
- User must belong to exactly one workspace (the
fromworkspace) - Source workspace must have at least one other member (cannot move the only member out)
- User cannot be the only admin in the source workspace unless another admin is assigned first
- Target workspace must be active
POST /auth/provision/move-user
{
"email": "agent@example.com",
"fromWorkspaceSubdomain": "tenant-a",
"toWorkspaceSubdomain": "tenant-b",
"firstName": "Alex",
"lastName": "Agent",
"avatar": "https://cdn.example.com/avatars/agent.png"
}
Use fromWorkspaceId / toWorkspaceId instead of subdomains when IDs are already stored.
Response:
{
"userId": "uuid",
"email": "agent@example.com",
"fromWorkspaceId": "uuid",
"fromSubdomain": "tenant-a",
"toWorkspaceId": "uuid",
"toSubdomain": "tenant-b",
"userRestored": false
}
userRestored is true when the core user row was soft-deleted and restored during the move.
After a successful move, update external systems (crm_workspace_id, SSO login-token workspaceId) to the to workspace. Issue a new login-token for the target workspace on next SSO.
| Situation | Result |
|---|---|
| User not found | 404 |
User not in from workspace | 400 |
User already in to workspace | 400 (use login-token only) |
| User in multiple workspaces | 400 |
Only member of from workspace | 400 |
Only admin of from workspace | 400 |
from and to are the same | 400 |
| Target workspace inactive | 400 |
Industry presets
Presets apply once at workspace activation when industryPreset is set on the workspace row. They add labels, custom fields, views, pipelines, dashboards, CRM workflow templates (DRAFT), and Sales Cockpit automation rules.
When a preset renames Person, Company, or Opportunity, the CRM also renames reciprocal relation field labels on linked records (for example, after Company becomes Employer, the Person detail field company shows Employer instead of Company). Only objects explicitly defined in the preset are updated; labels are not reset when a preset omits them.
Aventora workflow templates (v1)
Industry presets seed inactive Aventora automation in two layers. Templates are not created by upgrade alone on workspaces that already have presetAppliedAt set — run workspace:reapply-industry-preset after deploying a build that includes workflow seeding.
| Layer | Where in CRM | How to find | Default state | User activates |
|---|---|---|---|---|
| Aventora CRM Workflows | Workflows module | Settings → Workflows — names like Bulk SMS: Reconnect (internal [preset:…] key is hidden) | DRAFT | Open template → review → Activate |
| Sales Cockpit rules | Core aventoraAutomationRule table | Sales Cockpit (/cockpit) → Automation Suggestions panel | DRAFT / disabled | Enable rule; optional Auto-run when triggered |
Manual CRM workflows also appear on People records (single-record or bulk selection) via the workflow/command menu after activation.
CRM workflow templates (all industries)
| Key | UI name | Trigger | Hub action |
|---|---|---|---|
av_wf_manual_engage_person | Engage Person (Aventora) | Manual, single person | Form (channel + instruction) → engage |
av_wf_new_person_say_hello | New Person: Say Hello (SMS) | person.created | SAY_HELLO SMS |
av_wf_new_person_qualify_call | New Person: Qualify (Call) | person.created | CALL_LEAD phone |
av_wf_bulk_sms_checkin | Bulk SMS: Check Interest | Manual bulk people | CHECK_INTEREST SMS |
av_wf_bulk_reconnect | Bulk SMS: Reconnect | Manual bulk people | RECONNECT SMS |
av_wf_bulk_booking_link | Bulk SMS: Send Booking Link | Manual bulk people | SEND_BOOKING_LINK SMS |
av_wf_happy_birthday | Happy Birthday | Daily CRON 0 14 * * * (14:00 UTC) | Query people whose Date of Birth is today (month/day) → SAY_HELLO SMS |
av_wf_holiday_new_year | Happy Holiday: New Year | Yearly CRON 0 14 1 1 * | Query all people (capped at 200) → SMS |
av_wf_holiday_canada_day | Happy Holiday: Canada Day | Yearly CRON 0 14 1 7 * | Query all people (capped at 200) → SMS |
av_wf_holiday_independence_day | Happy Holiday: Independence Day | Yearly CRON 0 14 4 7 * | Query all people (capped at 200) → SMS |
av_wf_holiday_christmas | Happy Holiday: Christmas | Yearly CRON 0 14 25 12 * | Query all people (capped at 200) → SMS |
av_wf_manual_thank_you | Thank You After Meeting | Manual, single person | SEND_FOLLOW_UP SMS |
Holiday templates are separate workflows. Activate only the ones that apply (Canada Day vs Independence Day). Thanksgiving is not seeded because it is not a fixed calendar date.
Date of Birth: Standard Person has no birthdate. Insurance uses insDateOfBirth. Generic, real estate, mortgage, and financial advisor seed Date Of Birth (avDateOfBirth). Birthday lookup uses the first of those fields that exists. People without a date of birth are skipped.
Cron time is UTC. Admins can edit the schedule after seed. Holiday/birthday blasts stay DRAFT until activated because they can message many people. Query audiences skip do not contact and missing phone. If nobody matches, the step skips instead of failing the run.
Industry CRM workflow templates
| Industry | Key | UI name | Trigger | Behavior |
|---|---|---|---|---|
| Insurance | av_wf_ins_renewal_reminder | Policy Renewal Reminder | Daily CRON 14:00 UTC | Policies with insRenewalDate in the next 30 days → SMS the related Client |
| Insurance | av_wf_ins_lapsed_policy | Lapsed Policy Outreach | policy.updated (insPolicyStatus) + FILTER LAPSED | RECONNECT SMS to the Client |
| Insurance | av_wf_ins_quote_presented | Quote Presented Follow-Up | opportunity.updated (insPolicyPipelineStage) + FILTER Quote Presented | SEND_FOLLOW_UP SMS to the quote contact |
| Real estate | av_wf_re_follow_up_due | Follow-Up Due Today | Daily CRON 14:00 UTC | People with reNextFollowUpDate today → SMS |
| Real estate | av_wf_re_new_listing | New Listing Check-In | listing.created | SMS the related Contact |
| Real estate | av_wf_re_showing_scheduled | Showing Scheduled Follow-Up | opportunity.updated (reBuyerPipelineStage) + FILTER Showing Scheduled | SMS the deal contact |
| Mortgage | av_wf_mtg_closing_reminder | Closing Reminder | Daily CRON 14:00 UTC | Borrowers with mtgClosingDate in the next 7 days → SMS |
| Mortgage | av_wf_mtg_funded_thank_you | Funded Thank You | opportunity.updated (mtgMortgagePipelineStage) + FILTER Funded | Thank-you SMS |
| Financial advisor | av_wf_fa_review_due | Review Due Today | Daily CRON 14:00 UTC | Clients with faNextReviewDate today → booking-link SMS |
| Financial advisor | av_wf_fa_ask_referral | Ask For Referral | Manual, single person | SMS asking for an introduction |
Generic industry gets the shared templates only.
Workflow steps use custom action types START_AVENTORA_ENGAGEMENT (single person), START_AVENTORA_ENGAGEMENT_BULK (known people), and START_AVENTORA_ENGAGEMENT_QUERY (scheduled audience lookup). All call the shared AventoraEngagementExecutionService, which respects doNotContact on the person and uses workspace Aventora app variables + cockpit action mappings.
The workflow editor Cockpit action type control is a dropdown of executable cockpit actions (SEND_FOLLOW_UP, CALL_LEAD, …). That mapping selects Hub type (informational, conversational, appointment booking, …). Instruction is a multiline field prefilled from the selected action; you can edit it before the workflow runs.
Cockpit automation templates
| Key | Name | Signals | Default action |
|---|---|---|---|
av_cockpit_missed_call | Missed Call Follow-Up | MISSED_CALL | CALL_LEAD |
av_cockpit_no_reply | No Reply Follow-Up | NO_REPLY | SEND_FOLLOW_UP |
When Auto-run is enabled on an active rule, the cockpit execution path applies cooldown and max executions per lead per day guards before calling Hub.
GraphQL (cockpit rules): updateAventoraAutomationRule — toggle enabled and autoExecute from the Automation Suggestions UI.
Compliance
Auto workflows (person.created, daily/yearly CRON birthday and holiday blasts, cockpit auto-run) can contact leads without a manual click. Review every template before activation. People marked do not contact or missing the channel contact field (phone for SMS/call, email for email) are skipped. Date-based audiences also require that date. Holiday query audiences are capped at 200.
The full catalog is served at runtime by GET /auth/provision/industry-preset-catalog. Current values:
industryPreset | industryProfile values | Default profile | Profile required? |
|---|---|---|---|
real_estate | buyer_agent, listing_agent, team_leader, broker, general_realtor | general_realtor | Yes |
insurance | personal_lines, commercial_lines, benefits_advisor, broker_owner, general_insurance_advisor | general_insurance_advisor | Yes |
mortgage | mortgage_agent, mortgage_broker, commercial_mortgage, team_lead, broker_owner | mortgage_agent | Yes |
financial_advisor | financial_advisor, wealth_advisor, retirement_specialist, team_lead, practice_owner | financial_advisor | Yes |
generic (aliases: general, other) | default | default | No |
Profile rules:
- For
real_estate,insurance,mortgage, andfinancial_advisor,industryProfileis required — a missing or unknown profile returns 400. - For
generic,industryProfileis optional and falls back todefault. - Unknown
industryPresetvalues return 400. The legacy onboarding idsgeneralandotherresolve togeneric.
Important: industryPreset / industryProfile are applied only when a workspace is created. When a call reuses or joins an existing workspace, the preset is ignored (the workspace keeps whatever preset it was created with). This applies to every endpoint that accepts the fields — resolve, workspace, and demo-tenant.
POST /auth/provision/resolve does accept industryPreset / industryProfile, but only takes effect when resolve creates a new workspace (new tenantSubdomain, or a new personal workspace when no subdomain is passed). A resolve that joins an existing subdomain — or returns a user's existing membership — gets no preset (generic CRM + standard Aventora fields). See Laravel Integration — resolve.
To control preset:
- Pass
industryPreset+industryProfileon the call that creates the workspace —POST /auth/provision/resolve(new subdomain/personal),POST /auth/provision/workspace, ordemo-tenant - Use onboarding UI (self-serve signup)
- Run
workspace:apply-industry-presetafter the fact (first time or to change preset/profile — required for existing workspaces, since preset is not re-applied on join) - Run
workspace:reapply-industry-presetafter upgrades to backfill new template artifacts on workspaces that already have a preset (also creates Listing / Policy custom objects when those industries are upgraded to preset v1.1.0+, Insurance client fields / marital-status options when upgraded to preset v1.2.0+, Date of Birth plus birthday/holiday/industry workflow templates on later versions)
TIPS: One shared workspace is created with insurance preset before sync. tips:sync only creates users in that workspace — not new workspaces or presets per agent.
Color theme presets
Workspace admins can choose an accent color theme for the whole CRM UI. This is separate from industry presets (which seed CRM data and workflows). Color themes only change accent colors — buttons, links, highlights, and related UI chrome. They do not switch light vs dark mode.
CRM has two independent appearance controls:
| Setting | What it changes | Scope | Where in CRM |
|---|---|---|---|
Color theme (themePreset) | Accent palette (Classic, Ocean, Forest, …) | Entire workspace — all members see the same accents | Settings → Workspace → General → Color theme |
Light / Dark / System (colorScheme) | Light mode, dark mode, or follow OS | Per user — each member chooses their own | Top workspace menu → Theme, or Settings → User → Experience → Appearance |
The workspace dropdown Theme submenu (System / Dark / Light) is the same personal colorScheme setting as User → Experience — not the workspace color theme.
Workspace color theme changes apply immediately for all members after save. Default for new and existing workspaces (post-migration) is classic (classic indigo).
Available presets
themePreset value | Label | Accent palette |
|---|---|---|
classic | Classic | Indigo (classic default) |
ocean | Ocean | Teal |
forest | Forest | Jade |
sunset | Sunset | Orange |
violet | Violet | Violet |
rose | Rose | Crimson |
midnight | Midnight | Iris |
Valid values are enforced on updateWorkspace; invalid IDs are rejected.
API and storage
- Column:
core.workspace."themePreset"(default'classic') - Migration: applied automatically during normal CRM database upgrade
- GraphQL:
updateWorkspace(data: { themePreset: "ocean" })on workspace metadata API - Permission:
PermissionFlagType.WORKSPACE(same as workspace name/logo)
Check current value:
SELECT id, "displayName", "themePreset"
FROM core.workspace
WHERE id = '<uuid>';
Programmatic update (admin session or API key with workspace settings access):
mutation UpdateWorkspaceTheme {
updateWorkspace(data: { themePreset: "ocean" }) {
id
themePreset
}
}
Deploy note
After upgrading to a build that includes color themes:
- Ensure core migrations have run (
yarn command:prod upgrade) — addscore.workspace.themePreset. - No extra CLI command is required; admins pick a preset in Settings → Workspace → General.
Color themes and industry preset workflows ship in the same CRM build but are configured independently.
Sales Cockpit
Signal-driven sales layer on Person records: ingest signals → scores → suggested actions → /cockpit dashboard and Person Sales tab.
Enablement checklist
- Deploy with
upgrade(Sales Cockpit migrations) workspace:sync-aventora-sales-person-fieldsandworkspace:sync-aventora-variables- Wire Hub↔CRM:
AVENTORA_API_KEY, webhooks,AVENTORA_WEBHOOK_SECRET - aventora-admin: Enable Aventora CRM; configure cockpit action mappings
- Confirm
IS_SALES_COCKPIT_ENABLEDon workspace - Optional demo data:
workspace:seed-aventora-sales-cockpit-demo
Feature flags
| Flag | Effect |
|---|---|
IS_SALES_COCKPIT_ENABLED | /cockpit nav, default home route |
IS_SUGGESTED_ACTIONS_ENABLED | Reserved |
Dashboard panels
Priority Leads, Suggested Actions, Reconnect Recent, Reconnect Oldies, Stale Leads, Hot Leads, Recently Engaged, Leads Requiring Follow-up, Team Activity, Automation Suggestions (enable preset rules and optional Auto-run when triggered; respects cooldown and daily max per lead).
Signal sources
| Source | How |
|---|---|
| Hub webhooks | Engagement outcomes → signals |
| REST | POST /rest/sales-intelligence/signals |
| GraphQL | createAventoraPersonSignal |
| Demo | workspace:seed-aventora-sales-cockpit-demo |
Cockpit action → Hub mappings
Configured in aventora-admin → Account Settings → CRM Sync when provider is Aventora CRM.
Stored in domain-chatbot twenty_cockpit_action_mappings, synced to CRM app var AVENTORA_COCKPIT_ACTION_MAPPINGS via POST /auth/provision/sync-cockpit-action-mappings.
Hub accepts informational, conversational, appointment_booking, confirmational — not raw cockpit action names like ESCALATE_TO_SALES.
Cockpit APIs
- GraphQL:
aventoraSalesCockpitSummary,aventoraPersonIntelligence, … - REST:
GET /rest/sales-intelligence/cockpit-summary,POST /rest/sales-intelligence/signals - Execute:
POST /rest/aventora/start-engagementwithcockpitActionType
Sales Cockpit troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| "Sales cockpit is not available yet." | GraphQL query failed (not empty data) | Browser DevTools → Network → GetAventoraSalesCockpitSummary → read errors[0].message. Check server logs. |
| SQL error on load (e.g. missing column) | Migrations or person fields not synced | upgrade, workspace:sync-aventora-sales-person-fields |
| Panels show "No leads to show" | Query succeeded, no signals | workspace:seed-aventora-sales-cockpit-demo or ingest signals |
| Execute 400, invalid Hub type | Mappings not synced | Admin → Sync to CRM |
| Feature missing | Flag off | IS_SALES_COCKPIT_ENABLED |
No [preset:…] workflows after upgrade | Workspace already had presetAppliedAt; templates not backfilled | workspace:reapply-industry-preset |
reapply-industry-preset errors on workflow insert | Older workspace workflow metadata | Upgrade to latest build (applier omits legacy actor columns); re-run workspace:reapply-industry-preset |
| Workflows nav shows 404 ("Off the beaten path") while other objects work | User's assigned role lacks Workflows settings permission (WORKFLOWS flag). Workflow objects require that flag even when canReadAllObjectRecords is true. Common on tenant workspaces (isPersonalWorkspace: false) when the user is still on Member instead of Aventora User. | Settings → Roles → Aventora User → enable Workflows, or reassign the user to Aventora User. On VPS: yarn command:prod workspace:ensure-aventora-user-role-permissions (also reassigns members on Hub-integrated workspaces), then cache:flush, log out and back in. Console may log [AventoraSSO] record-index-denied-read-access. |
Debug GraphQL: failed cockpit load always means backend error. Empty data still shows panel grid with "No leads to show."
CRM-first Hub provisioning (existing CRM workspaces)
Automatic (CRM-triggered)
When AVENTORA_HUB_AUTO_PROVISION_ENABLED=true on aventora-crm, provisioning a workspace or adding a workspace member enqueues a BullMQ job that calls Aventora Assistant POST /internal/crm-hub-provision. CRM holds only the callback URL + shared secret; domain-chatbot super-admin credentials and telephony env stay on Assistant.
| CRM env | Assistant env |
|---|---|
AVENTORA_HUB_AUTO_PROVISION_ENABLED=true | CRM_HUB_PROVISION_CALLBACK_SECRET (same value as CRM secret) |
AVENTORA_HUB_PROVISION_CALLBACK_URL | DOMAIN_CHATBOT_*, TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_SMS_NUMBER (same as Hub runtime) |
- New workspace (via
/auth/provision/*createWorkspaceWithAdmin): jobaction=fullafter ~60s delay (deduped per workspace). - New workspace member: job
action=sync_membersafter ~30s delay (skipped server-side if Hub not linked yet).
Requires CRM worker process running (same as TIPS sync). Failures retry up to 3 times; CRM provisioning itself never fails because of Hub auto-provision.
Partner workspace billing defaults (Hub): After domain + account creation, Hub applies pay-as-you-go + credit, 100 welcome credits (PARTNER_CRM_INITIAL_CREDITS or DEFAULT_INITIAL_CREDITS), and platform default engagement rates. Only runs while the account is still on the initial free plan from registration.
Manual batch (ops backfill)
When CRM workspaces already exist but domain-chatbot, Engagement Hub, and CRM↔Hub wiring are missing, your Aventora operator runs a provisioning backfill from the Hub deployment. That process:
- Lists CRM workspaces that are not yet linked to Hub.
- Creates the matching domain-chatbot tenant and Hub account for each workspace.
- Applies shared inbound/Twilio defaults and stores CRM link credentials.
- Optionally syncs workspace members into domain-chatbot and Hub when members were added before linking.
Contact your Aventora administrator or support if you need this backfill for an existing deployment.
Required env (Hub / aventora-phone)
| Variable | Used by |
|---|---|
DOMAIN_CHATBOT_API_URL (or DOMAIN_CHATBOT_URL) | Workspace list + reverse provision API |
DOMAIN_CHATBOT_SUPER_ADMIN_TOKEN or SUPER_ADMIN_USERNAME + SUPER_ADMIN_PASSWORD | domain-chatbot super-admin auth |
CRM_API_URL, CRM_PROVISIONING_SECRET | Person ownership context for Hub-created CRM contacts |
DATABASE_URL (optional) | Hub accounts.domain_name lookup in dry-run summary |
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN (or TWILIO_API_KEY_SID + TWILIO_API_SECRET), TWILIO_SMS_NUMBER / TWILIO_PHONE_NUMBER | Telephony stamped onto new Hub/domain accounts (same vars as Hub runtime). Optional PROVISION_* overrides still accepted |
TWILIO_MASTER_ACCOUNT_SID, TWILIO_MASTER_AUTH_TOKEN (or TWILIO_MASTER_API_KEY + TWILIO_MASTER_API_SECRET), TWILIO_WEBHOOK_BASE_URL | aventora-admin Add Domain auto-provision (search/buy number, create API key) |
TWILIO_PROVISION_TEST_MODE=true (domain-chatbot, test credentials only) | E2E auto-provision dry run: magic number search results, synthetic API key, purchase +15005550006 via Twilio test API (no real number charges). Disable before production provisioning. |
CRM_HUB_PROVISION_CALLBACK_SECRET | Authenticates CRM POST /internal/crm-hub-provision (same value as CRM AVENTORA_HUB_PROVISION_CALLBACK_SECRET) |
On AWS staging, Hub receives DOMAIN_CHATBOT_SUPER_ADMIN_TOKEN from the same Secrets Manager value as Chatbot PLATFORM_ADMIN_API_KEY (platform-wide domain API key). Username/password remain valid for VPS/Compose and bootstrap superadmin ensure-super-admin (authoritative CHATBOT_SUPER_ADMIN_* in platform/shared). Chatbot CRM-provision routes accept either a super-admin JWT or that platform-wide API key via Authorization: Bearer.
CRM CRM_API_URL and CRM_PROVISIONING_SECRET are required on Engagement Hub for person ownership resolution (Hub calls POST /auth/provision/person-ownership-context when creating CRM contacts). They remain required on domain-chatbot for workspace list, link-workspace, and demo provisioning.
Required env (domain-chatbot)
| Variable | Used by |
|---|---|
CRM_API_URL, CRM_PROVISIONING_SECRET | CRM workspace list + link-workspace |
Inbound phone and Twilio credentials for new domains can be set during aventora-admin → Add Domain (optional telephony section: manual API key + number, or auto-provision from the platform master Twilio account). Classic Auth Token is only available post-provision in Account Settings. CRM workspace backfill (CLI / provision-from-crm-workspace) still accepts operator-supplied env or payload credentials including classic auth token.
Prerequisites
- CRM deployed with migrations and
workspace:sync-aventora-variables - domain-chatbot and Engagement Hub running with service keys configured
- CRM workspace subdomain (or
AVENTORA_ASSIGNED_DOMAIN) must be a valid domain slug:[a-z0-9-]{3,30}
Demo workspace provisioning
Ephemeral demos provision a CRM workspace via domain-chatbot + POST /auth/provision/demo-tenant.
domain-chatbot env
| Variable | Description |
|---|---|
CRM_API_URL, CRM_PROVISIONING_SECRET | CRM provisioning |
CRM_PUBLIC_BASE_URL | Workspace URL builder |
CRM_WEBHOOK_SECRET | Must match CRM AVENTORA_WEBHOOK_SECRET |
DEMO_ENABLE_CRM | Default true; false to skip |
HUB_API_URL, HUB_API_KEY | Demo engagement |
CRM endpoints
POST /auth/provision/demo-tenant— workspace +twentyApiKey+ Phone wiring (supportsindustryPreset)DELETE /auth/provision/workspace?subdomain=— teardown
Verification
- Create demo invitation in admin
- Confirm
demo_confighascrm_workspace_id,crm_subdomain - Open demo live page → CRM tab
- Run engagement demo → contact appears in CRM
- On revoke/expiry → workspace deleted
Demo slugs capped at 30 characters for CRM subdomain validation.
TIPS Services sync
Optional: sync TIPS Advisors agents → CRM users; clients → people. Ships on main; enable with TIPS_* env.
Setup (Tip Services)
- Create workspace (onboarding: Insurance Brokerage preset, or
workspace:apply-industry-preset) - Set
TIPS_SYNC_WORKSPACE_IDon server and worker tips:bootstrap-fields(fields + Member (Aventora Hub) role)tips:sync- Optional schedule:
TIPS_SYNC_ENABLED=true, recreate server orcron:tips:sync
Inbound sync
For each TIPS agent with crmEnabled:
ensureUserInWorkspaceForTipsSync— same primitive as SSOfindOrCreateUserInWorkspace(+ password sync, role fromcrmHubEnabled)- Updates workspaceMember
tips*fields - Upserts Person records for clients
TIPS vs client SSO
Client SSO (resolve) | TIPS sync | |
|---|---|---|
| Workspace | Per tenantSubdomain or personal | Fixed TIPS_SYNC_WORKSPACE_ID |
| Creates workspace | Sometimes | Never |
| Industry preset | Only via provision/workspace | On pre-built workspace |
| Trigger | User clicks Open CRM | Cron / CLI |
Verify scheduled sync
docker compose ps worker
docker compose logs server 2>&1 | grep -i TipsSync
docker compose logs worker --since 24h | grep -i tips
Laravel plugin
Building your own Laravel integration? Use Laravel Integration — the shareable guide for custom HTTP clients (provisioning, SSO,
page,avatar). This section covers the official Composer package only.
Package: aventora-crm/laravel-plugin/
composer require aventora/crm-laravel
php artisan vendor:publish --tag=crm-config
AVENTORA_CRM_URL=https://crm.example.com
AVENTORA_CRM_PROVISIONING_SECRET=...
AVENTORA_CRM_DEFAULT_TENANT=acme # optional shared subdomain
Route: GET /crm/sso → resolve → login-token → open CRM.
Optional query param page — full CRM path for embedded page mode (deep-links; Laravel route also sends embedded to hide CRM nav):
GET /crm/sso?page=/objects/people
GET /crm/sso?tenant=acme&page=/cockpit
Response JSON includes url (ready to open), plus page when requested. See SSO response for field list. GET /crm/sso never returns twentyApiKey (long-lived Admin key). Listen for Aventora\Crm\Events\CrmWorkspaceProvisioned on first workspace create to persist it server-side. If that key was not stored, call Crm::rotateWorkspaceApiKey() and listen for CrmWorkspaceApiKeyRotated.
Health check:
php artisan crm:health-check --email=demo@local.test --tenant=acme
Blade:
@include('crm::components.button', ['tenant' => 'acme', 'label' => 'Open CRM'])
Contacts and workspace API keys
Base URL: https://{subdomain}.{crm-host}
POST /rest/people
Authorization: Bearer <CRM_WORKSPACE_API_KEY>
Content-Type: application/json
{
"name": { "firstName": "Jordan", "lastName": "Client" },
"emails": { "primaryEmail": "jordan@example.com", "additionalEmails": [] },
"phones": {
"primaryPhoneNumber": "5551234567",
"primaryPhoneCountryCode": "US",
"primaryPhoneCallingCode": "+1",
"additionalPhones": []
}
}
API keys from first-time resolve (twentyApiKey, store once), POST /auth/provision/rotate-api-key, link-workspace, demo-tenant, or CRM Settings → API keys.
Bulk email / engagement (workspace API key)
POST /rest/aventora/start-engagement-bulk sends informational email (or phone/SMS) through Engagement Hub.
- Some contacts:
{ "personIds": ["…"], "channel": "email", "type": "informational", "emailSubject": "…", "emailBodyHtml": "<p>…</p>" }(max 1000 ids). - All contacts:
{ "audience": "all", "channel": "email", "type": "informational", "emailSubject": "…", "emailBodyHtml": "<p>…</p>" }. Do not sendpersonIdsorviewId. CRM skips do not contact and people without email, then chunks Hub submits at 1000. - Person view:
GET /rest/aventora/person-viewsthen{ "viewId": "…", "channel": "email", "type": "informational", "emailSubject": "…", "emailBodyHtml": "<p>…</p>" }. Mutually exclusive withpersonIdsand"audience": "all". CRM applies the view’s filters at send time.
Use a Hub template (emailTemplateId + emailTemplateParams) instead of emailBodyHtml when the layout is saved in Admin. See Outbound Email Templates.
MCP (Cursor / Claude)
Workspace API keys can also call POST /mcp (JSON-RPC 2.0). That endpoint is the Aventora CRM MCP server: named tools wrap /rest/people and /rest/notes. Copy the Cursor snippet from Settings → AI. Full tool list: CRM MCP.
Claude Custom Connectors use the same /mcp URL with OAuth (authorization-code + PKCE), not an API key. Operators register the confidential client with application:register-claude-mcp-client. See CRM MCP.
For outbound calls and SMS, use the Engagement Hub MCP with a Hub API key — not CRM MCP.
Webhooks
Hub → CRM: POST https://{subdomain}.{host}/rest/aventora/webhook with X-Aventora-Webhook-Secret.
CRM → Hub: per-workspace AVENTORA_API_KEY + server AVENTORA_BASE_URL.
Troubleshooting
Provisioning / SSO
# 1. Right host?
curl -sS "https://CRM_HOST/healthz"
# 2. Route exists? (expect 401, not 404)
curl -sS -X POST "https://CRM_HOST/auth/provision/resolve" \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"email":"debug@test.example","tenantSubdomain":"test"}'
# 3. Valid secret (expect 200)
curl -sS -X POST "https://CRM_HOST/auth/provision/resolve" \
-H "Authorization: Bearer YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"email":"debug@test.example","tenantSubdomain":"test"}'
| HTTP | Meaning |
|---|---|
404 on /auth/provision/* | Wrong host, old image, or proxy path |
401 | Wrong PROVISIONING_SECRET |
400 on resolve | Workspace conflict, multi-workspace user, orphan user |
400 on move-user | Wrong membership state, only member/admin, inactive target workspace |
Never expose provisioning secret or workspace API keys in browser JavaScript.
Error handling checklist
404on provision routes → hostname/deploy, not secret401→ secret mismatch400onresolve→ tenant/user conflict404onlogin-token→ unknownworkspaceId400onlogin-token→ inactive workspace, or invalidpagepath (embedded mode)- User does not have permission when changing color theme → role lacks Workspace settings permission, or server build missing
themePresetin workspace field permissions; grant Workspace on the role or upgrade server
Changelog
| Date | Change |
|---|---|
| 2026-08-23 | Bulk email by Person view: GET /rest/aventora/person-views lists Person views. POST /rest/aventora/start-engagement-bulk accepts viewId (mutually exclusive with personIds and "audience": "all"). See Outbound Email Templates. |
| 2026-08-23 | Rotate partner API key: POST /auth/provision/rotate-api-key revokes Partner Provisioning keys and returns a new twentyApiKey. Same PROVISIONING_SECRET as resolve. Laravel Crm::rotateWorkspaceApiKey() fires CrmWorkspaceApiKeyRotated. |
| 2026-08-23 | Query / Build using AI eligibility: SMS and phone require a phone number; email requires an email; date audiences require that date (birthday = Date of Birth, month and day). Missing data skips the person; empty audience skips the run. Applies to every query outreach and record-triggered send, not only birthday texts. Lookup no longer fails with Property "phones" was not found in "person". |
| 2026-08-23 | Host env file permissions: Restrict the Compose .env on the VPS (chmod 600; typical path /opt/aventora/crm/.env). Do not commit live secrets. |
| 2026-08-21 | Build using AI: Optional workflow-editor overlay (server env USE_AI, default off). Users describe automations in everyday language; the builder applies Aventora defaults (birthday texts use Date of Birth / BIRTHDAY_TODAY) and does not quiz on API names. Writes a draft on this workflow. Does not enable Twenty Ask AI (IS_AI_ENABLED stays off). |
| 2026-08-21 | Admin 2FA: Server env USE_2FA (default true) requires workspace admins to enable and enter 2FA at login. Set USE_2FA=false to skip that enforcement. Compose passes the flag into server/worker. |
| 2026-08-21 | Workspace owner is Admin: Laravel adminEmail and the TIPS workspace-owner agent get the standard Admin role (full settings). Extra SSO users stay Aventora User. Platform admin remains Admin. Backfill: workspace:backfill-provisioned-workspace-admins (also runs on Docker startup). |
| 2026-08-20 | Workflow command menu: Preset workflow labels no longer show the internal [preset:av_wf_…] key. Users see Bulk SMS: Reconnect, not [preset:av_wf_bulk_reconnect] Bulk SMS: Reconnect. |
| 2026-08-20 | CRM workflow templates: Birthday, four fixed-date holiday blasts, and Thank You After Meeting seed as DRAFT on every industry. Insurance, real estate, mortgage, and financial advisor also get industry-specific DRAFT workflows. Birthday needs Date Of Birth filled (insDateOfBirth or avDateOfBirth). Holiday audience is capped at 200 and skips do-not-contact / missing phone. Cron is 14:00 UTC; activate only the holidays that apply. Existing workspaces pick these up via workspace:reapply-industry-preset. |
| 2026-08-20 | Voice memos: New notes/tasks default to Voice Note / Voice Task plus the date and time. Custom titles are not overwritten. |
| 2026-08-20 | Voice-memo transcription: Docker Compose passes OPENAI_API_KEY into the CRM server/worker. When the key is unset, Notes/Tasks Transcribe is disabled. |
| 2026-08-20 | Engage by Aventora disabled reason: Quick/bulk toolbar actions stay visible when blocked. Hover and click explain missing callback number (Settings → Profile), missing AVENTORA_ENGAGEMENT permission, or Hub not connected. |
| 2026-08-20 | Bulk email to all contacts: POST /rest/aventora/start-engagement-bulk accepts "audience": "all" (mutually exclusive with personIds). See Outbound Email Templates. |
| 2026-08-20 | Partner workspace API key: POST /auth/provision/resolve returns twentyApiKey (Admin role) only when a new workspace is created. Store it server-side; Laravel GET /crm/sso omits it and fires CrmWorkspaceProvisioned. |
| 2026-08-18 | Claude Custom Connectors: CRM POST /mcp returns HTTP 200 (not Nest’s default 201) so Claude can read tool results. See CRM MCP. |
| 2026-08-18 | Claude Custom Connectors: CRM /mcp named tools run as the user who approved OAuth. See CRM MCP. |
| 2026-08-18 | Claude Custom Connectors: CRM /mcp accepts Application OAuth tokens (mcp:tools) in addition to workspace API keys. See CRM MCP. |
| 2026-08-18 | CRM MCP: POST /mcp is an Aventora-owned named-tool server (People/Notes). Cursor snippet is under Settings → AI. See CRM MCP. |
| 2026-08-18 | /crm URL: docs.aventora.ai/crm redirects to this complete guide so in-app Documentation links resolve. |
| 2026-08-18 | In-app help: CRM pages and panels include an info icon for English contextual help. Documentation nav opens docs.aventora.ai/crm; in-app /docs one-pager removed. |
| 2026-08-17 | Workflows — Aventora Engagement: Cockpit action type is a dropdown. Instruction is a multiline field prefilled from the selected action and can be edited. |
| 2026-08-17 | Insurance preset v1.2.0: Broader Client fields (kids counts, source/status/market, commercial/recruiting fields). Reapply adds missing SELECT options (Couple) and deactivates Dependents. People spreadsheet import maps Email 2 / Phone 2 and Note/Task columns. |
| 2026-08-11 | Partner iframe embeds: CRM FRAME_ANCESTORS env — unset = DENY (staging); set = CSP allowlist for partner host origins. |
| 2026-08-09 | Chatbot superadmin ensure: platform bootstrap uses superadmin ensure-super-admin so platform/shared CHATBOT_SUPER_ADMIN_PASSWORD creates or reconciles the DB hash (re-run bootstrap to recover drift). API-key / DOMAIN_CHATBOT_SUPER_ADMIN_TOKEN health does not prove password-login alignment. |
| 2026-08-09 | CRM Hub provision Twilio env: stamps telephony from Hub runtime TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_SMS_NUMBER (or phone number / API key pair). Separate PROVISION_* Twilio vars are optional overrides only — admin Add Domain and CRM workspace auto-provision share the same credentials. |
| 2026-08-09 | AWS Hub CRM provision auth: staging Hub injects DOMAIN_CHATBOT_SUPER_ADMIN_TOKEN from the bootstrap-managed Chatbot PLATFORM_ADMIN_API_KEY (same Secrets Manager JSON key). Username/password remain a VPS/Compose / superadmin ensure-super-admin path; Chatbot super-admin routes accept JWT or platform-wide API key. |
| 2026-07-28 | White-label brand name: optional server env WHITE_LABEL (Docker .env → Compose) sets the partner display name for CRM UI labels, user-facing API messages, and transactional emails. Unset/blank keeps Aventora / Aventora CRM. Injected into the SPA via window._env_. Logos and aventora.ai URLs are unchanged. |
| 2026-07-22 | Workspace demo seed cleanup: Docker startup runs workspace:remove-standard-objects-prefill unless SHOULD_PREFILL_STANDARD_OBJECTS=true. Soft-deletes Twenty’s fixed demo UUIDs only (Airbnb/etc companies, people, opportunities, Quick Lead, My First Dashboard); does not touch user or integration data. Pass --destroy for permanent deletion. |
| 2026-07-22 | Workspace activation: demo CRM sample data (companies/people/opportunities/Quick Lead/dashboard) is no longer loaded by default. Opt in with server env SHOULD_PREFILL_STANDARD_OBJECTS=true. |
| 2026-07-19 | Contact record tabs: when Person has listings / policies relations (Real Estate / Insurance presets), the CRM shows dedicated Listings / Policies tabs as the second tab after Timeline, with an Add button to create related records. |
| 2026-07-19 | Industry presets reapply: workspace:reapply-industry-preset falls back to the industry default profile when a workspace has industryPreset but no industryProfile (common on older real_estate tenants), then persists that profile. |
| 2026-07-19 | Industry presets v1.1.0: Real Estate seeds a Listing custom object (Contact → Listings); Insurance renames Opportunity → Quote and seeds a Policy custom object (Client → Policies). New PresetCustomObjectsApplier; existing workspaces pick this up via workspace:reapply-industry-preset. |
| 2026-07-17 | Add Domain telephony: auto-purchased Twilio numbers now set inbound SmsUrl to {TWILIO_WEBHOOK_BASE_URL}/sms/webhook (plus existing VoiceUrl). Missing SmsUrl caused Hub to miss customer SMS replies while delivery status still worked. |
| 2026-07-14 | Embedded SSO: Laravel plugin again sends embedded: true whenever page is set (hides CRM left nav). Direct login-token still needs explicit "embedded": true; page alone only sets returnToPath (Hub Call Logs). |
| 2026-07-14 | Default phone country: optional server env DEFAULT_PHONE_COUNTRY_CODE (ISO, e.g. US) falls back when workspace Phones field Default Country Code is unset; exposed via client-config; workspace setting still overrides. |
| 2026-07-12 | Industry presets: replaced the truncated "example profiles" list with the full catalog (all industryProfile values, default profile, and which presets require a profile); documented general/other → generic aliases and 400 behavior. |
| 2026-07-12 | Industry presets: corrected stale note — POST /auth/provision/resolve does accept industryPreset / industryProfile; they apply only when resolve creates a new workspace (new subdomain or personal), matching Laravel Integration. |
| 2026-07-12 | Industry presets: applying or re-applying a preset now also renames reciprocal relation field labels when Person, Company, or Opportunity object labels change (for example person.company follows the Company singular label). |
| 2026-07-05 | Campaign landing pages: Hub /campaign/{token} uses TWENTY_BASE_URL / CRM_API_URL for CRM public config and submit. |
| 2026-07-05 | SSO deep links: login-token optional embedded: true adds aventoraEmbedded=1 (hide nav). page alone sets returnToPath only — use for new-tab person links from Engagement Hub Call Logs. |
| 2026-07-05 | Campaign landing pages: CRM Campaigns UI (/campaigns), Hub /campaign/{token} form, public submit API, tag-only vs auto-engage. See Campaign landing pages. |
| 2026-07-02 | Person ownership vs CRM sync: documented that bulk Hub outreach can sync contacts/engagements without ownership context; Hub uses account email fallback when domain-user email is missing. |
| 2026-07-02 | Add Domain telephony (test mode): domain-chatbot TWILIO_PROVISION_TEST_MODE=true enables end-to-end auto-provision with Twilio test credentials (magic number +15005550006, synthetic API key). Use live master credentials with this flag off for real purchases. |
| 2026-07-02 | Add Domain telephony: optional Twilio setup in aventora-admin (skip, manual API key, or auto-provision). provision-domain accepts API key auth only; classic auth token remains Account Settings only. |