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

# Lead-form ad units

> Capture leads inline — render a Gravity lead form in your chat UI and let the engine handle submission, attribution, and billing.

Most Gravity ads are click-throughs: the user clicks and lands on the advertiser's
site. A **lead-form ad unit** captures the lead *in place* instead — the user
fills in a short form (email, phone, a question or two) without ever leaving your
app. The SDK renders the form, posts it to Gravity, and the engine stores the
lead and fires a conversion so attribution and CPA billing work out of the box.

<Note>
  Lead-form ads are served through the **same `gravity.getAds()` request** as
  every other ad. You don't request them differently — when an advertiser runs a
  lead-form campaign and it wins the auction, the ad object you receive simply
  carries an extra `leadForm` envelope. Render it and you're done.
</Note>

## How it works

<Steps>
  <Step title="Request ads as usual">
    Call `gravity.getAds(req, messages, placements)` exactly as you do today. No
    new parameters. See [Request ads](/ai-platforms/request-ads).
  </Step>

  <Step title="Engine attaches the form">
    If the winning campaign is configured as a lead-form ad unit, the engine
    attaches a `leadForm` envelope (`LeadFormConfig`) to that ad in the response.
    Regular campaigns don't carry it.
  </Step>

  <Step title="SDK renders the form">
    `<GravityAd />` detects `ad.leadForm` and renders the form (or a trigger that
    opens it). It fires the impression pixel on visibility, same as any ad.
  </Step>

  <Step title="User submits → engine stores the lead">
    On submit, the SDK `POST`s the values to `leadForm.submit_url` (the engine's
    `/api/v1/lead`). The engine hashes PII, stores the lead, and fires a
    conversion event for attribution + CPA billing. The SDK shows a success state
    (and optionally follows `success_redirect_url`).
  </Step>
</Steps>

## Render with `<GravityAd />`

The pre-built component handles everything. Two extra props are required for
lead-form ads compared to a normal click-through ad — **`sessionId`** and
**`apiKey`**:

```tsx theme={null}
import { GravityAd } from '@gravity-ai/react';

function ChatResponse({ response, ads, sessionId, user }) {
  return (
    <div>
      <p>{response}</p>
      {ads[0] && (
        <GravityAd
          ad={ads[0]}
          // Required for lead-form ads:
          sessionId={sessionId}            // must match the id used on the ad request
          apiKey={process.env.GRAVITY_API_KEY}
          // Optional — enables identity pre-fill (see below):
          userId={user?.id}
          userEmail={user?.email}
        />
      )}
    </div>
  );
}
```

You do **not** need to special-case the ad: `GravityAd` renders a normal
click-through when `ad.leadForm` is absent and the lead form when it's present.
The same component handles both, so a single integration covers every campaign
type an advertiser might run.

<Warning>
  Lead-form ads need two props that a plain click-through doesn't — miss either and the ad silently degrades:

  * **`sessionId`** must match the value you sent on the ad request — it's how the lead is attributed back to the impression. Omit it and the ad falls back to a plain click-through (you keep the impression and click; you just don't capture the lead inline).
  * **`apiKey`** authorizes the submission. The form `POST`s to `/api/v1/lead` with `Authorization: Bearer <apiKey>`; pass it explicitly or set `GRAVITY_API_KEY` in the environment. Without it the submission 401s. It's the same publisher key you already use to request ads.
</Warning>

### Lead-form props on `<GravityAd />`

| Prop        | Type     | Description                                                                                              |
| ----------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `sessionId` | `string` | **Required for lead forms.** Must match the ad-request session id.                                       |
| `apiKey`    | `string` | Bearer key for the submission `POST`. Falls back to `process.env.GRAVITY_API_KEY`.                       |
| `userId`    | `string` | End-user id on your side. Stored with the lead for attribution.                                          |
| `userEmail` | `string` | Used to pre-fill an email field when the campaign opts into `prefill.email`. The user can still edit it. |
| `userPhone` | `string` | Used to pre-fill a phone field when the campaign opts into `prefill.phone`.                              |

All other `GravityAd` props (`slotProps`, `showLabel`, `onImpression`,
`className`, …) work as documented in [Show ads](/ai-platforms/show-ads).

## Display modes

The advertiser picks how the form presents itself per campaign via
`leadForm.display`. The SDK default is `modal-bottom-right`.

| Mode                 | Behavior                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `modal-bottom-right` | The ad trigger sits in your slot; clicking it opens the form as a floating card pinned to the bottom-right. **Default.** |
| `modal-center`       | Clicking the trigger opens a centered, full-screen overlay.                                                              |
| `inline`             | A compact email-only ask renders directly in your slot — no click step. Best with email pre-fill.                        |

You can override the campaign's choice with `displayOverride` if a mode fits your
UI better:

