API details

Visitor and conversation endpoints sit underneath the widget and inbox workflow.

Use these endpoints when you need lower-level customer messaging integration details, especially for identity and CRM context sync tied to a widget visitor.

Typical uses

  • Create or update visitor identity and profile data
  • Push custom contact metadata from your product into HAL
  • Track visitor or company milestones from your product using simple event names
  • Link a visitor to one or more CRM companies
  • Update company stage and company metadata during identify

The route most customers want

If you want your own app or backend to send custom contact or company information into HAL, use POST /api/widget/identify.

POST https://api.chatwithhal.com/api/widget/identify

This route is the supported integration surface for enriching a widget visitor with known customer identity and CRM context.

Request fields

  • app_id or project_id: the HAL project
  • visitor_id: your stable identifier for the current visitor session or user
  • user_id, email, name: known identity fields
  • metadata: custom visitor and contact-level properties to merge into HAL context, such as locale, plan, or lifecycle_segment
  • company_ids: one or more existing HAL company IDs to link
  • company: optional company object with id, name, stage, and metadata
  • stage_update_mode: optional override for CRM stage precedence
  • user_hash: optional verification signature when identity verification is enabled

What HAL does with the data

  • metadata is stored with the visitor and merged into the linked contact metadata when the visitor can be mapped to a contact
  • company.metadata is merged into the linked company metadata
  • company.stage can move the linked company in CRM, subject to stage precedence rules
  • company.id sets the active company context for later widget company writes
  • company_ids links the visitor’s contact to existing HAL companies

Write semantics

  • If the visitor has an email, identify can create or match the contact automatically
  • If that contact has no linked companies yet, HAL can create a placeholder company so the record has account context
  • If you later send a real company ID, HAL links the contact to that company and may remove the placeholder if nothing else uses it
  • company_ids links existing companies; it does not create new companies by arbitrary ID
  • company.id is the recommended way to establish which linked company later widget CRM writes should target by default
  • company.metadata and contact metadata are merged, not replaced wholesale

Example request

{
  "app_id": "proj_123",
  "visitor_id": "user_456",
  "email": "alice@acme.com",
  "name": "Alice",
  "metadata": {
    "locale": "sl",
    "plan": "enterprise",
    "role": "admin"
  },
  "company": {
    "id": "acme",
    "name": "Acme Inc",
    "stage": "Qualified",
    "metadata": {
      "arr_band": "50k-100k",
      "crm_owner": "Sam"
    }
  }
}

Use set-properties for follow-up updates

If you already identified the visitor and only need to append or refresh custom visitor/contact properties, use POST /api/widget/set-properties.

{
  "app_id": "proj_123",
  "visitor_id": "user_456",
  "properties": {
    "feature_flag": "beta",
    "last_seen_area": "billing"
  }
}

This route merges into visitor and linked contact metadata. It is useful for product context, but it does not update company metadata. Lifecycle email campaign conditions can target these values with fields such as visitor_metadata.locale and contact_metadata.lifecycle_segment.

Track milestone events

Use POST /api/widget/track when you want to record that a visitor or linked company did something at a point in time. Keep this simple: send a high-level event name and an optional scalar value.

{
  "app_id": "proj_123",
  "visitor_id": "user_456",
  "name": "plan-upgraded",
  "value": "pro",
  "entity": "visitor"
}

You can also track by your own user, email, or company identifiers. This is useful when your app knows the customer identity but does not want to pass HAL's internal visitor ID through the rest of your stack.

{
  "app_id": "proj_123",
  "visitor_id": "user_456",
  "user_id": "user_123",
  "email": "buyer@example.com",
  "name": "invoice.paid",
  "value": 123.45,
  "entity": "company",
  "company_id": "company_456",
  "company": {
    "name": "Acme Ltd",
    "domain": "acme.example"
  }
}

For company-level events, set entity to company. HAL resolves company_id by existing HAL ID or external company ID, then creates the company when needed. When identity verification is required for the project, include a valid user_hash for events that assert user_id or email, or call verified identify() first.

  • name is required, lowercased on the server, and . / $ are normalized to -
  • value is optional and must be a string, number, boolean, or null
  • occurred_at is optional and must be an ISO timestamp when provided
  • Use events for milestones and product signals, not raw clickstream noise
  • These events surface in contact timelines, company activity, and inbox context
  • Lifecycle email campaigns created through MCP can use these same event names as triggers

