Guide

OpenAI Ads Lead Generation Tracking: Pixel and CAPI Setup

Lead-gen advertisers running OpenAI Ads need lead_created and registration_completed firing correctly from the moment a form is submitted, and the Conversions API (CAPI) matters more here than in ecommerce because leads routinely convert to a qualified sale or booked appointment offline, days after the original click. Get the pixel-side event right, then close the loop by sending the qualified-lead update from your CRM via CAPI — including the __oppref value captured at form submission, which is the detail most lead-gen setups miss entirely.

This guide covers the standard events that apply to lead generation, how to fire them correctly on form submit, how to send the follow-up qualified-lead event from your CRM once a human has actually reviewed the lead, and the common gaps — multi-step forms, third-party tools like Typeform or Calendly — that break tracking silently if you don't plan for them. If you haven't set up the pixel and CAPI at all yet, start with the Conversions API setup guide or the complete guide to ChatGPT Ads tracking — this post assumes both are already installed and focuses specifically on the lead-gen event layer on top of them.

The lead-gen event set

OpenAI Ads' supported events reference defines two standard events that cover most lead-gen tracking needs:

EventTypical triggerNotes
lead_createdForm submission, contact requestThe core lead-gen event — fire this the moment a visitor submits
registration_completedAccount or membership createdUse when the conversion is a full account, not just a contact form

Notice what's not in that list: there is no standard appointment_scheduled event. If your business model runs on booked calls or demos rather than form fills, don't assume that event name exists — check the supported events reference directly before you build against it. Your options are:

  1. Map the booking to lead_created, if a booked appointment is functionally your lead-capture moment (most demo-booking funnels work this way).
  2. Use a custom event with a clearly labeled custom_event_name (something like appointment_scheduled or demo_booked) if you need it tracked distinctly from a generic form-fill lead.

Either approach is valid — the mistake is guessing the name is standard and never verifying it. A custom event dedupes correctly as long as the custom_event_name matches between your pixel call and your CAPI call, same as the id field does for standard events.

Step 1: Fire lead_created on form submit (pixel side)

Once your pixel is initialized with your Pixel ID —

oaiq("init", { pixelId: "YOUR-PIXEL-ID" });

— fire lead_created in your form's submit handler, after you've confirmed the submission actually succeeded (not just that the user clicked "Submit"):

form.addEventListener("submit", function (event) {
  // ... your existing validation / submission logic ...

  fetch("/api/leads", {
    method: "POST",
    body: formData
  }).then(function (response) {
    if (response.ok) {
      oaiq("measure", "lead_created", {
        content_name: "demo-request-form"
      });
    }
  });
});

A few details worth getting right here:

  • Fire on confirmed success, not on click. Firing the event on the click handler before you know the submission actually went through will count failed submissions as leads. Wait for your backend to confirm the write succeeded.
  • Use content_name (or a similar identifying field) to distinguish forms if your site has more than one — a newsletter signup and a demo request are both plausibly lead_created events, but you'll want to tell them apart in reporting.
  • The pixel auto-captures the rest — source_url, timestamp, and the __oppref cookie value tying this submission back to the ad click that drove the visit, within the roughly 30-day attribution window the pixel maintains.

This gets you a real-time, browser-side signal that someone submitted a form. It does not tell you whether that person was a real, qualified prospect — that's what Step 2 is for, and it's the part most lead-gen tracking setups skip entirely.

Step 2: Send the qualified-lead event via CAPI from your CRM

This is the step that makes lead-gen tracking fundamentally different from ecommerce tracking. A purchase event fires once, at the moment of purchase, and that's the whole story. A lead is the start of a story that often plays out over days, inside your CRM, with no browser involved at all — a sales rep calls the lead, qualifies it, and only then does it become worth anything to your ad account's optimization signal.

That's exactly what CAPI is for: a server-side POST that doesn't depend on a browser session, sent whenever your CRM logic decides the lead is qualified — hours or days after the original form submit.

curl -X POST "https://bzr.openai.com/v1/events?pid=PIXEL-ID" \
  -H "Authorization: Bearer YOUR-API-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": [{
      "id": "lead-48213-qualified",
      "type": "lead_created",
      "action_source": "system_generated",
      "timestamp_ms": 1721000000000,
      "data": {
        "content_name": "demo-request-form",
        "oppref": "OPPREF-VALUE-CAPTURED-AT-FORM-SUBMIT"
      }
    }]
  }'

Two things in that payload are doing the real work:

The oppref field. CAPI does not automatically capture the __oppref cookie the way the pixel does — that only happens in the browser. If you want this CRM-side event attributed back to the ad that drove the original visit, your form-submit handler has to read the __oppref cookie value at the moment of submission and store it alongside the lead record in your CRM (a hidden form field or an API parameter both work). Then, when your CRM sends the qualified-lead event days later, it passes that stored value through explicitly. Skip this step and your qualified-lead events still land in OpenAI Ads Manager, but without the attribution context that connects them back to a specific campaign — which defeats much of the point of tracking them separately in the first place.

