# Headless WordPress forms with Contact Form 7, tested

We sent a headless front end's form to Contact Form 7 on WordPress 7.1 and logged each answer, from the hidden fields it needs to its HTTP 200 failures.

Author: Formgong
Published: 2026-11-03
Language: en
Canonical: https://formgong.com/en/blog/headless-wordpress-forms/

Either send the form to Contact Form 7's REST route with three hidden fields it expects, or post it to a form backend and leave WordPress out. In our test on 3 November 2026 with WordPress 7.1.4 and Contact Form 7 6.2.4.1, 4 of the 8 requests we sent were refused outright, 2 failed while answering HTTP 200, and 2 sent mail. WordPress stored none of them.

## Why does my form stop working on headless WordPress?

On a normal WordPress site, a form plugin draws the form, adds its hidden fields and its script, and WordPress sends the email. Once you go headless, your Next.js, Astro or Nuxt site draws the pages from WordPress content, and the shortcode `[contact-form-7 id="…"]` arrives as plain text. Nothing on the new front end knows how to turn it into a working form.

So you rebuild the form in your front end and send it somewhere. We set up WordPress 7.1.4 in Docker with Contact Form 7 6.2.4.1 and its default form, then sent that form from a page on another origin, the way a separate front end would, in Chromium. We logged every answer, first with no mail server, then with a stand-in that records each email and reports it sent.

## Can Contact Form 7 work with headless WordPress?

Yes, through its REST route, `/wp-json/contact-form-7/v1/contact-forms/<id>/feedback`. The table shows what each kind of request got back.

A front end on another origin posting to Contact Form 7 6.2.4.1 on WordPress 7.1.4, in Chromium, 3 November 2026. Two runs, same answers.
What we sentWhat WordPress answered
Read the form definition, signed out401, `wpcf7_forbidden`
JSON body415, `wpcf7_unsupported_media_type`
FormData, fields only400, `wpcf7_unit_tag_not_found`
Shortcode hash in the URL instead of the ID404, `rest_no_route`
Email field named `email` instead of `your-email`HTTP 200, status `validation_failed` on `your-email`
Full request, no mail server set upHTTP 200, status `mail_failed`
Full request, mail workingHTTP 200, `mail_sent`
Same request from a script with no pageHTTP 200, `mail_sent`
Three rules come out of it. The body has to be `FormData`; JSON gets a 415. The URL needs the numeric post ID, which you find in the address bar when you edit the form, not the short hash in the shortcode, which gets a 404. And the request needs a unit tag, the hidden field Contact Form 7 normally adds when it draws the form. With `_wpcf7`, `_wpcf7_unit_tag` set to `wpcf7-f5-o1` and `_wpcf7_container_post` set to 0, our request went through. Forminit's guide to Contact Form 7 on headless WordPress (2 August 2026) lists five hidden fields; three were enough here, and none of them is a documented interface, so check again after a plugin update.

We expected trouble with CORS, since the front end sat on another origin. There was none, and WordPress let the page read all 8 answers. A security plugin or a firewall rule can change that, and our guide to the [Contact Form 7 “error trying to send your message”](/en/blog/contact-form-7-error-trying-to-send-your-message/) shows what a blocked REST API looks like.

```javascript
// Contact Form 7 6.2.4.1 from a front end on another origin.
const WP = "https://cms.example.com";
const FORM_ID = 5; // the numeric post ID, not the hash in the shortcode

export async function sendToCf7(form) {
  const data = new FormData(form); // field names must match the CF7 tags: your-name, your-email…
  data.append("_wpcf7", String(FORM_ID));
  data.append("_wpcf7_unit_tag", `wpcf7-f${FORM_ID}-o1`);
  data.append("_wpcf7_container_post", "0");
  const response = await fetch(`${WP}/wp-json/contact-form-7/v1/contact-forms/${FORM_ID}/feedback`, {
    method: "POST",
    body: data, // FormData, not JSON: JSON gets a 415
  });
  const result = await response.json();
  // A failed send still answers HTTP 200, so check CF7's own status.
  if (result.status !== "mail_sent") throw new Error(result.message);
  return result;
}
```

## Why does Contact Form 7 return 200 when the form failed?

Because the HTTP status only tells you the REST route ran. The result is in the `status` field of the JSON. When we renamed `your-email` to `email`, the answer was HTTP 200 with `validation_failed`. When the server had no way to send mail, the answer was HTTP 200 with `mail_failed` and the message “There was an error trying to send your message.” A front end that checks only `response.ok` shows a thank-you for both, and the message is gone.

