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

# Track conversions and turn them into leads

> Use orwel.conversion() for signups, demo requests and purchases: examples per form type, the delivery result, deduplication, attribution and the mistakes that lose leads.

Most analytics tools treat a signup like any other click. In Orwel a **conversion** is different: it becomes a **lead**, tied to the visitor and everything they did before, enriched with their company, scored, sent to your inbox and to your integrations (HubSpot, Hyperflow, Zapier, Brevo).

Use `track()` for behavior and `conversion()` for the moments that matter to revenue.

## The short version

```ts theme={null}
const { success, reason } = await orwel.conversion('demo_request', {
  email: form.email,
  name: form.name,
  company: form.company,
})
```

Three rules cover almost every install:

1. **Await it** before you navigate away or unmount the form.
2. **Always include `email`** (or a phone).
3. **Call it once per outcome**, after your own validation passed.

## Examples by type of form

> [tab] Contact or demo form
>
> ```ts theme={null}
> async function onSubmit(form) {
>   orwel.identify({ email: form.email, name: form.name })
>
>   const { success, reason } = await orwel.conversion('demo_request', {
>     email: form.email,
>     name: form.name,
>     company: form.company,
>     message: form.message,
>   })
>
>   if (!success) console.warn('Orwel conversion not delivered:', reason)
>   showThanks() // change the UI after the await
> }
> ```
>
> `identify()` links the anonymous visitor to the person, so their earlier visits show up on the lead.

> [tab] Signup
>
> ```ts theme={null}
> async function onSignup(user) {
>   orwel.identify({ email: user.email, name: user.name, plan: 'free' })
>   await orwel.conversion('signup', { email: user.email, plan: 'free' })
>   router.push('/onboarding')
> }
> ```
>
> Navigate only after the await, so you know the conversion arrived before the page changes.

> [tab] Purchase or upgrade
>
> ```ts theme={null}
> await orwel.conversion('purchase', {
>   email: customer.email,
>   amount: 499,
>   currency: 'USD',
>   plan: 'growth',
>   order_id: order.id,
> })
> ```
>
> `amount` and `currency` are stored with the lead. A payment signal (`amount`, `plan`, `order_id`, or a code like `purchase`) moves the visitor to the customer stage.

> [tab] Newsletter
>
> ```ts theme={null}
> await orwel.conversion('newsletter_signup', { email })
> ```
>
> Small conversions are still worth capturing: they show which pages and campaigns start a relationship, even before anyone asks for a demo.

> [!TIP]
>
> Purchases confirmed only on your server (for example a Stripe webhook) can be created with [`POST /api/v1/leads`](https://www.orwel.io/docs/api-reference/leads/create) and a secret key. Keep `conversion()` in the browser for everything the visitor does on your site, because only the browser knows the visitor and their campaign.

## What the SDK does when you call it

**1. Waits for the visitor**

It waits up to 5 seconds for the visitor id, so the conversion attaches to the visitor and their history. If the id never resolves, nothing is sent and you get `{ success: false, reason: 'visitor_unresolved' }`. A lead with no visitor behind it would have no history and no source.

**2. Gives it an id**

Each call gets a `conversionId` generated in the browser. The server deduplicates on it, so a retry or a double click creates one lead.

**3. Adds the campaign**

The first and last touch UTMs and ad click ids the SDK kept for this visitor travel with the conversion. See [Attribution](https://www.orwel.io/docs/sdk/attribution.md).

**4. Sends immediately**

A conversion does not wait for the next batch: it is sent on its own right away, with `keepalive` so it survives a page change. The promise resolves with what the server answered.

## The result

| Result | Meaning | What to do |
| - | - | - |
| `{ success: true }` | The server stored the conversion. | Nothing. |
| `{ success: false, reason: 'visitor_unresolved' }` | The visitor id did not resolve within 5 seconds, so nothing was sent. | Check that `orwel.init()` runs on this page and is not blocked. |
| `{ success: false, reason: 'delivery_failed' }` | The request went out but was not acknowledged: network error, timeout or rejection. | Log it. With the development key, Dev mode shows the rejection reason. |
| `{ success: false, reason: 'error' }` | Something failed inside the SDK. | Turn on `debug: true` and check the console. |

A delivery problem never rejects the promise, so your form keeps working when Orwel is unreachable. The promise rejects only for invalid input: a code that is not snake\_case, properties that are not an object, or a malformed email. Those are bugs to catch while you build, not at runtime.

## Mistakes that lose leads

| Mistake | What happens | Fix |
| - | - | - |
| Not awaiting before a redirect | The request may be cut off by the navigation, and you never learn whether it arrived. | `await orwel.conversion(...)`, then navigate. |
| No email in the conversion | The conversion is stored, but there is nothing to enrich, nobody to notify and HubSpot or other integrations skip it. | Pass `email` in the properties, even if you called `identify()` earlier. |
| Calling it from two places | A submit handler and a success callback both fire, with two different ids, so you get two leads. | Call it once, after validation succeeds. |
| Calling it before validation | Every attempt with a typo becomes a lead. | Validate first, convert after. |
| Using `track()` for the signup | It is stored as an event: no lead, no email, no integration. | Use `conversion()` for outcomes. |
| Server only rendering | `conversion()` runs on the server, where the SDK is a no-op. | Call it from client code (an event handler or a client component). |

## Choosing codes

Use snake\_case codes that name the outcome: `demo_request`, `contact_form`, `signup`, `trial_started`, `purchase`, `newsletter_signup`. Unlike `track()`, conversion codes do not need to be in the workspace catalog.

Keep one code per outcome rather than one per form. Two contact forms on different pages should both send `contact_form`; the visitor's session already shows which page each one was sent from.

## `track` or `conversion`

| | `track` | `conversion` |
| - | - | - |
| Use it for | Behavior: features used, steps completed | Outcomes: signups, demos, purchases |
| Returns | `{ success, eventId }` immediately | `Promise<{ success, reason? }>` after the server answers |
| Code must be in the catalog | Yes | No |
| Stored as | An event | A conversion and a lead |
| Triggers | Journeys and features | Enrichment, scoring, notification email, integrations |

## Where conversions show up

* **Leads** lists every lead with its score, company and source. Open one to see the conversions, the pages visited before them and the campaign that brought the visitor.
* **Campaigns** joins leads with ad spend, for cost per lead and cost per qualified lead.
* Your **inbox** and email get a notification for each new lead from the production key.

## Testing

Use the [development key](https://www.orwel.io/docs/guides/dev-mode.md) while you build. Conversions are stored as test leads with a **Dev** badge, no email or integration fires, and Dev mode shows each request with warnings such as a missing email or missing attribution. Add `?utm_source=test` to the URL to check that attribution reaches the lead.

> [card] [Attribution](https://www.orwel.io/docs/sdk/attribution.md)
>
> How leads get their source and campaign.

> [card] [Troubleshooting](https://www.orwel.io/docs/guides/troubleshooting.md)
>
> What to do when a conversion fails.


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