```tsx theme={null}
<GravityAd ad={ad} sessionId={sessionId} apiKey={apiKey} />
// Force inline regardless of what the advertiser set:
<GravityLeadFormAd ad={ad} sessionId={sessionId} apiKey={apiKey} displayOverride="inline" />
```

## Identity pre-fill

If you already know the signed-in user's email or phone, pass `userEmail` /
`userPhone`. When the campaign opts in (`leadForm.prefill.email` /
`leadForm.prefill.phone`), the SDK seeds the first matching field so the user can
submit in one tap. Pre-fill is advisory — if you don't pass the value, the form
just renders blank.

With `display: "inline"` **and** an available pre-fill value, the SDK skips the
trigger entirely and shows the compact ask immediately.

## Render directly with `<GravityLeadFormAd />`

`<GravityAd />` delegates to `<GravityLeadFormAd />` under the hood. Use it
directly when you want lead-form-specific controls — `openOnMount`,
`displayOverride`, submission callbacks, or a custom success panel:

```tsx theme={null}
import { GravityLeadFormAd } from '@gravity-ai/react';

<GravityLeadFormAd
  ad={ad}
  sessionId={sessionId}
  apiKey={process.env.GRAVITY_API_KEY}
  userEmail={user?.email}
  openOnMount={false}                    // show the ad card first; open form on click
  onSubmitted={(leadId, values) => track('lead', { leadId })}
  onSubmitError={(err) => console.warn(err)}  // dev-only
  onDismiss={() => {}}
  successContent={<p>Thanks — we'll be in touch.</p>}
/>
```

