# Business conversions

A click goal measures a click, and a page goal measures a recorded page visit. An event goal measures an outcome your application reports after an operation succeeds, such as `lead_submitted` or `purchase_completed`.

Event goals choose **Browser-reported** or **Server-reported**. A browser event cannot satisfy a server-reported goal. Server reporting establishes that an authorized integration sent the outcome. Hoot Lens does not independently verify the lead, charge, or fulfillment.

## Set up a goal

In Settings, Goals, choose a business outcome and a static event name. The lead and purchase templates default to server reporting. Names use lower-case letters, digits, and underscores, starting with a letter, up to 64 characters. Do not use visitor details or transaction identifiers as names.

Counting starts with the goal definition. Changing its event name or reporting method starts a new definition, so old and new measurements do not share a baseline. An event goal excludes visits that began before its current definition, keeping goal counts and source-conversion rates aligned. Event goals work in the existing funnels, recording and visit filters, and before/after impact reports. Before/after comparisons remain observational.

## Report from the browser

Use the script tag's object API, or the equivalent ESM `track` export:

```js
// Run only after your application has confirmed that the operation succeeded.
window.Hootlens?.track('lead_submitted');

// USD15.00. Use an opaque random ID if your application needs to retry this outcome.
window.Hootlens?.track('purchase_completed', {
  id: outcomeReceiptId,
  amountMinor: 1500,
  currency: 'USD',
});
```

The lower-case command queue accepts `['track', name, options?]`. `track()` returns whether the active tracker accepted the event; it is not a delivery receipt. There is no pre-consent business-event queue. Missing, sampled-out, excluded, stopped, or privacy-blocked capture returns false and does not create a visitor record.

An optional ID must contain 16 to 128 ASCII letters, digits, underscores, or hyphens. Omit it to use a random ID. Do not use an order number, email, customer ID, or a hash derived from personal information. Reusing an ID in a page with different business content returns false. Network delivery retries reuse the immutable original batch.

Only `id`, `amountMinor`, and `currency` options are supported. Arbitrary properties, visitor identities, form contents, and caller-supplied provenance are rejected. A page can report at most 100 distinct business events, with at most 20 in one batch.

## Report from a server

Create a **conversion-only key** in Settings, Goals. Keep it in server environment variables. The PAT needs `conversions:write`, remains limited by its creator's current project permissions, and can be revoked. Read, replay, annotate, Firebase browser sign-in, and OAuth credentials do not authorize this endpoint.

Send the browser's optional anonymous context to your own backend alongside its existing operation:

```js
const hootContext = window.Hootlens?.context();
// Include hootContext in your application's existing request when it is available.
// A missing tracker or failed upload must never prevent the business operation.
```

Context contains only `projectId`, `recordingId`, and `sessionId`. It is available only during active, permitted capture and is not a credential. The recording may still be uploading. `Hootlens.flush()` can request delivery, but analytics must remain independent of the lead, registration, or payment succeeding.

After your backend has validated and completed the operation, persist a random receipt ID, the success timestamp, the original context, and any monetary value with a retryable background job. Derive purchase amounts from your authoritative transaction record, never from an untrusted frontend price. Keep that job's request unchanged across retries:

```js
const { recordingId, sessionId } = originalHootContext;
const response = await fetch(
  `${process.env.HOOTLENS_API_URL}/v1/projects/${process.env.HOOTLENS_PROJECT_ID}/conversions`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.HOOTLENS_CONVERSION_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      recordingId,
      sessionId,
      event: {
        id: persistedRandomReceiptId,
        name: 'lead_submitted',
        at: persistedSuccessTimestamp,
      },
    }),
  },
);
```

Use the project ID configured on your server, rather than choosing a destination from visitor input. Supply only the two correlation IDs in the body, not the context object's `projectId` field.

The API correlates only with an existing admitted recording and visit in that project. It does not create visits for people who declined recording or supply a campaign when none was captured. A not-yet-arrived recording is a retryable `409` correlation failure. Deleted, expired, foreign, or mismatched context is refused. A repeated identical receipt is idempotent; the same ID with different content returns `409`. Server and browser receipt namespaces are separate.

The response includes `recorded`, `duplicate`, `counted`, and `matchedGoalIds`. Test, internal, automated, unmatched, or otherwise ineligible outcomes do not become customer conversions. Do not interpret HTTP success alone as a counted conversion.

Each recorded page view accepts up to 100 distinct receipts per reporting method. The server endpoint also limits requests to 240 per minute and returns `422 conversion_limit` when that page view's receipt allowance is exhausted. A permanent limit error needs integration review rather than indefinite retries.

## Money and counting

One visit converts once per goal definition. Distinct purchases can add more reported events and value without adding another converting visit. For example, two purchases by one recorded visit produce one converting visit and two events.

Amounts are integers in the currency's minor unit, from zero to 1,000,000,000. Currency and amount must appear together. USD15.00 is `1500`, JPY1500 is `1500`, and KWD1.005 is `1005`. Supported codes are USD, EUR, GBP, CAD, AUD, NZD, JPY, CHF, SEK, NOK, DKK, INR, BRL, MXN, SGD, ZAR, KWD, BHD, OMR, KRW, AED, SAR, CNY, HKD, PLN, CZK, HUF, ILS, TRY, THB, MYR, PHP, and IDR.

Reports keep currencies separate. They perform no foreign-exchange conversion. An unsafe aggregate amount is unavailable and coverage is partial, rather than being reported as zero or an approximate total.

## Compare conversion sources

Traffic, **Which visits convert?**, selects a goal and its original channel, referrer, campaign source, campaign name, device class, entry page, or release. It shows eligible visits, converting visits, conversion rate, reported events, and separate currency totals.

The immutable attribution snapshot comes from the visit's first counted page. A later thank-you page, resize, or redirect does not replace its source. Rates use the same recorded population for numerator and denominator. These are rates among eligible recorded visits, not all website visitors or advertising-platform attribution.

Measurement has an explicit collection checkpoint. Historical breakdowns are unavailable before it and are not backfilled from later pages. Goal changes, bounded source buckets, missing instrumentation, or numeric overflow can make coverage partial. Per-shard source lists are capped, with additional values grouped under Other. Reports display up to 50 rows. Very broad ranges retain at most 2,500 source metric rows, preserve full totals, count the distinct omitted rows, and mark coverage partial.

The authenticated read endpoint is `GET /v1/projects/:projectId/conversions/report?from=<epoch-ms>&to=<epoch-ms>&goalId=<id>&dimension=channel`. Ranges cover at most 92 UTC days. Both conversion rates and event/value totals use the UTC day the visit began, as named by `windowBasis: visit_started`. A release breakdown names the release seen on entry, not proof that a release caused a conversion.

Agents with `read` can call `get_conversion_report` through MCP. That tool cannot submit outcomes. Submission stays with the server integration key.