action_source. For events your CRM generates based on internal logic — a sales rep marking a lead qualified, a scoring rule crossing a threshold — action_source values like system_generated or other (check current accepted values against the Conversions API reference) are more accurate than implying it came from a live web session, since by this point it usually hasn't. Amounts, if you attach deal value to a qualified lead, follow the same rule as every other monetary field in OpenAI Ads: ISO 4217 minor-unit integers with a currency code (so $500 is 50000 with "currency": "USD"), not a decimal dollar figure.

One more thing worth flagging here since it's the same integration pattern: the Bearer key in that request has to live in your CRM's server-side environment, never in a browser-facing script or a client-side integration snippet. If your CRM setup involves any code your marketing team can edit directly, review how to secure a CAPI implementation before this goes live — a leaked key here is the same risk as a leaked key anywhere else CAPI is used.

Why "form submitted" is not the same signal as "qualified lead"

It's tempting to treat Step 1 as the whole job — a form got submitted, fire lead_created, done. That undercounts the value of good tracking, not the volume of it.

Every lead-gen funnel has spam, tire-kickers, and people who fill out the wrong form. If your only signal to OpenAI Ads is "a form was submitted," your campaign optimization is being trained on a mix of real prospects and noise, with no way to tell the difference. The advertiser optimizing purely on raw form-fill volume and the advertiser optimizing on qualified-lead volume can run the exact same campaign and get meaningfully different budget allocation from OpenAI's systems, because the second one is feeding a cleaner signal.

Tracking both events separately — lead_created at form submit, and a distinct qualified-lead signal (whether that's a second lead_created call with different data, or a custom event like qualified_lead) once your CRM confirms the lead is real — gives OpenAI Ads two data points instead of one: volume at the top of the funnel, and quality further down it. That second signal is what your bidding actually needs to optimize toward if your real goal is booked meetings or closed deals, not just filled-out forms.

Common gaps: multi-step forms and third-party tools

Two patterns break lead-gen tracking more often than anything else, and both fail silently rather than throwing an error.

Multi-step forms. If your form spans multiple pages or steps — contact info, then qualifying questions, then a final submit — firing lead_created too early (say, after step one) counts partial completions as full leads. Fire it only on the final, confirmed submission step, the same way you'd wait for backend confirmation on a single-step form. If you want visibility into drop-off between steps, that's a separate, clearly-named custom event (form_step_completed or similar) — don't overload lead_created to mean two different things depending on which step someone reached.

Third-party form and scheduling tools. Typeform, Calendly, and similar embedded tools don't run inside your own page's data layer, which means your pixel's oaiq("measure", ...) call has nothing to hook into at the moment of submission. Two workarounds cover most cases:

  1. Redirect to a thank-you page after submission (most of these tools support this) and fire lead_created from that page's own script. This is simple but only works if the tool supports post-submit redirects and the visitor doesn't close the tab first.
  2. Use the tool's webhook to notify your own backend the moment a submission or booking happens, then send the event via CAPI from your server instead of the browser. This is more reliable — it doesn't depend on the visitor's browser staying open — but it means your webhook handler needs to already have (or be able to look up) the __oppref value if you want the event attributed correctly, which usually means you were capturing and passing it through as a hidden field on the embedded form in the first place.

Whichever workaround you use, verify it actually fires before you trust the resulting data — third-party tool updates have a habit of quietly changing redirect or webhook behavior without much warning.

End-to-end testing checklist

Before you trust any of this in production, walk through it once, deliberately:

  • [ ] Submit a real test form and confirm lead_created fires only after backend confirmation, not on click
  • [ ] Confirm the __oppref cookie value is present in your form submission and gets stored with the lead record in your CRM
  • [ ] Manually mark a test lead "qualified" in your CRM and confirm the CAPI event actually sends, with the correct id, type, and timestamp_ms within the 7-day window
  • [ ] Confirm the stored oppref value makes it into the CAPI payload for that qualified-lead event
  • [ ] If you use a custom event for appointments or demos, confirm the custom_event_name matches exactly between pixel and CAPI calls
  • [ ] Test a multi-step form end to end and confirm lead_created fires once, at final submission — not once per step
  • [ ] If you use a third-party form or scheduling tool, test the redirect or webhook path specifically, since these are the most common silent-failure point
  • [ ] Check that amounts (if you're passing deal value) are formatted as minor-unit integers with a currency code, not decimals

If every box on that list checks out, you have a lead-gen tracking setup that reflects what your sales team actually knows about lead quality — not just how many forms got filled out.


Lead-gen tracking has more moving parts than ecommerce tracking specifically because the conversion you actually care about happens outside the browser, inside your CRM, often days later. Getting the CAPI side wired correctly — with __oppref captured at the point of submission and passed through when the lead is qualified — is the piece that makes the rest of this worth doing. If your team doesn't have the backend time to build and test that CRM-to-CAPI handoff, that's precisely the gap a done-for-you setup is built to close.

Want this done for you?

Fixed-scope setup, tested end-to-end and documented.

Explore the serviceTake the free audit