| Prop                               | Type                                                 | Description                                                               |
| ---------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- |
| `ad`                               | `AdResponse \| null`                                 | The ad from `gravity.getAds()`.                                           |
| `sessionId`                        | `string`                                             | **Required.** Attribution key; must match the ad request.                 |
| `apiKey`                           | `string`                                             | Bearer key for the submission. Falls back to `GRAVITY_API_KEY`.           |
| `userId`, `userEmail`, `userPhone` | `string`                                             | Attribution + pre-fill (see above).                                       |
| `displayOverride`                  | `'inline' \| 'modal-center' \| 'modal-bottom-right'` | Override the campaign's display mode.                                     |
| `openOnMount`                      | `boolean`                                            | Render the form immediately instead of an ad card + CTA. Default `false`. |
| `extraFields`                      | `object`                                             | Extra metadata merged into the submission payload.                        |
| `submitUrl`                        | `string`                                             | Override the POST target. Defaults to `ad.leadForm.submit_url`.           |
| `onSubmitted`                      | `(leadId, values) => void`                           | Fires on a successful (2xx + `leadId`) submission.                        |
| `onSubmitError`                    | `(err) => void`                                      | Fires when the submission fails.                                          |
| `onDismiss`                        | `() => void`                                         | Fires when the user closes the form.                                      |
| `successContent`                   | `ReactNode`                                          | Replaces the form after a successful submit.                              |
| `slotProps`                        | object                                               | Per-element style/class overrides (see [Styling](#styling)).              |

## Styling

Every lead-form element ships with a stable `gravity-lf-*` class and the
component exposes `--gravity-leadform-*` CSS variables, so you can theme the form
to match your chat surface without touching component internals.

```css theme={null}
.my-chat-lead-card {
  --gravity-leadform-surface: var(--chat-bubble-bg);
  --gravity-leadform-border: var(--chat-border);
  --gravity-leadform-shadow: none;
  --gravity-leadform-accent: var(--brand);
  --gravity-leadform-accent-hover: var(--brand-strong);
}
```

```tsx theme={null}
<GravityLeadFormAd
  ad={ad}
  sessionId={sessionId}
  apiKey={apiKey}
  slotProps={{
    card: { className: 'my-chat-lead-card' },
    submit: { style: { background: '#111827' } },
    label: { style: { display: 'none' } },
  }}
/>
```

Stable hooks include `gravity-lf-trigger`, `gravity-lf-overlay`,
`gravity-lf-card`, `gravity-lf-header`, `gravity-lf-title`,
`gravity-lf-description`, `gravity-lf-inline-ask`, `gravity-lf-field`,
`gravity-lf-input`, `gravity-lf-consent`, `gravity-lf-submit`, and
`gravity-lf-close`.

## The submission endpoint

You only need this section if you're **not** using the SDK components — they do
all of this for you. The form `POST`s to `leadForm.submit_url` (the engine's
`/api/v1/lead`).

```bash theme={null}
curl -X POST https://api.trygravity.ai/api/v1/lead \
  -H "Authorization: Bearer $GRAVITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "sess_abc123",
    "campaignId": "8f3c1e2a-...-uuid",
    "userId": "user_789",
    "placement": "below_response",
    "placementId": "main",
    "fields": { "email": "ada@example.com", "use_case": "Analytics" },
    "consent": true
  }'
```

| Field                       | Required | Description                                                               |
| --------------------------- | -------- | ------------------------------------------------------------------------- |
| `sessionId`                 | **Yes**  | Attribution key. Must match the ad request that served this ad.           |
| `campaignId`                | **Yes**  | The `campaignId` from the served ad. Echo it back verbatim.               |
| `fields`                    | **Yes**  | Submitted values keyed by `LeadFormField.name`.                           |
| `userId`                    | No       | End-user id on your side.                                                 |
| `placement` / `placementId` | No       | Echo from the served ad for slot-level reporting.                         |
| `consent`                   | No       | Required only when the form rendered a consent checkbox (`consent_text`). |

**Success** is `HTTP 200` with `{ "status": "ok", "leadId": "..." }`. Treat any
other status as a failure and keep the form open — the SDK does exactly this, and
only fires `onSubmitted` / follows `success_redirect_url` when it gets a `200`
with a `leadId`.

| Status | Meaning                                                                                   |
| ------ | ----------------------------------------------------------------------------------------- |
| `200`  | Lead stored. Body: `{ status, leadId }`.                                                  |
| `400`  | `unknown_campaign` or `campaign_not_configured` — the campaign isn't a lead-form ad unit. |
| `401`  | Missing/invalid bearer key.                                                               |
| `403`  | Request origin/referer doesn't match the publisher's registered domain.                   |
| `503`  | Transient server error or timeout — retryable; keep the form open.                        |

<Warning>
  The endpoint deliberately returns a non-2xx (not a silent `204`) on failure, so
  a server-side error never looks like a captured lead. If you build a custom
  renderer, **only** show success on a `200` that contains a `leadId`.
</Warning>

## Attribution & billing

A submitted lead is recorded as a **conversion**, linked to the session,
impression, and campaign. If the advertiser runs the campaign on a CPA basis,
that conversion is what they're billed for — so lead-form ads monetize on
captured leads, not just clicks. Nothing extra is required on your side: the
impression pixel fires on view (as with any ad) and the conversion fires on
submit.

## The `leadForm` envelope

For reference, this is the shape attached to `ad.leadForm` when a lead-form
campaign wins. The advertiser configures these values on their campaign; you only
ever read them.

```ts theme={null}
interface LeadFormConfig {
  fields: LeadFormField[];        // 1–10 fields, ordered
  headline?: string;             // falls back to ad.title
  description?: string;          // falls back to ad.adText
  cta_text?: string;             // submit button label; falls back to ad.cta
  success_message?: string;
  privacy_text?: string;         // fine print under the form
  consent_text?: string;         // if set, a required consent checkbox is rendered
  success_redirect_url?: string; // followed on success (http/https only)
  display?: 'inline' | 'modal-center' | 'modal-bottom-right';
  prefill?: { email?: boolean; phone?: boolean };
  submit_url: string;            // engine-populated POST target
}

interface LeadFormField {
  name: string;                  // submitted key
  label: string;
  type?: 'email' | 'text' | 'tel' | 'url' | 'number' | 'textarea' | 'checkbox' | 'select';
  required?: boolean;
  placeholder?: string;
  options?: string[];            // for select / checkbox groups
}
```

## FAQ

<AccordionGroup>
  <Accordion title="Do I have to change my ad request?">
    No. Lead-form ads come back on the same `gravity.getAds()` call. The only
    change is rendering: pass `sessionId` and `apiKey` to `GravityAd` so it can
    render and submit the form.
  </Accordion>

  <Accordion title="What if I don't pass sessionId or apiKey?">
    Without `sessionId`, an ad that would have been a lead form renders as a
    normal click-through (you keep the impression and click). With `sessionId`
    but no `apiKey`, the form renders but submission 401s. Pass both for the full
    experience.
  </Accordion>

  <Accordion title="Is raw PII exposed?">
    The user types their details directly into the form on your page; on submit
    they go to Gravity over HTTPS, where the engine hashes PII at rest. Use the
    same `apiKey` you already use for ad requests.
  </Accordion>

  <Accordion title="Can I render the form myself?">
    Yes — read `ad.leadForm`, render your own inputs, and `POST` to
    `leadForm.submit_url` with the payload shape above. Only show success on a
    `200` containing a `leadId`.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Show ads" icon="window" href="/ai-platforms/show-ads">
    The full rendering reference for `GravityAd` variants and slot styling.
  </Card>

  <Card title="Request ads" icon="bolt" href="/ai-platforms/request-ads">
    How the ad request works — the same call serves lead-form ads.
  </Card>
</CardGroup>
