> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hihobbes.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Track demo conversions

> Send session duration, email category, and buying-intent signals to your analytics and advertising tools.

Use browser events for live elapsed-time and accepted-email signals. Use the
existing `session.completed` webhook for final buying intent, including when
analysis finishes after the visitor closes the page. Hobbes emits the signals;
your GTM tags or webhook receiver submit conversions to your ad account.

## Browser contract

The existing `window.dataLayer.push` and `hobbes:event` CustomEvent carry:

| Event | Additional fields | When |
| - | - | - |
| `hobbes_email_classified` | `hobbes_email_domain_type` | The backend accepts the call-start request and returns its email-check result. |
| `hobbes_session_duration_threshold` | `hobbes_duration_threshold_seconds` (60, 300, 600), `hobbes_session_elapsed_seconds` | Once for each crossed threshold in a live demo. |

All subsequent session events also include `hobbes_email_domain_type` and
`hobbes_session_recording_id` when available. `hobbes_session_id` remains the
voice call ID for compatibility. Join to webhook `session.id` using
`hobbes_session_recording_id`, not the call ID. The organization, widget session,
surface, client timestamp, and existing first-touch attribution fields remain.
If a recording ID is absent, use a single delivery route for that conversion
signal. Cross-route deduplication requires the recording ID.

Categories are `personal`, `corporate`, `disposable`, and `unknown`. Corporate
means a company provider/domain type, not a qualified buyer. Rejected emails do
not emit a classification event. The current call-start policy rejects disposable
email; consumers should nevertheless accept that category in the contract.
Missing email, classification timeout, malformed/unknown result, and older sessions
are `unknown`. There is no additional classifier request or lookup. The new
browser fields contain no email address or email domain.

```js theme={null}
{
  event: 'hobbes_session_duration_threshold',
  hobbes_event: 'hobbes_session_duration_threshold',
  hobbes_surface: 'full_page',
  hobbes_organization_id: 'organization-uuid',
  hobbes_session_id: 'voice-call-uuid',
  hobbes_session_recording_id: 'recording-uuid',
  hobbes_email_domain_type: 'corporate',
  hobbes_duration_threshold_seconds: 300,
  hobbes_session_elapsed_seconds: 300,
  hobbes_gclid: 'captured-click-id',
  hobbes_client_timestamp: '2026-09-10T12:05:00.000Z'
}
```

Duration begins with `hobbes_session_started`: the accepted demo is ready and its
startup/research overlay has closed. Page load, launcher opening, lead submission,
and company research do not run this clock. Listening, mute, quiet gaps, and time
in a background tab count. A disconnected transport pauses emission; reconnection
with the same call ID catches up crossed thresholds using the original clock.
Explicit end, idle/system timeout, or terminal connection closure ends the clock.
Timer throttling may delay a push; elapsed seconds can exceed its threshold.
Re-renders and surface changes do not restart it. A new call starts a new set.
Unmount/page closure ends browser delivery; no intent polling or return visit is required.

The existing webhook `session.duration_seconds` remains recording wall time
(`end_time - start_time`). It can include backend startup and earlier chat in a
recording reused for a demo, so it is not numerically interchangeable with the
ready-demo browser clock. Choose and name a duration definition for each
conversion action; use one route consistently. Do not interpret either as active
speaking time or verified attention.

## GTM setup

1. Load your GTM container on the same document as the widget. For a
   hosted standalone demo, use its configured container. A browser data layer is
   document-local; an unrelated parent page does not automatically receive it.
2. Add Data Layer Variables for `hobbes_duration_threshold_seconds`,
   `hobbes_email_domain_type`, `hobbes_session_recording_id`, and
   `hobbes_organization_id`.
3. Add a Custom Event trigger named exactly
   `hobbes_session_duration_threshold`. Create one trigger per desired threshold,
   with the numeric variable equal to 60, 300, or 600. Optionally require email
   type to equal `corporate`.
4. For company-email conversions, use Custom Event `hobbes_email_classified`
   with `hobbes_email_domain_type` equal to `corporate`.
5. Attach the relevant conversion tag and configure consent through the existing
   CMP/GTM consent setup. A data-layer event is not consent to send advertising
   data. Preview in Tag Assistant before publishing your container.

