Skip to main content

REST API
LitePro
New 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):

config/freeform.php
<?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'],
],
],
],
];
SettingPurpose
headless.enabledMaster switch. When false, headless routes return 404.
headless.allowedOriginsGlobal CORS allow-list (supports * wildcards like https://*.example.com).
forms.{handle}.exposeManifestAllow GET manifest for that form.
forms.{handle}.allowSubmitAllow 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": "…" }]
}
}
IntentUse
submitFinal submit (or single-page forms)
next / backMulti-page navigation
validateValidate without completing
saveDraftSave 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

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).

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:

  1. List that origin in headless.allowedOrigins (or per-form / per-profile overrides)
  2. 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