> For the complete documentation index, see [llms.txt](https://kb.craftformswp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kb.craftformswp.com/documentation/for-developers/headless-backend-mode.md).

# Headless Backend Mode

**Location:** CraftForms → Settings → *Headless mode* tab. **Availability:** CraftForms PRO (the same tier as embedding / external submissions).

<figure><img src="https://97821998-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FM2yV1foUgoGHBC66VBDw%2Fuploads%2F8oA6JLLz79hXY8ixsFxg%2FSCR-20260720-ofai.png?alt=media&amp;token=e574f6a3-dfc7-448b-84c5-f282ac64dc01" alt=""><figcaption></figcaption></figure>

Headless backend mode locks a WordPress install down to the minimum surface a static site needs to talk to it. Turn it on when this WordPress site exists **only** to serve and receive CraftForms forms for an external front-end (Astro, Hugo, Next, plain HTML, etc.) and has no public pages of its own.

It is a single switch. Everything it changes is PHP-layer, reversible, and can be overridden instantly with a `wp-config.php` constant if anything goes wrong.

***

### What it does <a href="#what-it-does" id="what-it-does"></a>

When enabled, for **anonymous** (logged-out) visitors only:

* **Blacks out the public front-end.** Any page, post, or archive request returns a bare `403` instead of rendering a theme. Logged-in users (you, editors) browse normally. `robots.txt` and the favicon are still served.
* **Restricts the REST API to the CraftForms namespace.** Anonymous callers can reach only `/craftforms/v1/*` — the embed render and form submission endpoints. Everything else (`wp/v2` content, user lists, the REST index) is closed. **Logged-in requests, including the block editor you build forms in, are never touched.**
* **Disables XML-RPC** entirely (a classic brute-force and pingback-DDoS vector), including the `X-Pingback` header.
* **Removes discovery leaks** — the generator version, RSD, and WLW links in the page `<head>`.
* **Disables the file editor** (`DISALLOW_FILE_EDIT`), so the theme/plugin code editor in wp-admin is turned off.

> Because it blacks out the *entire* public front-end, this is a switch for a **dedicated** form backend — not for a site that also serves public pages.

***

### What it does NOT do <a href="#what-it-does-not-do" id="what-it-does-not-do"></a>

\
These need server-level configuration (nginx/Apache, host control panel) and **cannot** be done from a plugin:

* It does not hide or rename the login page.
* It does not block direct requests to `xmlrpc.php`, or PHP execution inside `/uploads`, at the server level. (XML-RPC *methods* are disabled, but the file itself still responds.)
* It does not add a login gate, two-factor auth, or a Web Application Firewall.

***

### How to harden further <a href="#how-to-harden-further" id="how-to-harden-further"></a>

For the server-level steps above, follow the companion guide: [Use WordPress as a Locked-Down Form Backend for Static Sites](https://craftformswp.com/use-wordpress-as-a-locked-down-form-backend-for-static-sites/). It covers blocking `xmlrpc.php`, gating the login page, disabling PHP in `/uploads`, and adding security headers at the edge — and links to a community gist with copy-paste server rules.

***

### Lockout recovery (the `CRAFTFORMS_HEADLESS_DISABLE` constant) <a href="#lockout-recovery-the-craftforms_headless_disable-constant" id="lockout-recovery-the-craftforms_headless_disable-constant"></a>

If a misconfiguration ever leaves you unable to reach `wp-admin`, add this line to `wp-config.php` (anywhere above the `/* That's all, stop editing! */` comment):

{% code collapsedlinecount="10" %}

```php
define( 'CRAFTFORMS_HEADLESS_DISABLE', true );
```

{% endcode %}

**How it works:** the constant is a hard override. While it is present, every protection above is switched off — even if the toggle in the database is still on. The stored setting is preserved, so removing the line resumes hardening exactly as it was; no database access is needed to recover.

Because logged-in users are never blocked by this mode, a genuine admin lockout is unlikely in the first place. The constant is there as a guaranteed escape hatch, and the Settings → Headless mode tab shows an *"Currently overridden"* notice whenever it is active.

***

### How it works internally <a href="#how-it-works-internally" id="how-it-works-internally"></a>

* **State** lives in the `craftforms_headless_mode` option (`{ "enabled": true|false }`), written by `POST craftforms/v1/settings/headless`.
* **Effective state** = stored `enabled` **and** the `CRAFTFORMS_HEADLESS_DISABLE` constant is not truthy (`CraftForms\Headless_Hardening::is_enabled()`).
* Hooks register on `plugins_loaded` only when effective: `template_redirect` (front-end blackout), `rest_pre_dispatch` (REST restriction), plus the `xmlrpc_enabled`, `wp_headers`, and `the_generator` filters and the `DISALLOW_FILE_EDIT` define.
* `template_redirect` never fires for the REST API, WP-Cron, admin-ajax, or `wp-login.php`, so the front-end blackout cannot break form submissions, the login page, or scheduled tasks.

#### A note on permalinks <a href="#a-note-on-permalinks" id="a-note-on-permalinks"></a>

On installs using **plain permalinks**, the pretty `/wp-json/…` URLs are not rewrite-recognized REST routes; such a request falls through to the normal front-end and is caught by the blackout (a plaintext `403`) rather than the REST filter (a JSON `401`). The canonical `?rest_route=…` form — which `embed.js` and the submission client always use — is dispatched as real REST and gets the proper namespace restriction. Both surfaces end up closed; only the error shape differs.
