Form Tracking
The Apex snippet automatically detects and intercepts form submissions on your site — including forms built without a native <form> tag. When a visitor submits a form, the snippet sends a tracking event and can auto-identify the visitor by email, all without any code changes.
How It Works
Apex handles two kinds of forms:
Native <form> elements
On page load, the snippet queries all <form> elements and attaches a submit event listener to each one. When a form is submitted, three things happen in order:
- Hidden fields are injected into the form before it posts
- A
form_submitevent is sent to/api/eventsvia beacon - If an email is found, the visitor is automatically identified
Formless regions (React, Plasmic, etc.)
Many modern frameworks build forms using input fields and buttons without wrapping them in a <form> tag. The snippet detects these "formless regions" by scanning for groups of 2+ input fields near a submit-like button. When the button is clicked:
- A
form_submitevent is sent withsource: "formless" - If the region has an email box with an address in it, the visitor is automatically identified
An email box is one with type="email", autocomplete="email", or "email" in its name, id, placeholder, or aria-label. An address typed into any other box, like a search or a filter, is never used to identify anyone.
Someone else's email
Invite, referral, gift, and share forms hold another person's address. Apex still counts the send, but it does not identify the visitor from it. A box counts as someone else's when its name, id, placeholder, aria-label, or label mentions invite, friend, referral, gift, recipient, teammate, or colleague. The whole form counts as someone else's when its send button says Invite or Refer. In that case no email or name is read from it. A form with the visitor's own email box next to a referral box still identifies the visitor from their own box.
Info
Formless detection uses heuristics: it looks for containers with multiple input/textarea fields and a button marked type="submit" or labeled like Submit, Send, Sign Up, Subscribe, or Request. A single unlabeled button (an arrow, "Go") counts only when the region has an email box. Search areas (role="search", type="search", or a box labeled for searching) are skipped. If your form uses a non-standard button label, consider using apex.track() as described below.
Hidden Field Injection
The snippet adds these hidden <input> fields to the form at submit time:
| Field Name | Value | Purpose |
|---|---|---|
apex_visitor_id | The visitor's UUID | Links the form submission to the anonymous visitor profile |
apex_utm_source | UTM source value | Attribution data from the apex_attr cookie |
apex_utm_medium | UTM medium value | Attribution data from the apex_attr cookie |
apex_utm_campaign | UTM campaign value | Attribution data from the apex_attr cookie |
apex_experiment | Active experiment ID | Which experiment was running when the form was submitted |
apex_variant | Assigned variant | Which variant the visitor saw |
These fields are only added if a value exists. If the visitor has no UTM data, those fields are omitted. If a field with the same name already exists in the form, the snippet will not overwrite it.
Tip
The hidden fields are added to the DOM right before the form posts. Your form processor (HubSpot, Mailchimp, custom backend, etc.) receives them as regular form fields. Most CRM platforms store unknown fields in a catch-all or custom properties area.
Automatic Email Detection
The snippet scans submitted forms for email addresses. It checks fields in this order:
input[type="email"]input[name="email"],input[name="email_address"],input[name="user_email"],input[name="contact_email"]- Any text input whose value matches an email regex pattern
When an email is found, the snippet automatically calls window.apex.identify(email) to stitch the anonymous visitor ID to a known identity. This means your CRM lead and the Apex visitor profile get linked without any custom code.
Everything Else They Filled In
Along with the email, the snippet sends the other fields the visitor filled in, labelled the way your form labels them (the <label>, then aria-label, then the placeholder, then the field name). Apex keeps the last five submissions on the person. They show on the person's page under What they sent and in your team's new-person email.
The snippet never reads password fields, hidden fields, file uploads, or anything that looks like a card number, bank number, one-time code, token or captcha, and the server checks again. A form with a password box or card fields in it sends no typed fields at all, only the email and name. It keeps up to 25 fields per form. A company field also fills in the person's company when Apex doesn't have one yet. These fields travel only with the identity call, never on the form_submit event, so they don't reach your webhooks or ad platforms.
Product paths, and workspaces that turned off Pick up names and emails from forms, send none of this.
When Someone You Know Types Something New
Anyone can type your customer's email into a public form, so a form never replaces a name, company or job title Apex already has. It fills them in only when they're blank. What the visitor typed is still kept with the submission, so you can see it on their page.
Your own systems can update them: a signed sign-in, or a call made with your server key.
Sign-In and Sign-Up Forms
A sign-up is someone arriving, so Apex adds them to your people and your team gets the new-person email.
A sign-in is someone coming back. If Apex hasn't met them yet, which is common when you install Apex after you already have customers, they join your people quietly: no new-person email, and no lead sent to your ad platforms.
The snippet treats a form as a sign-in when it has one password box and nothing that makes an account. Any of these makes it a sign-up:
- a box marked
autocomplete="new-password" - a second password box, for confirming
- a name field
- a submit button that says Sign up, Create, Register, Join, Get started or Start free trial
Marking your password box autocomplete="current-password" on sign-in and autocomplete="new-password" on sign-up makes the call exact, and helps password managers too.
Leaving a Form Alone
Add data-apex-ignore to a form, or to anything around it, and the snippet leaves it alone: no hidden fields, no form_submit event, and nothing read from it.
<form action="/internal/search" data-apex-ignore>
...
</form>
This works for formless regions too. Put the attribute on the element that wraps the inputs and the button.
To stop reading every form on the site, turn off Pick up names and emails from forms on the Website snippet page. Submissions still count, with nothing typed in them.
To send the same thing from your own form handler, pass formFields as a list of { label, value }:
window.apex.identify(emailValue, {
name: nameValue,
formFields: [
{ label: "Company", value: companyValue },
{ label: "How can we help?", value: messageValue }
]
});
Identity Stitching
When identify() is called (either automatically from form detection or manually), the snippet sends a request to the identity API:
/api/identity/stitchLinks an anonymous visitor ID to a known email address
Payload includes the workspace key, visitor ID, email, optional metadata, and timestamp. The request is sent via navigator.sendBeacon to ensure delivery even as the page navigates away after form submission.
This creates a permanent link between the visitor's anonymous browsing history (pageviews, experiments, engagement) and their real identity. All past events retroactively associate with the identified user.
Manual Identification
If your form doesn't use a standard <form> element (e.g., a React controlled form or a JavaScript-driven flow), you can call identify manually:
// After your custom form submission logic
window.apex.identify(emailValue, {
name: nameValue,
company: companyValue,
source: "pricing_page_form"
});
The second argument is an optional metadata object — any key/value pairs you want stored alongside the identity.
The Form Submit Event
Every form submission also generates a form_submit event sent to /api/events:
{
"workspaceKey": "your-workspace-key",
"type": "form_submit",
"visitorId": "abc-123-def",
"url": "https://yoursite.com/contact",
"timestamp": "2025-07-15T10:30:00.000Z",
"data": {
"formAction": "https://yoursite.com/api/submit",
"email": "visitor@example.com",
"assignments": [
{ "experimentId": "exp-456", "variant": "variant_b" }
],
"attribution": {
"utm_source": "google",
"utm_medium": "cpc",
"gclid": "abc123"
}
}
}
This event is sent via beacon so it survives page navigation.
Working with Third-Party Forms
The snippet intercepts standard HTML <form> elements. Most third-party form builders (HubSpot, Typeform embeds, Gravity Forms) render as <form> tags and work automatically.
For forms built with modern frameworks (React, Vue, Plasmic, Webflow) that don't use <form> tags, the formless region detection handles most cases automatically.
Warning
Some embedded forms load inside iframes (e.g., Typeform hosted forms, some Calendly embeds). The snippet cannot intercept forms inside cross-origin iframes. For these, use window.apex.identify() in your own post-submission callback if the platform provides one.
Manual Form Tracking
If automatic detection doesn't catch your form (uncommon button labels, heavily abstracted UI frameworks, or custom submit flows), you can track submissions manually:
// Call this in your form's submit handler
window.apex.track("form_submit", {
email: emailField.value,
formName: "contact-form"
});
// If you also want to identify the visitor
window.apex.identify(emailField.value, {
name: nameField.value,
source: "contact_form"
});
This works alongside automatic detection — Apex deduplicates events from the same visitor within a short window.
What Gets Tracked
| Form Type | Detection | Event Source | Hidden Fields |
|---|---|---|---|
Native <form> | Automatic | form_submit | Yes (attribution injected) |
| Formless region | Automatic | form_submit (source: "formless") | No (uses fetch/JS) |
Manual apex.track() | Your code | form_submit | No (you control the payload) |
| Cross-origin iframe | Not supported | Use platform callback | N/A |
Next Steps
- Custom Events — send custom events via
window.apex.track() - Experiments — how experiment data flows into form submissions
- Installation — verify the snippet is running correctly