Submissions
This page covers what happens after a visitor clicks Submit — where the data goes, how leads are matched, what metadata is captured, and how to query submissions later.
What Happens On Submit
- Validation runs — every visible required field, every validation rule. If anything fails, submit is blocked.
- The browser POSTs to
/api/forms/public/{slug}/submitwith the full responses payload. - The backend creates or matches a lead — by
email, thenphone, thennameheuristics. Returning visitors with cachedvotel_lead_idare matched first. - A
form_submissionsrow is written with the responses, lead reference, form metadata, and visitor metadata. - Workflow triggers fire — any workflow listening for "Form Submitted" on this form runs.
- The visitor sees the success path — Thank You screen, ending screen, or redirect URL per Form Settings.
What's Recorded On Each Submission
Every submission row in the form_submissions table contains:
| Column | Contents |
|---|---|
id | Submission UUID |
tenant_id | Your account |
form_id | Which form |
lead_id | The CRM lead this submission attaches to |
response_data | JSONB — all the field values keyed by field UUID |
metadata | JSONB — any extra metadata per submission |
submitted_at | UTC timestamp |
ip_address | Visitor IP |
user_agent | Browser user-agent string |
lead_name / lead_email / lead_phone | Denormalized for fast querying |
lead_visitor_id | Visitor ID if click tracking was active |
form_name | Form name at time of submission (history-stable even if form is renamed) |
form_type | premium / conversational / popup |
form_version | Form version at submission time |
submitted_from_page | The visitor's URL when they hit submit (typically /f/<slug>) |
referrer_url | The HTTP Referer header |
is_embedded | Whether the form was loaded inside an iframe (embed mode) |
is_final | True when this is a complete submission (vs a partial save) |
How response_data Is Keyed
Each form field has a UUID. The response_data JSONB uses these UUIDs as keys.
{
"ea86deed-4836-4dc4-b608-32fe90da09d3": "Quiz Taker",
"fb4ce17b-3d2a-4521-a0cf-952086d799a9": "option_1",
"8788e1c2-9840-406f-a67c-d170e0d2e28e": "user@example.com",
"__hidden__utm_source": "facebook",
"__hidden__ref": "alice"
}
Hidden fields use the prefix __hidden__<key> (see Hidden Fields & Variables).
For multi-value fields (Multi Select, Checkbox), the value is comma-separated ("red,green,blue").
For composite fields (Address, Contact Info, Full Name), the value is a JSON-encoded string of the sub-fields:
{
"address-uuid": "{\"line1\":\"123 Main St\",\"city\":\"NYC\",\"state\":\"NY\",\"zip\":\"10001\",\"country\":\"US\"}"
}
Lead Matching
When a submission arrives, the backend tries to match it to an existing CRM lead — in this priority order:
- Cached lead ID — if
localStorage.votel_lead_idis set on the visitor's browser (returning visitor) - Email match — if a lead with the submitted email exists
- Phone match — normalized E.164 form
- Name match — first + last name, only as a last resort and only with other corroborating data
If no match is found, a new lead is created. Either way, the lead's CRM record is updated with any field whose Save to Contact Field mapping was set.
Pre-population from Cached Leads
When a returning visitor lands on your form's URL, the form fetches the cached lead's data and pre-fills any fields with a contact-field mapping. So a returning visitor who already gave you their name + email will see those fields filled in on subsequent forms.
The cache is stored in localStorage.votel_lead_id and is per-browser, per-device. Returning visitors on a different device start fresh.
Viewing Submissions
There are several places to see what's been submitted:
CRM Contacts List
Navigate to Contacts. Each lead created/updated by a submission appears here. Open a contact to see all their submissions across forms in their activity timeline.
Per-Form Submissions Report
From the form's edit page, navigate to its Submissions tab. This shows every submission for that form in a sortable, filterable table.
Dynamic Query API
For programmatic access or custom dashboards, the /api/form-submission-reporting/query endpoint lets you build queries with column selection, filters, sort, and pagination.
curl -X POST 'https://yourdomain.com/api/form-submission-reporting/query' \
-H 'Authorization: Bearer <your-token>' \
-H 'Content-Type: application/json' \
-d '{
"form_id": "9b75108e-2fdd-4f8d-88c7-2ed7cbdb709d",
"limit": 50,
"filters": [],
"columns": ["lead_email", "submitted_at", "response_data"]
}'
See the API Reference for the full schema.
Workflows on Submissions
Every submission can trigger a workflow. In the Workflows section, set up a workflow with the Form Submitted trigger filtered to your form. The workflow gets each submission's response_data and lead context as variables.
Common workflow patterns:
- Send a confirmation email — fire an email node with the submitted email
- Notify your team — Slack / SMS notification node with the lead's details
- Tag the lead — apply tags based on which form/path the visitor took
- Score the lead — bump lead score based on form completion or specific answers
- Add to a campaign — enroll the lead in an outbound campaign
Partial Saves (Save & Resume)
When Save & Resume is enabled on a conversational form, partial submissions write rows with is_final: false as the visitor progresses. When they complete the form, the row's is_final flips to true.
To filter your reports / workflows to only completed submissions, add is_final = true to your query.
File Upload Storage
When a form has a File Upload field and the visitor uploads a file, the file is stored in your tenant's attachment storage and the submission's response_data for that field holds the URL:
{
"file-upload-uuid": "/api/upload/<tenant>/attachment/<uuid>.pdf"
}
The URL is publicly fetchable (with auth) for downloading the file. See Field Properties → File Upload for size/type config.
Payment Submissions
When a form has a Payment field, the submission row includes the Stripe charge / customer / subscription IDs in metadata:
{
"stripe_customer_id": "cus_...",
"stripe_charge_id": "ch_...",
"stripe_subscription_id": "sub_...",
"amount": 2900,
"currency": "USD"
}
See Payment Fields for the modes.
Next Steps
- Integrations — Workflows, payments, contact field mapping
- Hidden Fields & Variables — Capturing UTM data per submission
- Workflows Overview — Trigger workflows from form submissions