Skip to main content
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.
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.

How it works

1

Request ads as usual

Call gravity.getAds(req, messages, placements) exactly as you do today. No new parameters. See Request ads.
2

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.
3

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.
4

User submits → engine stores the lead

On submit, the SDK POSTs 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).

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:
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.
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 POSTs 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.

Lead-form props on <GravityAd />

PropTypeDescription
sessionIdstringRequired for lead forms. Must match the ad-request session id.
apiKeystringBearer key for the submission POST. Falls back to process.env.GRAVITY_API_KEY.
userIdstringEnd-user id on your side. Stored with the lead for attribution.
userEmailstringUsed to pre-fill an email field when the campaign opts into prefill.email. The user can still edit it.
userPhonestringUsed 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.

Display modes

The advertiser picks how the form presents itself per campaign via leadForm.display. The SDK default is modal-bottom-right.
ModeBehavior
modal-bottom-rightThe ad trigger sits in your slot; clicking it opens the form as a floating card pinned to the bottom-right. Default.
modal-centerClicking the trigger opens a centered, full-screen overlay.
inlineA 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:
<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:
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>}
/>
PropTypeDescription
adAdResponse | nullThe ad from gravity.getAds().
sessionIdstringRequired. Attribution key; must match the ad request.
apiKeystringBearer key for the submission. Falls back to GRAVITY_API_KEY.
userId, userEmail, userPhonestringAttribution + pre-fill (see above).
displayOverride'inline' | 'modal-center' | 'modal-bottom-right'Override the campaign’s display mode.
openOnMountbooleanRender the form immediately instead of an ad card + CTA. Default false.
extraFieldsobjectExtra metadata merged into the submission payload.
submitUrlstringOverride the POST target. Defaults to ad.leadForm.submit_url.
onSubmitted(leadId, values) => voidFires on a successful (2xx + leadId) submission.
onSubmitError(err) => voidFires when the submission fails.
onDismiss() => voidFires when the user closes the form.
successContentReactNodeReplaces the form after a successful submit.
slotPropsobjectPer-element style/class overrides (see 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.
.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);
}
<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 POSTs to leadForm.submit_url (the engine’s /api/v1/lead).
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
  }'
FieldRequiredDescription
sessionIdYesAttribution key. Must match the ad request that served this ad.
campaignIdYesThe campaignId from the served ad. Echo it back verbatim.
fieldsYesSubmitted values keyed by LeadFormField.name.
userIdNoEnd-user id on your side.
placement / placementIdNoEcho from the served ad for slot-level reporting.
consentNoRequired 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.
StatusMeaning
200Lead stored. Body: { status, leadId }.
400unknown_campaign or campaign_not_configured — the campaign isn’t a lead-form ad unit.
401Missing/invalid bearer key.
403Request origin/referer doesn’t match the publisher’s registered domain.
503Transient server error or timeout — retryable; keep the form open.
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.

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.
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

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.
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.
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.
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.

Next

Show ads

The full rendering reference for GravityAd variants and slot styling.

Request ads

How the ad request works — the same call serves lead-form ads.