Practical guide

Headless WordPress forms with Contact Form 7, tested

A headless front end posting to Contact Form 7's REST route, case by case: what WordPress refused, what it said was fine when it was not, and what it kept.

The short answer. 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.

Formgong

Headless WordPress forms with Contact Form 7, tested
Headless WordPress forms with Contact Form 7, tested

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-emailHTTP 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” shows what a blocked REST API looks like.

send-to-cf7.js
// 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 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 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 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.

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

Official references for this guide: Formgong: What is a headless form backend?. WordPress, where data is stored.

Rate this guide

Your rating

We read every rating. A comment appears here only after we approve it.

← Back to the blog