Skip to main content

Form JSON

Introduction​

A form as data instead of markup: its pages and fields, their choices, the rules the server will apply, the conditions, how the form is meant to look, and what a script needs to submit it.

Two ways to get it​

From a template, with output="json". Put it in a template whose type is JSON and you have an endpoint with ExpressionEngine's own routing and caching. Field defaults can use EE variables here, because the output is parsed after the tag runs.

From the JSON API: GET forms/{form}. It is the same document, wrapped in {"data": …}. Defaults that use EE variables are left out there, because nothing parses them.

A form as JSON

Example
templates/default_site/api.group/contact.html
{exp:formidable:form form_handle="contact_us" output="json"}
Rendered output (excerpt)Captured from ExpressionEngine 7.5.26 on 2026-10-11. Signed values and the CSRF token shown as “…”, host replaced with example.com, blank lines collapsed.
{
"schema": 1,
"form": {
"id": 1,
"handle": "contact_us",
"name": "Contact Us",
"open": true,
"closed_message": "",
"members_only": false,
"templating": {
"theme": "tailwind",
"columns": 12,
"instructions": "top",
"attributes": {}
},
"success_message": "Thank you — your submission has been received.",
"pages": [
…
"fields": [
{
"handle": "first_name",
"label": "First Name",
"fieldtype": "text",
"stores_value": true,
"multiple": false,
"hidden": false,
"required": false,
"instructions": "We prefer your legal name",
"default": "",
"page": 1,
"section": "s_1_1",
"columns": 4,
"templating": {
"columns": 12,
"attributes": {
"instructions": {
"class": "text-sm text-grey-300"
}
}
},
"attributes": {
"type": "text"
},
"options": null,
"rules": [
{
"name": "whenPresent",
"value": ""
}
],
"conditions": {
"mode": "show",
"show": [],
"require": []
}
},
{
"handle": "last_name",
…
"submit": {
"url": "https://example.com/index.php?ACT=104&endpoint=forms/contact_us/submissions",
"action_url": "https://example.com/index.php?ACT=101",
"method": "POST",
"meta": "…",
"csrf_token": "…",
"honeypot": {
"name": "formidable_hp",
"js_name": ""
},
"multipart": false
}
}

What is in it​

Key
schemaThe document version. It changes only when a published key changes meaning or is removed. New keys may appear at any time.
formId, handle, name, open, closed_message, members_only, success_message, templating, and pages with their sections, navigation labels and conditions.
fieldsOne entry per field in builder order: handle, label, fieldtype, whether it stores a value or several, hidden, required, instructions, default, page, section, width, templating, attributes, options, rules and conditions.
submitWhere and how to submit: the API's submissions URL, the submit action URL, the signed meta, the session's csrf_token, the honeypot input names, and whether the form needs multipart/form-data.
  • options are resolved to {value, label} pairs, whatever their source, and are null for a field without choices. A Recipients field publishes its values and labels only, never its addresses.
  • rules are the rules the server will apply, as {name, value} pairs.
  • attributes are the field's authored attributes, without id, name and value, which the client decides.
  • templating, on the form and on each field, is how the form is meant to look, resolved exactly as the form tag resolves it: theme, grid width, instruction position, and the attributes for each part. A field's templating.columns is its width in the template's grid, with {field_columns} already substituted.
  • A field whose choices depend on another field carries depends_on, the other field's handle, and each of its options a parent list of that field's values it belongs to. Offer only the options whose parent includes the other field's answer, plus those without parent. The server refuses any other pairing.
  • A Computed field carries computed, its definition with fields named by handle, so a client can show the result as the visitor types. Whatever is posted for it is ignored: the server works it out again.
  • For a guest on a members-only form, open is false and closed_message is filled in.

Submitting it​

Post to submit.url as described in JSON API › Submitting. Or post to submit.action_url, the same action a rendered form posts to, with an X-Requested-With: XMLHttpRequest header so the answer comes back as JSON rather than a redirect:

  • every field under its handle;
  • _formidable_meta set to submit.meta, and csrf_token set to submit.csrf_token;
  • submit.honeypot.name present and empty. When submit.honeypot.js_name is not empty, send that input non-empty;
  • multipart/form-data when submit.multipart is true.

The answer is {"success": true, "submission_id": …} or {"success": false, "errors": {"handle": "message", …}}.

The CSRF token belongs to the browser session that fetched the document, so the submission has to come from the same session, in practice the same site. A front end on another domain can read the document, but should submit through the JSON API with a key.

Never published​

Workflows, recipients and their addresses, connections and their credentials, email templates, encryption settings, and anything from the Registry beyond a fieldtype's handle.