So check `result.status === "mail_sent"`, as the code above does, and show `result.message` otherwise. The `mail_failed` case is easy to hit on a fresh server. The WordPress Docker image we used had no way to send mail at all, and many hosts block PHP mail. Our guide to [PHP mail() not sending](/en/blog/php-mail-not-sending-email/) covers that side.

## Does Contact Form 7 save submissions on headless WordPress?

No. WordPress had 6 rows in its posts table before the test and 6 after, with 2 messages sent. Contact Form 7 emails the message and keeps nothing, headless or not; a plugin such as Flamingo adds storage. If the email bounces or lands in spam, there is no copy.

The route is also open to anyone. The same request sent from a Node script, with no page and no browser, got `mail_sent`. On a normal WordPress site the form at least lives on a page a bot has to load. A headless setup publishes the route and the hidden fields in your front-end code, so add a CAPTCHA plugin or rate limiting before you go live. [Contact Form 7 spam](/en/blog/contact-form-7-spam/) compares the options.

## Should a headless WordPress form go through WordPress at all?

Only if you need something WordPress does with it, such as a CF7 hook that writes to your database or a CRM plugin you already pay for. Otherwise the front end can post to a [headless form backend](/en/blog/headless-form-backend/) and WordPress stays a content store. There are no hidden fields to copy, the form keeps working when WordPress is down or private, and each message is stored as well as emailed.

We sent the same front-end form to a local copy of Formgong's backend. The text-only post was stored with the field names it had, `your-name`, `your-email` and the rest. With a 2 KB PDF attached it was refused with a 400, “This form requires Turnstile, and no token was sent”, because Formgong takes files only from forms with its Turnstile check on the page. If your form has an upload, add that widget first.

```html
<!-- The same form, sent straight to a form backend. WordPress is not involved. -->
<form action="https://formgong.com/submit" method="POST">
  <input type="hidden" name="access_key" value="fk_your_access_key">
  <label>Name <input name="name" autocomplete="name" required></label>
  <label>Email <input name="email" type="email" autocomplete="email" required></label>
  <label>Subject <input name="subject"></label>
  <label>Message <textarea name="message" required></textarea></label>
  <div aria-hidden="true" style="position:absolute;left:-9999px">
    <input name="botcheck" tabindex="-1" autocomplete="off">
  </div>
  <button type="submit">Send</button>
</form>
```

## Do you have to rename your Contact Form 7 fields?

Not for Formgong, and that is the one practical difference we found against Forminit's guide. Its migration steps ask you to rename fields to its own typed names, such as `fi-sender-fullName` and `fi-sender-email`. Formgong keeps any field name as sent, so `your-name` and `your-message` arrive in your email as they are. The one field worth renaming is the email, because Formgong sets Reply-To only from a field called `email` (or `_replyto`), so with `your-email` the Reply button in your mail app would not reach the visitor. Both services otherwise do the same job here, and Basin's older guide (30 January 2022) is about pasting its form into a WordPress site that Strattic serves as static pages, not a separate front end. The [WordPress docs](/en/docs/wordpress/) cover Formgong on a WordPress-rendered site.

## Does your headless setup behave differently?

If your WordPress, Contact Form 7 version or host answers differently from what we saw, write to [support@formgong.com](mailto:support@formgong.com) with the response. We will set it up the same way and add the case to the table.

## Frequently asked questions

### Can I use Contact Form 7 with headless WordPress?

Yes, through its REST route. Send FormData to the numeric form ID with the unit tag fields, and check the JSON status, not only the HTTP code.

### What does wpcf7_unit_tag_not_found mean?

The request has no unit tag. Add _wpcf7_unit_tag with the value wpcf7-f<ID>-o1, plus _wpcf7 and _wpcf7_container_post.

### Why does Contact Form 7 return 200 when the email was not sent?

The HTTP code only says the route ran. mail_failed and validation_failed both came back as HTTP 200 in our test.

### Does Contact Form 7 store submissions?

No. Our WordPress had the same number of posts before and after two sent messages. Add a storage plugin or use a form backend.

## Sources and documentation

- [Formgong: What is a headless form backend?](https://formgong.com/en/blog/headless-form-backend/)
- [WordPress](https://formgong.com/en/docs/wordpress/)

[Get a form key](https://formgong.com/en/#top)
