Skip to main content

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

  1. Concepts
  2. Environment variables
  3. Deployment and upgrade
  4. Admin CLI commands
  5. Provisioning and SSO
  6. Workspaces, users, and industry presets
  7. Sales Cockpit
  8. Campaign landing pages
  9. Demo workspace provisioning
  10. TIPS Services sync
  11. Laravel plugin
  12. Contacts and workspace API keys
  13. MCP (Cursor / Claude)
  14. Webhooks
  15. Troubleshooting

Concepts

ConceptCRM meaningAPI surface
UserPerson who can log in to a workspacePOST /auth/provision/* on apex CRM_API_URL
Contactperson record in the People modulePOST /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)

VariableRequiredDescription
PROVISIONING_SECRETYesBearer token for /auth/provision/* (server-side only)
APP_VERSIONYesValid semver (e.g. 1.20.0); drives workspace migrations. Never leave empty.
FRONTEND_URLMulti-workspaceApex URL, e.g. https://crm.aventora.ai
SERVER_URLYesAPI base; usually same as FRONTEND_URL
IS_MULTIWORKSPACE_ENABLEDProductiontrue for {subdomain}.crm.aventora.ai
AVENTORA_BASE_URLHub integrationEngagement Hub API base URL
AVENTORA_WEBHOOK_SECRETRecommendedValidates X-Aventora-Webhook-Secret on inbound webhooks
AVENTORA_HUB_AUTO_PROVISION_ENABLEDOptionalWhen true, enqueue Hub/domain provisioning after CRM workspace or member provisioning
AVENTORA_HUB_PROVISION_CALLBACK_URLWith auto-provisionAssistant URL, e.g. http://hub:8010/internal/crm-hub-provision
AVENTORA_HUB_PROVISION_CALLBACK_SECRETWith auto-provisionShared Bearer secret with Assistant (CRM_HUB_PROVISION_CALLBACK_SECRET)
DEFAULT_PHONE_COUNTRY_CODEOptionalISO 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_OBJECTSOptionalDefault 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_LABELOptionalPartner 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_2FAOptionalDefault 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)

VariableDefaultDescription
PERSON_WALLfalseKill switch; when true, members see only their own Person records unless workspace opts out
PERSON_WALL_WORKSPACE_IDSemptyComma-separated workspace UUID allowlist (empty = all workspaces)
PERSON_WALL_WORKSPACE_IDemptyDeprecated single-UUID allowlist
PERSON_WALL_EXCLUDE_SYSTEM_RECORDSfalseHide integration-created persons from scoped members
PERSON_WALL_SYSTEM_ACTOR_NAMEemptyDisplay name for system-created person createdBy. When empty, uses WHITE_LABEL (or Aventora).
PERSON_WALL_DEBUGfalseLog 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)

VariableRequiredDescription
CRM_API_URLYesApex URL for /auth/provision/*
CRM_PROVISIONING_SECRETYesMust match CRM PROVISIONING_SECRET
CRM_PUBLIC_BASE_URLContacts / URLsDefaults to CRM_API_URL
CRM_WORKSPACE_API_KEYContact writesWorkspace-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)

VariableRequiredDefaultDescription
TIPS_CLIENT_IDFor syncOAuth client id
TIPS_CLIENT_SECRETFor syncOAuth secret
TIPS_API_BASE_URLNohttps://tipsadvisors.tipservices.ca/apiInclude /api
TIPS_SYNC_WORKSPACE_IDFor syncSingle workspace UUID
TIPS_SYNC_ENABLEDNofalseEnable scheduled cron
TIPS_SYNC_CRON_PATTERNNo0 * * * *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:flushupgradeworkspace:sync-aventora-sales-person-fieldsworkspace:remove-standard-objects-prefillcache:flushworkspace:sync-aventora-variables.

What upgrade does

  1. Pending TypeORM core migrations
  2. Workspace-level data migrations for current APP_VERSION minor
  3. Updates core.workspace.version per workspace

Check workspace versions:

SELECT id, version, "displayName", "activationStatus"
FROM core.workspace
ORDER BY version NULLS FIRST;

New deploy checklist

  1. Update .env (APP_VERSION, URLs, secrets)
  2. docker compose up -d (server runs migrations + upgrade + cron register)
  3. Confirm curl …/healthz and worker Up
  4. workspace:sync-aventora-variables
  5. Existing workspaces with an industry preset: workspace:reapply-industry-preset (seeds new [preset:…] workflows and cockpit rules without changing preset id/profile)
  6. 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

CommandPurpose
upgradePrimary migrator — run on every deploy
cache:flushFlush Redis; run around upgrade
workspace:sync-aventora-variablesEnsure Aventora app vars on all workspaces (non-destructive)
workspace:sync-aventora-sales-person-fieldsSales Cockpit Person fields (auto after upgrade in Docker)
workspace:remove-standard-objects-prefillSoft-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-demoDemo signals on existing people
workspace:apply-industry-presetApply preset to existing workspace (first time or with overrides)
workspace:reapply-industry-presetRe-apply preset from each workspace’s stored industryPreset / industryProfile
tips:bootstrap-fieldsTIPS fields + hub role + RLS
tips:syncOne-off TIPS inbound sync
cron:register:allRegister background crons (incl. TIPS)
workspace:backfill-person-created-byBackfill person.createdBy for PERSON_WALL readiness
workspace:backfill-provisioned-workspace-adminsPromote 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.

MethodPathPurpose
POST/auth/provision/resolveFind or create workspace + user; optional customSettings. Returns twentyApiKey when a new workspace is created
POST/auth/provision/rotate-api-keyRevoke Partner Provisioning keys and mint a new Admin JWT (twentyApiKey)
POST/auth/provision/login-tokenSSO token + workspaceUrl; optional page, avatar, customSettings
POST/auth/provision/person-ownership-contextResolve workspace member + Person Wall scope for Hub contact creates
POST/auth/provision/userAdd user to existing workspace
POST/auth/provision/move-userMove user membership from one workspace to another (access only)
POST/auth/provision/workspaceCreate or reuse named workspace (supports industryPreset)
GET/auth/provision/workspace?subdomain= or ?displayName=Lookup workspace
GET/auth/provision/workspaces?activationStatus=ACTIVEList workspaces with hubConnected, adminEmail, assignedDomain
POST/auth/provision/link-workspaceWire Hub↔CRM; returns twentyApiKey
POST/auth/provision/demo-tenantDemo workspace + API key + Phone wiring
POST/auth/provision/sync-cockpit-action-mappingsPush Sales Cockpit mappings
GET/auth/provision/industry-preset-catalogPreset 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).

KeyTypeCRM behavior
engagementInitiatorPhonestringSets 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):

VariableDescription
CRM_API_URLApex URL for /auth/provision/person-ownership-context
CRM_PROVISIONING_SECRETMust 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.

ModeParametersCRM behavior
Full CRMNo pageNormal CRM with left navigation and mobile bottom nav
Embedded pagepage (+ 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 paramRequiredDescription
tenantNoCRM workspace subdomain. Omit for the user’s personal workspace.
pageNoFull internal CRM path (see Available pages). Omit for full CRM.
avatarNoPublic 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"
}
FieldDescription
urlOpen this URL in iframe, popup, or redirect
loginTokenShort-lived token (also inside url)
expiresAtToken expiry (ISO timestamp)
workspaceIdCRM workspace ID
subdomainWorkspace subdomain
userIdCRM user ID
pageEcho 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

  1. Format: full internal CRM path, starting with /
    • Valid: /cockpit, /objects/people, /settings/profile
    • Invalid: cockpit, https://..., //evil.com
  2. Validation: invalid paths return HTTP 400 from login-token
  3. URL encoding: when passing page as a query param, encode it: page=%2Fobjects%2Fpeople
  4. Permissions: a valid path may still show an empty or restricted view if the user lacks CRM permissions
  5. 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

Screenpage valueNotes
Sales Cockpit/cockpitDefault home when Sales Cockpit is enabled
People (list)/objects/peopleStandard CRM object
Companies (list)/objects/companiesStandard CRM object
Opportunities (list)/objects/opportunitiesStandard CRM object
Tasks (list)/objects/tasksStandard CRM object
Notes (list)/objects/notesStandard CRM object
Dashboards (list)/objects/dashboardsIf enabled in workspace
Workflows (list)/objects/workflowsIf enabled in workspace

Record detail pages (optional deep links)

Pattern: /object/{singularName}/{recordId}

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

Your app menuSSO call
CRM Home (full)GET /crm/sso
Sales CockpitGET /crm/sso?page=/cockpit
PeopleGET /crm/sso?page=/objects/people
CompaniesGET /crm/sso?page=/objects/companies
OpportunitiesGET /crm/sso?page=/objects/opportunities
ProfileGET /crm/sso?page=/settings/profile
ExperienceGET /crm/sso?page=/settings/experience
Connected accountsGET /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

FeatureWithout pageWith page
Left navigationVisibleHidden
Mobile bottom navVisibleHidden
Log out in CRMHidden (SSO session)Hidden (SSO session)
Landing screenCRM default homeYour page path
Who navigatesUser (CRM nav)Your app (submenu + SSO)

Verify embedded SSO

  1. GET /crm/sso → full CRM with navigation
  2. GET /crm/sso?page=/cockpit → Cockpit only, no nav
  3. GET /crm/sso?page=/objects/people → People list, no nav
  4. GET /crm/sso?page=/welcome400 (invalid path)
  5. 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?

FlowWho picks workspaceCreates workspace?
resolve + tenantSubdomainClient passes subdomainYes, if subdomain new
resolve without tenantSubdomainCRM per emailYes, personal workspace if none exists
login-token with workspaceIdCaller (stored ID)Never
provision/workspace or demo-tenantExplicit API callYes, if subdomain new
TIPS tips:syncTIPS_SYNC_WORKSPACE_ID envNever
aventora-admin SSOcrm_workspace_id in domain settingsNever at login time

Resolve decision tree

User already has a workspace membership:

  • Returns that workspace (no create)
  • If requested tenantSubdomain differs → 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) — use move-user instead
  • 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 from workspace)
  • 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.

SituationResult
User not found404
User not in from workspace400
User already in to workspace400 (use login-token only)
User in multiple workspaces400
Only member of from workspace400
Only admin of from workspace400
from and to are the same400
Target workspace inactive400

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.

LayerWhere in CRMHow to findDefault stateUser activates
Aventora CRM WorkflowsWorkflows moduleSettings → Workflows — names like Bulk SMS: Reconnect (internal [preset:…] key is hidden)DRAFTOpen template → review → Activate
Sales Cockpit rulesCore aventoraAutomationRule tableSales Cockpit (/cockpit) → Automation Suggestions panelDRAFT / disabledEnable 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)

KeyUI nameTriggerHub action
av_wf_manual_engage_personEngage Person (Aventora)Manual, single personForm (channel + instruction) → engage
av_wf_new_person_say_helloNew Person: Say Hello (SMS)person.createdSAY_HELLO SMS
av_wf_new_person_qualify_callNew Person: Qualify (Call)person.createdCALL_LEAD phone
av_wf_bulk_sms_checkinBulk SMS: Check InterestManual bulk peopleCHECK_INTEREST SMS
av_wf_bulk_reconnectBulk SMS: ReconnectManual bulk peopleRECONNECT SMS
av_wf_bulk_booking_linkBulk SMS: Send Booking LinkManual bulk peopleSEND_BOOKING_LINK SMS
av_wf_happy_birthdayHappy BirthdayDaily CRON 0 14 * * * (14:00 UTC)Query people whose Date of Birth is today (month/day) → SAY_HELLO SMS
av_wf_holiday_new_yearHappy Holiday: New YearYearly CRON 0 14 1 1 *Query all people (capped at 200) → SMS
av_wf_holiday_canada_dayHappy Holiday: Canada DayYearly CRON 0 14 1 7 *Query all people (capped at 200) → SMS
av_wf_holiday_independence_dayHappy Holiday: Independence DayYearly CRON 0 14 4 7 *Query all people (capped at 200) → SMS
av_wf_holiday_christmasHappy Holiday: ChristmasYearly CRON 0 14 25 12 *Query all people (capped at 200) → SMS
av_wf_manual_thank_youThank You After MeetingManual, single personSEND_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

IndustryKeyUI nameTriggerBehavior
Insuranceav_wf_ins_renewal_reminderPolicy Renewal ReminderDaily CRON 14:00 UTCPolicies with insRenewalDate in the next 30 days → SMS the related Client
Insuranceav_wf_ins_lapsed_policyLapsed Policy Outreachpolicy.updated (insPolicyStatus) + FILTER LAPSEDRECONNECT SMS to the Client
Insuranceav_wf_ins_quote_presentedQuote Presented Follow-Upopportunity.updated (insPolicyPipelineStage) + FILTER Quote PresentedSEND_FOLLOW_UP SMS to the quote contact
Real estateav_wf_re_follow_up_dueFollow-Up Due TodayDaily CRON 14:00 UTCPeople with reNextFollowUpDate today → SMS
Real estateav_wf_re_new_listingNew Listing Check-Inlisting.createdSMS the related Contact
Real estateav_wf_re_showing_scheduledShowing Scheduled Follow-Upopportunity.updated (reBuyerPipelineStage) + FILTER Showing ScheduledSMS the deal contact
Mortgageav_wf_mtg_closing_reminderClosing ReminderDaily CRON 14:00 UTCBorrowers with mtgClosingDate in the next 7 days → SMS
Mortgageav_wf_mtg_funded_thank_youFunded Thank Youopportunity.updated (mtgMortgagePipelineStage) + FILTER FundedThank-you SMS
Financial advisorav_wf_fa_review_dueReview Due TodayDaily CRON 14:00 UTCClients with faNextReviewDate today → booking-link SMS
Financial advisorav_wf_fa_ask_referralAsk For ReferralManual, single personSMS 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

KeyNameSignalsDefault action
av_cockpit_missed_callMissed Call Follow-UpMISSED_CALLCALL_LEAD
av_cockpit_no_replyNo Reply Follow-UpNO_REPLYSEND_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:

industryPresetindustryProfile valuesDefault profileProfile required?
real_estatebuyer_agent, listing_agent, team_leader, broker, general_realtorgeneral_realtorYes
insurancepersonal_lines, commercial_lines, benefits_advisor, broker_owner, general_insurance_advisorgeneral_insurance_advisorYes
mortgagemortgage_agent, mortgage_broker, commercial_mortgage, team_lead, broker_ownermortgage_agentYes
financial_advisorfinancial_advisor, wealth_advisor, retirement_specialist, team_lead, practice_ownerfinancial_advisorYes
generic (aliases: general, other)defaultdefaultNo

Profile rules:

  • For real_estate, insurance, mortgage, and financial_advisor, industryProfile is required — a missing or unknown profile returns 400.
  • For generic, industryProfile is optional and falls back to default.
  • Unknown industryPreset values return 400. The legacy onboarding ids general and other resolve to generic.

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:

  1. Pass industryPreset + industryProfile on the call that creates the workspace — POST /auth/provision/resolve (new subdomain/personal), POST /auth/provision/workspace, or demo-tenant
  2. Use onboarding UI (self-serve signup)
  3. Run workspace:apply-industry-preset after the fact (first time or to change preset/profile — required for existing workspaces, since preset is not re-applied on join)
  4. Run workspace:reapply-industry-preset after 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:

SettingWhat it changesScopeWhere in CRM
Color theme (themePreset)Accent palette (Classic, Ocean, Forest, …)Entire workspace — all members see the same accentsSettings → Workspace → General → Color theme
Light / Dark / System (colorScheme)Light mode, dark mode, or follow OSPer user — each member chooses their ownTop 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 valueLabelAccent palette
classicClassicIndigo (classic default)
oceanOceanTeal
forestForestJade
sunsetSunsetOrange
violetVioletViolet
roseRoseCrimson
midnightMidnightIris

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:

  1. Ensure core migrations have run (yarn command:prod upgrade) — adds core.workspace.themePreset.
  2. 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

  1. Deploy with upgrade (Sales Cockpit migrations)
  2. workspace:sync-aventora-sales-person-fields and workspace:sync-aventora-variables
  3. Wire Hub↔CRM: AVENTORA_API_KEY, webhooks, AVENTORA_WEBHOOK_SECRET
  4. aventora-admin: Enable Aventora CRM; configure cockpit action mappings
  5. Confirm IS_SALES_COCKPIT_ENABLED on workspace
  6. Optional demo data: workspace:seed-aventora-sales-cockpit-demo

Feature flags

FlagEffect
IS_SALES_COCKPIT_ENABLED/cockpit nav, default home route
IS_SUGGESTED_ACTIONS_ENABLEDReserved

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

SourceHow
Hub webhooksEngagement outcomes → signals
RESTPOST /rest/sales-intelligence/signals
GraphQLcreateAventoraPersonSignal
Demoworkspace: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-engagement with cockpitActionType

Sales Cockpit troubleshooting

SymptomLikely causeFix
"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 syncedupgrade, workspace:sync-aventora-sales-person-fields
Panels show "No leads to show"Query succeeded, no signalsworkspace:seed-aventora-sales-cockpit-demo or ingest signals
Execute 400, invalid Hub typeMappings not syncedAdmin → Sync to CRM
Feature missingFlag offIS_SALES_COCKPIT_ENABLED
No [preset:…] workflows after upgradeWorkspace already had presetAppliedAt; templates not backfilledworkspace:reapply-industry-preset
reapply-industry-preset errors on workflow insertOlder workspace workflow metadataUpgrade 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 workUser'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 envAssistant env
AVENTORA_HUB_AUTO_PROVISION_ENABLED=trueCRM_HUB_PROVISION_CALLBACK_SECRET (same value as CRM secret)
AVENTORA_HUB_PROVISION_CALLBACK_URLDOMAIN_CHATBOT_*, TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_SMS_NUMBER (same as Hub runtime)
  • New workspace (via /auth/provision/* createWorkspaceWithAdmin): job action=full after ~60s delay (deduped per workspace).
  • New workspace member: job action=sync_members after ~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:

  1. Lists CRM workspaces that are not yet linked to Hub.
  2. Creates the matching domain-chatbot tenant and Hub account for each workspace.
  3. Applies shared inbound/Twilio defaults and stores CRM link credentials.
  4. 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)

VariableUsed 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_PASSWORDdomain-chatbot super-admin auth
CRM_API_URL, CRM_PROVISIONING_SECRETPerson 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_NUMBERTelephony 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_URLaventora-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_SECRETAuthenticates 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)

VariableUsed by
CRM_API_URL, CRM_PROVISIONING_SECRETCRM 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

VariableDescription
CRM_API_URL, CRM_PROVISIONING_SECRETCRM provisioning
CRM_PUBLIC_BASE_URLWorkspace URL builder
CRM_WEBHOOK_SECRETMust match CRM AVENTORA_WEBHOOK_SECRET
DEMO_ENABLE_CRMDefault true; false to skip
HUB_API_URL, HUB_API_KEYDemo engagement

CRM endpoints

  • POST /auth/provision/demo-tenant — workspace + twentyApiKey + Phone wiring (supports industryPreset)
  • DELETE /auth/provision/workspace?subdomain= — teardown

Verification

  1. Create demo invitation in admin
  2. Confirm demo_config has crm_workspace_id, crm_subdomain
  3. Open demo live page → CRM tab
  4. Run engagement demo → contact appears in CRM
  5. 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)

  1. Create workspace (onboarding: Insurance Brokerage preset, or workspace:apply-industry-preset)
  2. Set TIPS_SYNC_WORKSPACE_ID on server and worker
  3. tips:bootstrap-fields (fields + Member (Aventora Hub) role)
  4. tips:sync
  5. Optional schedule: TIPS_SYNC_ENABLED=true, recreate server or cron:tips:sync

Inbound sync

For each TIPS agent with crmEnabled:

  • ensureUserInWorkspaceForTipsSync — same primitive as SSO findOrCreateUserInWorkspace (+ password sync, role from crmHubEnabled)
  • Updates workspaceMember tips* fields
  • Upserts Person records for clients

TIPS vs client SSO

Client SSO (resolve)TIPS sync
WorkspacePer tenantSubdomain or personalFixed TIPS_SYNC_WORKSPACE_ID
Creates workspaceSometimesNever
Industry presetOnly via provision/workspaceOn pre-built workspace
TriggerUser clicks Open CRMCron / 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/ssoresolvelogin-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 send personIds or viewId. CRM skips do not contact and people without email, then chunks Hub submits at 1000.
  • Person view: GET /rest/aventora/person-views then { "viewId": "…", "channel": "email", "type": "informational", "emailSubject": "…", "emailBodyHtml": "<p>…</p>" }. Mutually exclusive with personIds and "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"}'
HTTPMeaning
404 on /auth/provision/*Wrong host, old image, or proxy path
401Wrong PROVISIONING_SECRET
400 on resolveWorkspace conflict, multi-workspace user, orphan user
400 on move-userWrong membership state, only member/admin, inactive target workspace

Never expose provisioning secret or workspace API keys in browser JavaScript.

Error handling checklist

  • 404 on provision routes → hostname/deploy, not secret
  • 401 → secret mismatch
  • 400 on resolve → tenant/user conflict
  • 404 on login-token → unknown workspaceId
  • 400 on login-token → inactive workspace, or invalid page path (embedded mode)
  • User does not have permission when changing color theme → role lacks Workspace settings permission, or server build missing themePreset in workspace field permissions; grant Workspace on the role or upgrade server

Changelog

DateChange
2026-08-23Bulk 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-23Rotate 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-23Query / 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-23Host 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-21Build 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-21Admin 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-21Workspace 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-20Workflow 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-20CRM 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-20Voice memos: New notes/tasks default to Voice Note / Voice Task plus the date and time. Custom titles are not overwritten.
2026-08-20Voice-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-20Engage 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-20Bulk email to all contacts: POST /rest/aventora/start-engagement-bulk accepts "audience": "all" (mutually exclusive with personIds). See Outbound Email Templates.
2026-08-20Partner 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-18Claude Custom Connectors: CRM POST /mcp returns HTTP 200 (not Nest’s default 201) so Claude can read tool results. See CRM MCP.
2026-08-18Claude Custom Connectors: CRM /mcp named tools run as the user who approved OAuth. See CRM MCP.
2026-08-18Claude Custom Connectors: CRM /mcp accepts Application OAuth tokens (mcp:tools) in addition to workspace API keys. See CRM MCP.
2026-08-18CRM 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-18In-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-17Workflows — Aventora Engagement: Cockpit action type is a dropdown. Instruction is a multiline field prefilled from the selected action and can be edited.
2026-08-17Insurance 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-11Partner iframe embeds: CRM FRAME_ANCESTORS env — unset = DENY (staging); set = CSP allowlist for partner host origins.
2026-08-09Chatbot 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-09CRM 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-09AWS 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-28White-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-22Workspace 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-22Workspace 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-19Contact 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-19Industry 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-19Industry 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-17Add 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-14Embedded 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-14Default 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-12Industry presets: replaced the truncated "example profiles" list with the full catalog (all industryProfile values, default profile, and which presets require a profile); documented general/othergeneric aliases and 400 behavior.
2026-07-12Industry 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-12Industry 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-05Campaign landing pages: Hub /campaign/{token} uses TWENTY_BASE_URL / CRM_API_URL for CRM public config and submit.
2026-07-05SSO 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-05Campaign landing pages: CRM Campaigns UI (/campaigns), Hub /campaign/{token} form, public submit API, tag-only vs auto-engage. See Campaign landing pages.
2026-07-02Person 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-02Add 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-02Add 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.