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",
"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) |
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.
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": "0.1.0"
}
pluginVersion matches Freeform. minimumClientVersion is the oldest npm client line the plugin expects to work with.
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