Example conversion identity: `organization-uuid:recording-uuid:demo_ready_300s`.
Use a distinct definition for corporate-only milestones, such as
`corporate_demo_ready_300s`. Supply the same identity to your receiver if both
sources are involved. Do not make separate browser and offline conversion actions
for the same logical milestone and assume Google will deduplicate across them.
See [Google's data-layer guide](https://developers.google.com/tag-platform/tag-manager/datalayer).

## Completion webhook contract

Configure an enabled organization webhook with both `notify_on_active_session`
and `notify_on_inactive_session` enabled. Those cover eligible analyzed sessions
regardless of booking or qualification; enabling only booked/qualified filters
would omit the mid-funnel sessions this integration needs. Existing internal,
staging-preview, and unclaimed website-chat exclusions still apply.

The existing payload retains transcript, sales summary, contact/consent, and
tracking fields. This abbreviated example shows the conversion fields:

```json theme={null}
{
  "event": "session.completed",
  "idempotency_key": "session.completed:organization-uuid:recording-uuid",
  "timestamp": "2026-09-10T12:12:01+00:00",
  "session": {
    "id": "recording-uuid",
    "organization_id": "organization-uuid",
    "email_domain_type": "corporate",
    "start_time": "2026-09-10T12:00:00+00:00",
    "end_time": "2026-09-10T12:10:30+00:00",
    "analysis_completed_at": "2026-09-10T12:12:00+00:00",
    "duration_seconds": 630,
    "tracking": { "gclid": "captured-click-id", "utm_source": "google" }
  },
  "sales_summary": { "buying_intent": "high" },
  "person": { "overall_intent": "medium" }
}
```

`buying_intent` is this session's persisted analysis, not the person's aggregate.
Low, medium, and high are delivered; the receiver chooses its target levels.
Incomplete/failed analysis does not send `session.completed`. A missing intent is
not synthesized as low; skip it for scored conversions. Historical email category
and analysis timestamp may be unknown/null. `webhook.test` is a connectivity test,
not a conversion.

Use `session.end_time` for a conversion defined as "completed high-intent demo".
Use `analysis_completed_at` only if the chosen definition is "analysis became
available". Never use the receiver clock or top-level emission timestamp as a
replacement. A recording-duration threshold can use `start_time + threshold`
when start/end and duration support that threshold. Do not use that timestamp for
a ready-demo threshold with a different clock origin.

The logical event key is also sent in `Idempotency-Key`. Retries preserve it,
the body, and signature; rebuilding delivery for the same recording retains the
logical key. Existing bounded retries and delivery diagnostics are unchanged.

## Receiver and Google mapping

Verify `X-Webhook-Signature: sha256=<hex>` over the **raw request body** using
HMAC-SHA256 and the configured webhook secret, with a timing-safe comparison.
Validate the expected organization. Ignore events other than `session.completed`.
For example, select `sales_summary.buying_intent === 'high'`, optionally require
`session.email_domain_type === 'corporate'`, and choose a named definition such as
`completed_high_intent_v1`.

Claim a durable unique key `(organization_id, session.id, conversion_definition)`
in the same database transaction that queues the outbound conversion. Return 2xx
only after that transaction commits. An outbox worker can retry Google delivery
without losing the conversion if the process stops. A duplicate webhook should
acknowledge the existing item; do not mark an item delivered before Google accepts
it. An in-memory set alone is insufficient in a deployed receiver.

A minimal transformation after signature, organization, consent, and dedup checks:

```js theme={null}
function toGoogleEvent(payload, conversionKey) {
  if (payload.event !== 'session.completed') return null;
  if (payload.sales_summary?.buying_intent !== 'high') return null;
  const session = payload.session;
  if (!session?.tracking?.gclid || !session.end_time) return null;
  return {
    eventSource: 'WEB',
    eventTimestamp: session.end_time,
    transactionId: conversionKey,
    adIdentifiers: { gclid: session.tracking.gclid }
  };
}
```

For Data Manager, wrap that event in an ingestion request with a Google Ads
operating account and your offline conversion action as
`productDestinationId`. Add only the conversion value/currency you have
defined. Authentication, consent fields, account permissions, ingestion errors,
and diagnostics belong to your existing Google integration. Follow
[Google's event ingestion guide](https://developers.google.com/data-manager/api/devguides/events/send-events)
for the current request schema and destination setup.

Missing click IDs must remain missing and be reported as unmatched/skipped for
this click-based recipe. UTMs, a corporate email category, or a raw email address
are not a GCLID. This work does not introduce enhanced-conversion identifiers or
new Google consent collection. Existing Hobbes consent-aware first-touch storage
and verified cross-domain handoff are retained.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.