REST APILiteProNew in 5.16+
The headless REST API ships with the Freeform plugin. Official npm clients are published under the Solspace npm org.
Most customers should use @solspace/freeform-react or @solspace/freeform-vue and never call these endpoints by hand. This page is the contract reference when you build a custom client or debug requests.
Enable the API
Configure this in your Craft CMS project (where Freeform is installed), not in your frontend repo.
Edit or create config/freeform.php alongside Craft’s other config files (e.g. your-craft-project/config/freeform.php). Merge a headless section (keep any existing Freeform settings):
<?php
return [
'headless' => [
'enabled' => true,
'allowedOrigins' => [
'http://localhost:3000',
'https://app.example.com',
],
'forms' => [
'contact' => [
'exposeManifest' => true,
'allowSubmit' => true,
// Optional per-form CORS override
// 'allowedOrigins' => ['https://app.example.com'],
],
],
],
];
| Setting | Purpose |
|---|---|
headless.enabled | Master switch. When false, headless routes return 404. |
headless.allowedOrigins | Global CORS allow-list (supports * wildcards like https://*.example.com). |
forms.{handle}.exposeManifest | Allow GET manifest for that form. |
forms.{handle}.allowSubmit | Allow POST submit for that form. |
A config example ships with Freeform at packages/plugin/config/headless.example.php.
Endpoints
URLs below are relative to your Craft site. Official clients resolve them with your baseUrl.
Form manifest
GET /freeform/api/forms/{handle}/manifest
Accept: application/json
Returns layout, fields, conditionals, security metadata, and submit endpoint URLs.
Submit
POST /freeform/api/forms/{handle}/submit
Content-Type: application/json
# or multipart/form-data
JSON body shape (simplified):
{
"values": {
"email": "jane@example.com",
"message": "Hello"
},
"intent": "submit",
"context": {
"draftToken": null,
"draftKey": null,
"stateToken": null,
"sourceUrl": "https://app.example.com/contact"
},
"meta": {
"honeypot": { "name": "freeform_form_handle", "value": "" },
"javascriptTest": { "name": "freeform_form_handle", "value": "" },
"captchas": [{ "name": "g-recaptcha-response", "value": "…" }]
}
}
| Intent | Use |
|---|---|
submit | Final submit (or single-page forms) |
next / back | Multi-page navigation |
validate | Validate without completing |
saveDraft | Save progress (token + key returned in draft) |
Scoped Duplicate CheckingProNew in 5.17+
The form's Duplicate Check and Scope by Field settings also apply to headless submissions. Include the scope field in values, for example "values": { "productId": "123", "email": "jane@example.com" }. Keep submission storage enabled; a missing scope value or a duplicate is returned as a validation error.
For cookie-based modes, load the form before the first submission and retain the visitor cookie across requests. Cross-origin clients must send credentials and use matching CORS and cookie settings. Without the same cookie, requests cannot be recognized as the same guest browser. See Scoped Duplicate Checking for configuration and limitations.
Limit SubmissionsNew in 5.17+
An enabled builder submission cap also applies to new headless submissions in Lite and Pro. Freeform rejects submissions once the total or current calendar-period limit is reached, using the normal submission error response. Stored submissions are required; spam and deleted submissions are excluded, and edits to existing submissions do not consume another slot.
Manifest requests still return form configuration. Handle the full-form display in your frontend; Twig's automatic limit-message rendering does not run for API requests. See Limit Submissions for counting rules and periods.
Form SchedulingNew in 5.17+
Forms using Schedule Form reject submissions before their start or after their end time, including submissions from a form loaded while it was still open. The response uses the normal validation error structure. Scheduling is available in Lite and Pro.
Manifest requests still return form configuration outside the schedule; they do not automatically hide your frontend or run Twig's message rendering. Handle the availability display in your custom frontend. See Form Scheduling for setup, messages, and timezone behavior.
Submit context allow-list
Client-supplied context is restricted to:
| Key | Purpose |
|---|---|
draftToken / draftKey | Resume or update a saved draft (both required together) |
stateToken | Multi-page session token from a prior submit response |
sourceUrl | Browser page URL (e.g. Mollie return / submission source tracking) |
token | Signed context token when a profile sets requiresSignedToken |
Anything else (for example disable, elementId, submissionId) is stripped before Freeform applies form context. Do not rely on passing internal form settings through submit context.
On draft_saved, the response includes:
{
"status": "draft_saved",
"draft": { "token": "…", "key": "…", "resumeUrl": null },
"state": { "values": { "…": "…" }, "pageIndex": 0 }
}
Pass context.draftToken and context.draftKey on later submits to update or resume the same draft.
In React, enable Save in the form builder, then follow the simple URL recipe in React JS → Save & Continue Later (draftToken / draftKey props + session-token / key query params).
Multipart submits use a _freeform JSON field for the payload plus file inputs, matching what @solspace/freeform-core sends automatically. Standalone file fields and table file cells both use this path (table cells use nested keys under the table handle).
CSRF token
GET /freeform/tokens
Accept: application/json
When Craft CSRF is enabled, include the token on submit:
- JSON: header
X-CSRF-Token: {value} - Multipart: form field named with the Craft CSRF param
Official clients fetch and attach this for you when credentials: "include" is used (default).
File Drag & Drop
POST /freeform/api/forms/{handle}/files/{fieldHandle}
POST /freeform/api/forms/{handle}/files/{fieldHandle}/delete
Used by the file-dnd extension. Requires CSRF and the upload token header provided by the client.
Payment Fields
- Stripe
- Square
- PayPal
- Mollie
POST /freeform/api/forms/{handle}/payments/stripe/checkpoint
Content-Type: application/json
Used by the payment.stripe extension before Stripe confirmation. Validates the form, verifies the PaymentIntent belongs to this form’s Stripe field, and stores an encrypted checkpoint so Freeform’s existing Stripe callback / webhook can create the submission after payment (including 3DS redirects).
Official clients call this for you. Prefer @solspace/freeform-react or @solspace/freeform-vue rather than calling the endpoint by hand.
Related Stripe routes (also used by classic Freeform) remain under /freeform/payments/stripe/… (PaymentIntent create/update, callback, webhook).
POST /freeform/payments/square/payments
Content-Type: application/json
FF-SQUARE-INTEGRATION: {opaque-integration-hash}
Used by the payment.square extension after the Web Payments SDK tokenizes the card. Body includes the Square nonce, currency, and current form values so Freeform can resolve fixed or dynamic amounts server-side with the Square access token (never exposed in the manifest).
Official clients call this for you. Prefer @solspace/freeform-react or @solspace/freeform-vue rather than calling the endpoint by hand. This is the same Square charge route used by classic Freeform templates.
POST /freeform/payments/paypal/orders
POST /freeform/payments/paypal/orders/{orderId}/capture
Content-Type: application/json
FF-PAYPAL-INTEGRATION: {opaque-integration-hash}
Used by the payment.paypal extension with PayPal Buttons. Create locks the amount from current form values; capture finalizes the PayPal order. Client secrets stay server-side.
Official clients call these for you. Prefer @solspace/freeform-react or @solspace/freeform-vue rather than calling the endpoints by hand. These are the same PayPal routes used by classic Freeform templates.
POST /freeform/payments/mollie/create?integration={opaque-integration-hash}
Content-Type: application/json
GET /freeform/payments/mollie/callback
POST /freeform/payments/mollie/webhook
Used by the payment.mollie extension. Create returns a paymentId and Mollie checkoutUrl. The create body may include returnOrigin (your SPA origin, e.g. http://localhost:3000); when that origin is listed in headless.allowedOrigins, Mollie’s browser return uses {returnOrigin}/freeform/payments/mollie/callback so the Vite/Next proxy keeps session cookies on the SPA host. Webhook stays on the public Craft URL.
Official clients call create for you. Prefer @solspace/freeform-react or @solspace/freeform-vue rather than calling the endpoints by hand. These are the same Mollie routes used by classic Freeform templates.
Named profiles (advanced)
GET /freeform/api/manifests/{profile}/manifest?properties[eventId]=123
POST /freeform/api/manifests/{profile}/submit
Profiles are explicit config entries under headless.profiles — never inferred from integrations. Use them for contextual manifests that need auth, signed tokens, or typed query properties.
Submit the same allow-listed properties on POST that you used for the manifest GET (JSON body properties, or nested under multipart _freeform). Required properties missing on submit are rejected.
When a context provider is configured, the manifesto may include:
context.defaultValues— seeded field valuescontext.hiddenFields/context.lockedFields— handles the client should hide or treat as read-only (official packages honor these; server also re-applies provider defaults for those handles on submit)
If the profile sets requiresSignedToken: true, include a non-empty token via:
- header
X-Freeform-Context: …, or context.tokenin the submit body (or nested under multipart_freeform), or- query
contextTokenon the manifest request
Cryptographic verification of that token is planned; until then Freeform requires the token to be present so the flag is not a silent no-op.
Field ConfigurationImproved in 5.17+
The manifest includes field-specific settings under fields.{handle}.frontend.config. The official React and Vue renderers use these settings to provide the corresponding controls.
| Field or feature | Configuration keys | Purpose |
|---|---|---|
| Range Slider | min, max, step | Slider bounds and increment. Its starting value is provided separately as field.defaultValue. |
| Summary | fields, hideEmpty, emptyValue, checkedLabel, uncheckedLabel, filesLabel | Source field handles, empty-answer behavior, and translated display labels. |
| Searchable Dropdown and Multiple Select | searchable | An object containing enabled, placeholder, noResults, toggleLabel, resultsLabel, and removeLabel. |
| Text and Textarea character counts | showCharacterCount, characterCountMessages | Enables the counter and supplies translated count and limit messages. The character limit is provided as field.validation.maxLength. |
| Auto-growing Textarea | autoGrow, autoGrowMaxHeight | Enables automatic resizing and optionally limits its height in pixels. |
| Email suggestions | suggestEmailCorrections, emailSuggestionLabels | Enables suggestions and supplies translated message and action labels. |
| Password toggle | showPasswordToggle, passwordToggleLabels | Enables the visibility toggle and supplies translated show and hide labels. |
| International Phone | international, defaultCountry, allowedCountries, examples, labels | Enables international input and supplies the initial country, an array of allowed country codes, example numbers, and translated country-selector labels. |
| File Upload and File Upload Drag & Drop | uploadRequirements | Translated requirements text, included when Show Upload Requirements is enabled. |
Optional feature keys may be omitted when the feature is disabled or its setting is unset. Custom renderers must implement the behavior represented by these settings.
Browser autofill is provided separately as fields.{handle}.attributes.input.autocomplete. A custom autocomplete input attribute takes precedence over the Browser Autofill setting.
Headless forms use their own themes and renderers. Twig formatting-template styling and JavaScript Plugin overrides do not automatically apply.
The form builder's processing overlay and focus-first-error settings are not currently included in the headless manifest. Implement these behaviors in your front end when needed.
CORS and credentials
Browser apps that call Craft on another origin must:
- List that origin in
headless.allowedOrigins(or per-form / per-profile overrides) - Send requests with credentials (
credentials: "include") so CSRF cookies work
Recommended: proxy /freeform/* through your frontend (Next.js rewrites, Vite proxy, etc.) so the browser stays same-origin.
Set explicit origins. A misconfigured fallback to GraphQL’s * is unsafe for credentialed requests.
Security metadata
The manifest includes security for honeypot, javascript test, and captcha site keys. Official clients fill meta automatically. For public forms, enable a captcha integration in Freeform — honeypot / JS test alone are soft signals for direct API callers.
Compatibility headers
Manifests include:
{
"schemaVersion": "1.0",
"pluginVersion": "5.15.19",
"minimumClientVersion": "1.0.0"
}
pluginVersionmatches Freeform.minimumClientVersionidentifies the oldest compatible npm client version. Use that version or newer.
Next steps
- Getting Started — install packages
- React JS — React
<Freeform />anduseFreeform() - Vue.js — Vue
<Freeform />anduseFreeform() - Next.js — App Router + proxy
- Nuxt — Nuxt + client-only Freeform + proxy
- React JS → Stripe payments — Payment Element in headless React
- React JS → Square payments — Web Payments SDK in headless React
- React JS → PayPal payments — PayPal Buttons in headless React
- React JS → Mollie payments — hosted checkout redirect in headless React
- Vue.js → Payments — same payment extensions in Vue
- GraphQL — headless manifest/submit via Craft GraphQL