Skip to main content

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

  1. Validation runs — every visible required field, every validation rule. If anything fails, submit is blocked.
  2. The browser POSTs to /api/forms/public/{slug}/submit with the full responses payload.
  3. The backend creates or matches a lead — by email, then phone, then name heuristics. Returning visitors with cached votel_lead_id are matched first.
  4. A form_submissions row is written with the responses, lead reference, form metadata, and visitor metadata.
  5. Workflow triggers fire — any workflow listening for "Form Submitted" on this form runs.
  6. 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:

ColumnContents
idSubmission UUID
tenant_idYour account
form_idWhich form
lead_idThe CRM lead this submission attaches to
response_dataJSONB — all the field values keyed by field UUID
metadataJSONB — any extra metadata per submission
submitted_atUTC timestamp
ip_addressVisitor IP
user_agentBrowser user-agent string
lead_name / lead_email / lead_phoneDenormalized for fast querying
lead_visitor_idVisitor ID if click tracking was active
form_nameForm name at time of submission (history-stable even if form is renamed)
form_typepremium / conversational / popup
form_versionForm version at submission time
submitted_from_pageThe visitor's URL when they hit submit (typically /f/<slug>)
referrer_urlThe HTTP Referer header
is_embeddedWhether the form was loaded inside an iframe (embed mode)
is_finalTrue 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:

  1. Cached lead ID — if localStorage.votel_lead_id is set on the visitor's browser (returning visitor)
  2. Email match — if a lead with the submitted email exists
  3. Phone match — normalized E.164 form
  4. 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