Lifecycle event names

HAL normalizes event names to lowercase and replaces . and $ with - for workflow matching. For example, first_invoice.created matches email campaigns as first_invoice-created. Use stable milestone names across projects so onboarding campaigns remain predictable.

Space Invoices

Common events include trial.started, user.first_seen, entity.created, first_invoice.created, first_invoice.sent, api_key.created, subscription.started, and usage.limit_reached.

Apollo white-label

Use the same milestone structure in a separate HAL project, then attach Apollo-specific templates, colors, sender identity, and campaigns so branding and reporting stay isolated.

Metadata fields in lifecycle campaigns

The same metadata written through identify and set-properties can be used by MCP lifecycle email campaigns. Use browser_language for the browser-detected locale, visitor_metadata.locale for a locale stored on the visitor, and contact_metadata.locale for a locale stored on the contact. Company attributes use company_metadata.<key>.

create_email_campaign({
  project_slug: "space-invoices",
  name: "Localized activation follow-up",
  template_id: "workflow-email-template-...",
  trigger: { type: "event", event_name: "first_invoice.created" },
  conditions: [
    { field: "visitor_metadata.locale", operator: "equals", value: "sl" },
    { field: "contact_metadata.lifecycle_segment", operator: "equals", value: "founder" }
  ]
})

Set stage for an identified visitor

Use POST /api/widget/set-stage when a widget-side session should move the active or explicitly targeted company to a specific CRM stage.

{
  "app_id": "proj_123",
  "visitor_id": "user_456",
  "company_id": "acme",
  "stage_name": "Qualified",
  "stage_update_mode": "advance_only"
}

This requires the visitor to already resolve to a contact with at least one linked company. In practice, that usually means calling identify() with an email first and sending company.id when you know the current account.

Set deal value for an identified visitor

Use POST /api/widget/set-deal when your app knows the current opportunity value and should update the active or explicitly targeted company record.

{
  "app_id": "proj_123",
  "visitor_id": "user_456",
  "company_id": "acme",
  "deal_value": 12000,
  "stage_name": "Qualified",
  "notes": "Annual plan in security review"
}
  • deal_value writes to the company, not the contact
  • stage_name is optional
  • company_id is optional but recommended when the contact can be linked to multiple companies
  • The route fails if the visitor has not been identified into a contact/company context yet

Company selection order

  • If company_id is provided on track, set-stage, or set-deal, HAL targets that linked company
  • Otherwise HAL uses the active company established by identify(company.id)
  • If there is no active company but exactly one linked company exists, HAL uses that company
  • A legacy first-linked fallback still exists for backward compatibility, but new integrations should not rely on link order

CRM stage precedence

Widget-facing stage updates accept an optional stage_update_mode field on visitor identify and stage-setting requests.

  • advance_only is the default and prevents widget updates from moving a company backward when the CRM already has a later stage
  • exact applies the requested stage exactly and should be reserved for authoritative backend syncs

The project default can be configured in Settings → Widget → CRM Stage Sync. Per-request values still override that default when you need different behavior.

What this is not

The current public API key surface under /api/v1 does not yet expose visitor/company metadata write routes. Use the widget-facing identify flow when you need to push end-user contact and company context.

Tracked events are intentionally simpler than a full analytics event schema: just a name, optional scalar value, and optional identity/company context.

Common failure cases

  • 401 on identify when identity verification is required but user_hash is missing or invalid
  • 404 on set-stage or set-deal when the supplied visitor_id does not exist in the project
  • 400 on set-stage or set-deal when the visitor exists but is not linked to a contact/company yet
  • 400 company_not_linked on legacy visitor-only track, set-stage, or set-deal when an explicit company_id is supplied but is not linked to the visitor's contact
  • 400 on legacy visitor-only track when entity is company but the visitor has no linked company context
  • 400 on track when value is not a scalar or name is blank
  • 401 on track when identity verification is required and a supplied user_id or email is not verified
  • 400 when stage_update_mode is not one of advance_only or exact

Guidance

Prefer the higher-level REST API and MCP docs first. Drop to this layer when you need deeper visitor-specific integration behavior tied to a customer-facing widget session.