<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rahul Sharma</title>
    <description>The latest articles on DEV Community by Rahul Sharma (@rahul_sharma_15bd129bc69e).</description>
    <link>https://dev.to/rahul_sharma_15bd129bc69e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3476785%2F7115035a-75f6-4d9a-926e-0da2af1b4b40.png</url>
      <title>DEV Community: Rahul Sharma</title>
      <link>https://dev.to/rahul_sharma_15bd129bc69e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahul_sharma_15bd129bc69e"/>
    <language>en</language>
    <item>
      <title>CF7 to CleverReach Integration Errors: What Each One Means and How to Fix It</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Fri, 19 Jun 2026 10:10:12 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/cf7-to-cleverreach-integration-errors-what-each-one-means-and-how-to-fix-it-2ff8</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/cf7-to-cleverreach-integration-errors-what-each-one-means-and-how-to-fix-it-2ff8</guid>
      <description>&lt;p&gt;A site owner posted on the WordPress forums that their CF7 to CleverReach integration had been throwing errors for a week. The logs looked scary. The plugin author said the spam-related errors could be ignored. Thread closed.&lt;/p&gt;

&lt;p&gt;That answer is only partly right. The logs in this thread actually contain three completely different types of errors, each with a different cause and a different fix. If you are seeing similar errors in your CF7 to CleverReach setup, here is what each one actually means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 1: &lt;code&gt;invalid_grant&lt;/code&gt; on the OAuth Token Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://rest.cleverreach.com/oauth/token.php
400 Bad Request
{"error":"invalid_grant","error_description":"Authorization code doesn't exist or is invalid for the client"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most important error in the log and the only one that is genuinely a problem with your integration. It means the OAuth authorization code your plugin used to connect to CleverReach has expired or been invalidated.&lt;/p&gt;

&lt;p&gt;When you first connect the CF7 CleverReach plugin to your account, CleverReach issues an authorization code. That code is exchanged for an access token and a refresh token. If something invalidates those tokens, the plugin can no longer authenticate and every attempt to refresh fails with &lt;code&gt;invalid_grant&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This happens when you change your CleverReach password, when you revoke the app's access in your CleverReach account settings, or when the authorization code expires before the plugin could exchange it. It can also happen if you migrated your WordPress site to a new server and the stored tokens did not transfer correctly.&lt;/p&gt;

&lt;p&gt;The fix is to disconnect the plugin from CleverReach and reconnect it from scratch. Go to the plugin settings, revoke the current connection, and go through the authorization flow again. This gives you a fresh set of tokens and clears the &lt;code&gt;invalid_grant&lt;/code&gt; error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 2: &lt;code&gt;403 Forbidden: email not allowed&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://rest.cleverreach.com/v2/forms.json/239438/send/activate
403 Forbidden
{"error":{"code":403,"message":"Forbidden: email not allowed"}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This error appears dozens of times in the log. The plugin author said to ignore it because CleverReach rejects known spam emails. That is accurate but there is more to understand here.&lt;/p&gt;

&lt;p&gt;CleverReach maintains a blocklist of email addresses and domains that have been flagged as spam sources, disposable inboxes, or previously bounced addresses. When your CF7 form receives a submission from one of these addresses, CleverReach refuses to add it to your list. That is actually CleverReach protecting your sender reputation, not something going wrong with your integration.&lt;/p&gt;

&lt;p&gt;Looking at the actual email addresses in the log, some are clearly spam bot submissions. The field values for several of these entries contain Telegram phishing links and promotional spam text in what should be a name field. These are not real people filling in your form. They are bots.&lt;/p&gt;

&lt;p&gt;So the &lt;code&gt;403 email not allowed&lt;/code&gt; errors are doing you a favour. CleverReach is rejecting spam bot submissions that should never have reached your email list in the first place. You do not need to fix this error. You need to stop the spam bots from submitting your form so they never hit the CleverReach API at all.&lt;/p&gt;

&lt;p&gt;Adding a honeypot field or enabling Cloudflare Turnstile on your CF7 form stops most bots before they submit. CF7 has a built-in honeypot option that bots fill in but real users cannot see, and CF7 rejects any submission where that field has a value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 3: &lt;code&gt;400 Bad Request: duplicate address&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://rest.cleverreach.com/v2/groups.json/1173358/receivers
400 Bad Request
{"error":{"code":400,"message":"Bad Request: \"duplicate address 'ackstein01@aol.com'\""}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This error means someone tried to sign up with an email address that already exists in your CleverReach group. This is not a bug and it is not something you need to fix. CleverReach prevents duplicate email addresses in the same group.&lt;/p&gt;

&lt;p&gt;If you want to update an existing contact's details when they submit your form again, you need to use CleverReach's update endpoint rather than the add endpoint. Most CF7 integration plugins use the add endpoint by default and do not handle duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem: Your Form Has No Spam Protection
&lt;/h2&gt;

&lt;p&gt;The most revealing thing in this log is what the spam bot submissions look like. Fields that should contain a person's name contain Telegram links and cryptocurrency scam text. This means your CF7 form is completely unprotected and bots are submitting it freely.&lt;/p&gt;

&lt;p&gt;Every spam submission that reaches CleverReach uses one of your API calls and potentially triggers a 403 error in your logs. More importantly, if a bot ever submits with a real-looking email address that CleverReach does not recognise as spam, that address gets added to your marketing list without that person's consent.&lt;/p&gt;

&lt;p&gt;Fixing your spam problem protects your CleverReach list quality, reduces unnecessary API calls, and clears most of the errors from your log. The best options are CF7's built-in honeypot, Google reCAPTCHA, or Cloudflare Turnstile, all of which CF7 supports natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Reliable Way to Connect CF7 to CleverReach
&lt;/h2&gt;

&lt;p&gt;The dedicated CF7 CleverReach plugin works for many people but it has limitations. It relies on an OAuth connection that can break when credentials change, it does not handle duplicate contacts gracefully, and it gives you limited control over which fields are sent and how.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-email-automation/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; connects CF7 directly to CleverReach's REST API using a straightforward API key instead of OAuth. API keys do not expire the way OAuth tokens do, so you do not get &lt;code&gt;invalid_grant&lt;/code&gt; errors weeks after setting up the integration. You also get full control over the field mapping, which means you can send exactly the data CleverReach expects and avoid payload issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invalid_grant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OAuth tokens expired or revoked&lt;/td&gt;
&lt;td&gt;Disconnect and reconnect the plugin in settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403 email not allowed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CleverReach blocked a spam or invalid email&lt;/td&gt;
&lt;td&gt;Add spam protection to your CF7 form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;400 duplicate address&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Email already exists in CleverReach group&lt;/td&gt;
&lt;td&gt;Normal behaviour, or switch to an update endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>wordpress</category>
      <category>emailmarketing</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CF7 Webhook Getting a 400 Bad Request? The Problem Is Probably Not JSON Encoding</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Thu, 18 Jun 2026 12:30:47 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/cf7-webhook-getting-a-400-bad-request-the-problem-is-probably-not-json-encoding-37hi</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/cf7-webhook-getting-a-400-bad-request-the-problem-is-probably-not-json-encoding-37hi</guid>
      <description>&lt;p&gt;A developer posted on the WordPress forums with a detailed bug report. Their CF7 webhook was sending data to an external REST API and getting a 400 Bad Request response. The API was returning validation errors saying required fields were empty. They had checked the debug log, seen the request body wrapped in quotes, and concluded the plugin was double-encoding the JSON.&lt;/p&gt;

&lt;p&gt;The plugin author explained that the debug log shows a string representation of the payload for logging purposes, not the actual bytes sent over the network. The JSON was never double-encoded.&lt;/p&gt;

&lt;p&gt;The real problem was a wrong field name. The developer used &lt;code&gt;_notify_leadtype_id&lt;/code&gt; but the API required &lt;code&gt;leadtype_id&lt;/code&gt;. Because the field name was wrong, the API never received a value for that required field and rejected the whole request.&lt;/p&gt;

&lt;p&gt;This is one of the most common reasons CF7 webhooks return 400 errors, and it is almost always misdiagnosed as an encoding issue. This post explains how to read webhook debug logs correctly and how to find the actual cause of 400 errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Debug Log Is Actually Showing You
&lt;/h2&gt;

&lt;p&gt;When a CF7 webhook plugin sends a debug email after a failed request, the request body section looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Request&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Body:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;lead_type_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 318825,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;contact_name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;John Doe&lt;/span&gt;&lt;span class="se"&gt;\"\n&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outer quotes and all those backslash characters make it look like the JSON has been double-encoded or stringified inside a string. It has not. This is just how a JSON string looks when it is printed as a PHP string for logging purposes. The backslashes are escape characters that PHP adds when displaying a string that contains quote marks.&lt;/p&gt;

&lt;p&gt;The actual bytes sent to the API over the network look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lead_type_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;318825&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contact_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is valid JSON. The API receives it correctly formatted. The debug log display is misleading but it is not evidence of a problem with the encoding.&lt;/p&gt;

&lt;p&gt;If you see a 400 response with validation errors saying fields are empty, the encoding is almost certainly fine. Look at the field names instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Cause of "Field Cannot Be Empty" on a 400 Response
&lt;/h2&gt;

&lt;p&gt;When an API returns a 400 with a message like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"leadtype_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Field cannot be empty"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means the API received the request, parsed the JSON successfully, and then checked whether required fields were present. It found that &lt;code&gt;leadtype_id&lt;/code&gt; was missing or null.&lt;/p&gt;

&lt;p&gt;This happens for three reasons in CF7 webhook setups:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong field name in your webhook body.&lt;/strong&gt; You used &lt;code&gt;_notify_leadtype_id&lt;/code&gt; but the API expects &lt;code&gt;leadtype_id&lt;/code&gt;. The API ignores the field it does not recognise and treats the required field as empty. This was exactly the problem in the forum thread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field value is empty because the CF7 field was not filled in.&lt;/strong&gt; If you are mapping a CF7 form field to the API and the user left that field blank, the API receives an empty string or null for a required field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The value is a hardcoded static value that was not included correctly.&lt;/strong&gt; The developer needed to send &lt;code&gt;lead_type_id: 318825&lt;/code&gt; as a fixed integer, not from a form field. If the webhook plugin does not support hardcoded static values in the request body, the field either gets sent as the placeholder text or not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Find the Correct Field Names
&lt;/h2&gt;

&lt;p&gt;Every API has documentation that lists the exact field names it expects. These are case-sensitive and exact. &lt;code&gt;leadtype_id&lt;/code&gt;, &lt;code&gt;lead_type_id&lt;/code&gt;, and &lt;code&gt;LeadTypeId&lt;/code&gt; are all different field names that an API treats as completely different keys.&lt;/p&gt;

&lt;p&gt;Before setting up a webhook, open the API documentation for the endpoint you are calling. Find the request body schema and write down the exact field names as they appear in the docs. Do not copy them from a code example or a forum post. Copy them from the official reference page for that specific endpoint.&lt;/p&gt;

&lt;p&gt;For the RO App API in this thread, the correct documentation page was &lt;code&gt;roapp.readme.io/reference/create-lead&lt;/code&gt;. The field there was &lt;code&gt;leadtype_id&lt;/code&gt;, not &lt;code&gt;_notify_leadtype_id&lt;/code&gt;. One character difference, completely silent failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Field Names Before Connecting CF7
&lt;/h2&gt;

&lt;p&gt;Before wiring up your CF7 form, test the API call directly with your exact field names and values. The fastest way is cURL from a terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.example.com/lead/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "leadtype_id": 318825,
    "contact_name": "Test User",
    "contact_phone": "123456789"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns a success response, your field names are correct and your credentials work. Then when you set up CF7, you know any problems are in the mapping, not in your understanding of the API.&lt;/p&gt;

&lt;p&gt;If this returns a 400, the problem is in your request. Check the field names against the documentation one more time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending Static Values Alongside Form Fields
&lt;/h2&gt;

&lt;p&gt;The developer needed to send &lt;code&gt;leadtype_id&lt;/code&gt; as a fixed number (not from a form field) alongside dynamic values like &lt;code&gt;contact_name&lt;/code&gt; from the form. This is a common requirement when integrating with CRMs that need an internal category ID alongside the contact details.&lt;/p&gt;

&lt;p&gt;Not all CF7 webhook plugins support mixing static values and dynamic form field values in the same request body. Some only support CF7 field placeholders.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-json-mapping/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; supports both. You can set some fields as static hardcoded values (like &lt;code&gt;"leadtype_id": 318825&lt;/code&gt;) and map other fields dynamically from your CF7 form submission. The request body is built correctly and sent as a real JSON object to your API endpoint.&lt;/p&gt;

&lt;p&gt;This is exactly the use case from the forum thread: a fixed lead type ID that never changes, combined with dynamic contact details from the form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Checklist When You Get a 400 Response
&lt;/h2&gt;

&lt;p&gt;Work through these before assuming anything is wrong with the plugin:&lt;/p&gt;

&lt;p&gt;First, check the field names in your webhook body against the API documentation. Copy and paste them directly from the official reference. Do not type them from memory.&lt;/p&gt;

&lt;p&gt;Second, look at the validation errors in the response body. If the API tells you which field is empty or invalid, it is telling you exactly what to fix.&lt;/p&gt;

&lt;p&gt;Third, test the API call directly with cURL or Postman using your exact payload. If it works there, the problem is in how your plugin is building the request.&lt;/p&gt;

&lt;p&gt;Fourth, check whether your plugin supports static hardcoded values in the request body. If you need to send a fixed ID alongside form fields and the plugin does not support that, you will always get a missing field error.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Connect Contact Form 7 to Monday.com CRM</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Wed, 17 Jun 2026 10:37:24 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/how-to-connect-contact-form-7-to-mondaycom-crm-47dn</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/how-to-connect-contact-form-7-to-mondaycom-crm-47dn</guid>
      <description>&lt;p&gt;Someone asked on the WordPress support forums: "What is the best way to connect Contact Form 7 to Monday CRM? Is a webhook a good approach?"&lt;/p&gt;

&lt;p&gt;The reply said it is not possible with CF7's built-in features alone and suggested finding a plugin or writing custom code.&lt;/p&gt;

&lt;p&gt;That answer is accurate but leaves the person with no path forward. This post gives you the full picture - how Monday.com's API works, what a webhook approach actually involves, and the simplest way to get CF7 submissions into Monday.com without writing any code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CF7 Does Not Connect to Monday.com Out of the Box
&lt;/h2&gt;

&lt;p&gt;Contact Form 7 is a form builder. It handles displaying your form, validating the fields, and sending you an email when someone submits. It was not built to push data to external services.&lt;/p&gt;

&lt;p&gt;Connecting CF7 to Monday.com requires something in the middle that catches the form submission and sends it to Monday's API. That something is either a plugin, a webhook handler, or custom PHP code.&lt;/p&gt;

&lt;p&gt;The good news is that Monday.com has a well-documented API and the connection is straightforward once you understand the pieces involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Monday.com Receives Data from External Sources
&lt;/h2&gt;

&lt;p&gt;Monday.com accepts incoming data through its REST API. When you want to create a new item in a Monday board from a form submission, you send a request to Monday's API with the field values mapped to the columns in your board.&lt;/p&gt;

&lt;p&gt;Monday.com uses GraphQL for its API, which means you send structured queries rather than simple POST bodies. A basic item creation request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.monday.com/v2
Authorization: Bearer YOUR_MONDAY_API_TOKEN
Content-Type: application/json

{
  "query": "mutation { create_item (board_id: YOUR_BOARD_ID, item_name: \"CONTACT_NAME\", column_values: \"{\\\"email\\\": {\\\"email\\\": \\\"EMAIL\\\", \\\"text\\\": \\\"EMAIL\\\"}}\") { id } }"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get your API token from Monday.com under your profile avatar at the top right, then Admin, then API. Your board ID appears in the URL when you open the board.&lt;/p&gt;

&lt;p&gt;This is the call that needs to happen automatically every time someone submits your CF7 form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Use Contact Form to API (Easiest, No Code)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-third-party-integration-guide/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; lets you connect CF7 to Monday.com directly from the WordPress dashboard. You set the Monday API endpoint, add your API token as an Authorization header, and map your CF7 fields to the Monday column values.&lt;/p&gt;

&lt;p&gt;When someone submits your form, the plugin sends the data to Monday automatically. A new item appears in your board with the contact's name, email, phone, and any other fields you mapped.&lt;/p&gt;

&lt;p&gt;This takes about ten minutes to set up and requires no code. You do not need to write GraphQL queries by hand or manage any webhook URLs. The plugin handles the outbound request and logs the response so you can confirm each submission arrived.&lt;/p&gt;

&lt;p&gt;If you are running a business and you need leads going into Monday.com reliably, this is the approach that saves the most time and causes the fewest problems long term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Webhook Approach (More Setup, More Fragile)
&lt;/h2&gt;

&lt;p&gt;The person in the forum asked whether webhooks are a good approach. Technically yes, but it involves more moving parts.&lt;/p&gt;

&lt;p&gt;A webhook approach typically means setting up a third-party automation tool like Zapier or Make between CF7 and Monday.com. CF7 sends form data to the automation tool, which then sends it to Monday.&lt;/p&gt;

&lt;p&gt;The problem with this approach is the cost and the dependency chain. Zapier charges per task. Make charges per operation. Every form submission uses up credits. If the automation tool has downtime or changes their pricing, your integration breaks or becomes expensive.&lt;/p&gt;

&lt;p&gt;There are also more points of failure. The form submits to CF7, CF7 sends to the automation tool, the automation tool sends to Monday. Any of those steps can fail silently.&lt;/p&gt;

&lt;p&gt;A direct API connection from CF7 to Monday removes the middle layer entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Custom PHP Code
&lt;/h2&gt;

&lt;p&gt;If you are comfortable with PHP, you can write a function that hooks into CF7's submission event and sends data directly to Monday's GraphQL API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wpcf7_before_send_mail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'send_cf7_to_monday'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;send_cf7_to_monday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$contact_form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$contact_form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="no"&gt;YOUR_FORM_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$submission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WPCF7_Submission&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_instance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$submission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$data&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$submission&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_posted_data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'your-name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'your-email'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$token&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONDAY_API_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;MONDAY_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$board_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONDAY_BOARD_ID'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;MONDAY_BOARD_ID&lt;/span&gt;  &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$column_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mutation { create_item (board_id: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$board_id&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;', item_name: "'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'", column_values: "'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;addslashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$column_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'") { id } }'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;wp_remote_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://api.monday.com/v2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'Authorization'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Bearer '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'body'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'query'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your token and board ID to &lt;code&gt;wp-config.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONDAY_API_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-token-here'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONDAY_BOARD_ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'1234567890'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works but requires you to maintain the code, update it when your form changes, and handle errors yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Approach Is Right for You
&lt;/h2&gt;

&lt;p&gt;If you want leads in Monday.com starting today without writing code, use Contact Form to API. It was built exactly for this kind of CF7-to-CRM connection and handles the API call, authentication, and field mapping from a simple admin interface.&lt;/p&gt;

&lt;p&gt;If you already have Zapier or Make and use it for other things, the webhook route adds one more zap or scenario. Expect to pay per submission.&lt;/p&gt;

&lt;p&gt;If you are a developer and want full control, the custom PHP approach is clean and maintainable as long as you are comfortable owning it.&lt;/p&gt;

&lt;p&gt;Most WordPress site owners who ask "how do I connect CF7 to Monday" are looking for the no-code path. That is Contact Form to API.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>crm</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>CF7 Webhook Throwing a 500 Error? Checkboxes and Acceptance Fields Are Probably the Cause</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:37:52 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/cf7-webhook-throwing-a-500-error-checkboxes-and-acceptance-fields-are-probably-the-cause-1794</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/cf7-webhook-throwing-a-500-error-checkboxes-and-acceptance-fields-are-probably-the-cause-1794</guid>
      <description>&lt;p&gt;You set up a webhook in CF7 to send form data to an external service. You test it with a simple email field and it works perfectly. Then you add a checkbox, an acceptance field, or a multi-select dropdown to your form and suddenly every submission throws a 500 error.&lt;/p&gt;

&lt;p&gt;The form stops working entirely. No data reaches your webhook. Your visitors see a broken form.&lt;/p&gt;

&lt;p&gt;This is a real bug that was reported on the WordPress support forums and confirmed across WordPress 6.9, PHP 8.2, and CF7 6.1.4. Here is exactly what is happening and how to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Actually Going Wrong
&lt;/h2&gt;

&lt;p&gt;When a user submits a CF7 form, the webhook feature takes the field values and replaces the placeholders in your webhook body template with the actual submitted values.&lt;/p&gt;

&lt;p&gt;For a simple text or email field, the value is a plain string. The replacement works fine.&lt;/p&gt;

&lt;p&gt;But checkbox fields, acceptance fields, and multi-select dropdowns return &lt;strong&gt;arrays&lt;/strong&gt; in PHP, not strings. When the webhook code tries to do a string replacement with an array value, PHP throws a fatal error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHP Fatal error: Uncaught TypeError: str_replace(): Argument #2 ($replace)
must be of type string when argument #1 ($search) is a string
in webhook.php:464
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CF7 Apps plugin assumed every field value would be a string. Acceptance checkboxes, regular checkboxes, and multi-select dropdowns break that assumption because they return multiple selected values as an array.&lt;/p&gt;

&lt;p&gt;The result: a 500 server error on every submission that includes those field types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Field Types Trigger This Error
&lt;/h2&gt;

&lt;p&gt;Three CF7 field types cause this problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acceptance fields&lt;/strong&gt; — the GDPR consent checkbox that CF7 provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[acceptance accept-terms] I agree to the terms[/acceptance]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checkbox fields&lt;/strong&gt; — any field using &lt;code&gt;[checkbox]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[checkbox services "Web Design" "SEO" "Hosting"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dropdowns with the &lt;code&gt;multiple&lt;/code&gt; attribute&lt;/strong&gt; — allowing users to select more than one option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[select* services multiple "Option 1" "Option 2" "Option 3"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three return arrays from CF7's submission handler. Any webhook plugin that does not handle arrays will throw the same fatal error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quick Fix If You Must Keep Using CF7 Apps Webhook
&lt;/h2&gt;

&lt;p&gt;The CF7 Apps team released a beta fix for this bug. If you are already committed to using their webhook feature, download the beta from their support thread and install it manually.&lt;/p&gt;

&lt;p&gt;However, be aware that even after the fix, single-select dropdowns send their values as a JSON array with one item rather than a plain string. So a user selecting "Option 2" from a dropdown arrives at your webhook as &lt;code&gt;["Option 2"]&lt;/code&gt; instead of &lt;code&gt;"Option 2"&lt;/code&gt;. Depending on what your receiving API expects, this may cause further problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Better Fix: Use a Plugin That Handles Arrays Properly From the Start
&lt;/h2&gt;

&lt;p&gt;Patching around bugs in webhook plugins is a maintenance burden. Every time CF7 updates or the webhook plugin updates, you risk the same class of problem coming back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-third-party-integration-guide/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; was built specifically to handle CF7 form data and send it to external APIs correctly. It handles checkbox arrays, acceptance fields, and multi-select dropdowns without throwing 500 errors because it processes CF7's submission data properly before building the outbound request.&lt;/p&gt;

&lt;p&gt;You get a clean configuration interface where you map CF7 fields to your API's expected format, set your endpoint URL, configure authentication headers, and test the connection all from the WordPress dashboard. No fatal PHP errors. No broken forms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Keeps Happening With CF7 Webhooks
&lt;/h2&gt;

&lt;p&gt;CF7's field types fall into two categories: fields that return a single string value (text, email, phone, textarea) and fields that return arrays (checkboxes, acceptance, multi-select). Most webhook plugins are built and tested only with the simple string fields. Array fields get added to forms later and the webhook silently breaks.&lt;/p&gt;

&lt;p&gt;The pattern repeats across different plugins because the underlying issue is the same: not checking the type of a field value before doing string operations on it.&lt;/p&gt;

&lt;p&gt;If your form collects consent, services selected, or any kind of multi-option choice, you need a webhook or API integration tool that explicitly handles arrays. Contact Form to API converts array field values (comma-separated or serialized) into a format your API can consume, so your checkbox selections arrive at your CRM cleanly rather than crashing the whole submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Check If You Are Seeing a 500 Error
&lt;/h2&gt;

&lt;p&gt;If your CF7 form is throwing a 500 error on submission and you have a webhook configured, check these things in order:&lt;/p&gt;

&lt;p&gt;First, look at your server's PHP error log. The error will include a stack trace pointing to the exact file and line. If you see &lt;code&gt;str_replace()&lt;/code&gt; with a TypeError about array arguments, this is the same bug described here.&lt;/p&gt;

&lt;p&gt;Second, check whether your form contains any checkbox, acceptance, or multi-select dropdown fields. Remove them temporarily and test the form. If the 500 error disappears, those fields are the trigger.&lt;/p&gt;

&lt;p&gt;Third, decide whether to patch the existing webhook plugin or switch to a tool that handles CF7 field types correctly. If you are managing more than one form or more than one integration, switching to Contact Form to API pays off quickly because you manage everything from one reliable dashboard rather than hunting down bugs across multiple plugins.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>CF7 File Uploads: Why the Attachment Never Arrives in Your Email</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Mon, 15 Jun 2026 07:00:51 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/cf7-file-uploads-why-the-attachment-never-arrives-in-your-email-1a0j</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/cf7-file-uploads-why-the-attachment-never-arrives-in-your-email-1a0j</guid>
      <description>&lt;p&gt;Two developers posted the same problem on the WordPress forums. Both had the CF7 file field set up correctly in the form. Both had &lt;code&gt;[your-file]&lt;/code&gt; in the Mail tab. Neither received the file as an attachment in their email.&lt;/p&gt;

&lt;p&gt;One developer found the fix in a video. Here is what the video showed that the CF7 documentation does not mention anywhere:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is a third place you need to add the file tag — the File Attachments field at the bottom of the Mail tab. Most people never scroll down far enough to see it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the entire fix. But understanding why CF7 works this way, what each of the three placements actually does, and how to handle file uploads that need to go somewhere beyond email is worth covering properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Places the File Tag Goes (And What Each One Does)
&lt;/h2&gt;

&lt;p&gt;CF7's file upload has three completely separate functions that require separate tag placements:&lt;/p&gt;

&lt;h3&gt;
  
  
  Placement 1: The Form Tab
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[file your-file filetypes:pdf|doc|docx limit:2mb]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This renders the file input element in the HTML form. Without this, users have no way to select a file. This is the only placement that most tutorials cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Placement 2: The Mail Tab Body
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;School/Organization Logo: [your-file]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inserts the &lt;strong&gt;filename&lt;/strong&gt; (not the file itself) into the email body as text. It tells you what file was uploaded but does not attach it. This is where the confusion starts. Both developers in the thread had this placement and assumed it would attach the file. It does not. It just prints the filename as a string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Placement 3: The Mail Tab File Attachments Field
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[your-file]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the field that actually attaches the file to the outgoing email. It is at the bottom of the Mail tab, below the Message Body field. It is easy to miss because it is small, unlabelled in some CF7 versions, and completely absent from the official documentation.&lt;/p&gt;

&lt;p&gt;Without this third placement, the file is uploaded to your server (&lt;code&gt;wp-content/uploads/wpcf7_uploads/&lt;/code&gt;) but never attached to the email. The email arrives with the filename mentioned in the body but no attachment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by Step: Correct CF7 File Upload Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Form Tab
&lt;/h3&gt;

&lt;p&gt;Use the File button in the CF7 form editor toolbar to insert the tag (do not type it manually — the UI generates a clean tag with the correct format):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[file file-upload filetypes:pdf|doc|docx|jpg|png limit:5mb]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common mistakes in the form tag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;filetypes:image/*&lt;/code&gt; with the wildcard — some servers reject this. Use explicit types: &lt;code&gt;image/jpeg|image/png|image/gif&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;limit&lt;/code&gt; too high — hosting providers often have a lower &lt;code&gt;upload_max_filesize&lt;/code&gt; in &lt;code&gt;php.ini&lt;/code&gt; than what you set in CF7. CF7's limit cannot exceed the server's PHP limit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Mail Tab Body (Optional)
&lt;/h3&gt;

&lt;p&gt;Add this where you want the filename to appear in the email body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uploaded file: [file-upload]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is optional. If you only want the attachment without mentioning it in the body, skip this placement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Mail Tab File Attachments Field (Required for Attachment)
&lt;/h3&gt;

&lt;p&gt;Scroll to the bottom of the Mail tab. Find the field labelled &lt;strong&gt;File Attachments&lt;/strong&gt; (it may be collapsed or hidden below the message body area). Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[file-upload]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the step that actually causes the file to be sent as an email attachment. Without it, the file exists on your server but never reaches the recipient's inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Filetypes Wildcard Problem
&lt;/h2&gt;

&lt;p&gt;The original poster used &lt;code&gt;filetypes:audio/*|video/*|image/*&lt;/code&gt; with wildcards. This is a CF7-supported syntax but can cause issues on some server configurations where the MIME type detection does not resolve wildcards correctly.&lt;/p&gt;

&lt;p&gt;The safer approach is explicit MIME types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[file your-file filetypes:image/jpeg|image/png|image/gif|image/webp|audio/mpeg|video/mp4 limit:1mb]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for common document uploads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[file your-file filetypes:pdf|doc|docx|xls|xlsx|txt limit:5mb]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CF7 accepts both MIME type strings and file extensions. File extensions are simpler to read and work reliably across server configurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Uploaded Files Actually Go
&lt;/h2&gt;

&lt;p&gt;Files uploaded through CF7 go to &lt;code&gt;wp-content/uploads/wpcf7_uploads/&lt;/code&gt; temporarily. CF7 deletes them after the email is sent. They are not permanently stored on your server.&lt;/p&gt;

&lt;p&gt;This is what the original poster saw in Flamingo: a number (the temporary file ID) rather than a clickable link. Flamingo records that a file was submitted but does not store the file itself. Once CF7 processes the form and sends the email, the file is gone from the server.&lt;/p&gt;

&lt;p&gt;If you need permanent file storage (files accessible after the email is sent), you have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1:&lt;/strong&gt; Use a plugin like "CF7 to Post" or "CF7 Storage" to save uploads permanently alongside the form submission record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2:&lt;/strong&gt; Forward the file to external storage via API. &lt;a href="https://www.contactformtoapi.com/automate-wordpress-form-submissions/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; can send form submission data including file references to an external endpoint where the file can be stored in cloud storage (S3, Google Drive, Dropbox, etc.) as part of a webhook workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Fields Optional
&lt;/h2&gt;

&lt;p&gt;The second question in the thread: how to make certain fields optional.&lt;/p&gt;

&lt;p&gt;In CF7, field tags with an asterisk are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[text* your-name]    &amp;lt;- required
[text your-company]  &amp;lt;- optional
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the asterisk to make a field optional. For file uploads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[file your-file filetypes:pdf limit:5mb]         &amp;lt;- optional upload
[file* your-file-required filetypes:pdf limit:5mb] &amp;lt;- required upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional file fields do not block form submission if left empty. Required fields with &lt;code&gt;*&lt;/code&gt; trigger CF7's validation and prevent submission if no file is selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Placement&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[file your-file ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Form tab&lt;/td&gt;
&lt;td&gt;Renders the file input in the HTML form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[your-file]&lt;/code&gt; in body&lt;/td&gt;
&lt;td&gt;Mail tab body&lt;/td&gt;
&lt;td&gt;Inserts the filename as text in the email body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[your-file]&lt;/code&gt; in File Attachments&lt;/td&gt;
&lt;td&gt;Mail tab bottom&lt;/td&gt;
&lt;td&gt;Actually attaches the file to the outgoing email&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three are needed if you want the file to appear as an email attachment. The third one is the one nobody mentions and the one that solves the problem.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>email</category>
    </item>
    <item>
      <title>How to Use a CF7 Field to Query an External API and Populate Other Fields</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Fri, 12 Jun 2026 11:28:35 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/how-to-use-a-cf7-field-to-query-an-external-api-and-populate-other-fields-285h</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/how-to-use-a-cf7-field-to-query-an-external-api-and-populate-other-fields-285h</guid>
      <description>&lt;p&gt;A developer asked on the WordPress forums: can a CF7 field value be used to query an external API and populate other fields on the same form?&lt;/p&gt;

&lt;p&gt;The plugin author replied: not out of the box, you need custom AJAX code and JavaScript. Thread closed.&lt;/p&gt;

&lt;p&gt;That answer points in the right direction but gives nothing to work with. This post builds the complete implementation: a WordPress AJAX proxy endpoint on the server side (to keep your API keys out of the browser), JavaScript that fires on field input, and DOM manipulation that populates CF7 fields from the API response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Need a Server-Side Proxy
&lt;/h2&gt;

&lt;p&gt;The instinct is to call the external API directly from JavaScript in the browser. Do not do this if the API requires an API key or any form of authentication.&lt;/p&gt;

&lt;p&gt;Calling an authenticated API from client-side JavaScript exposes your credentials in the browser's network tab. Anyone can open DevTools, copy your API key, and use it against your quota or billing account.&lt;/p&gt;

&lt;p&gt;The correct pattern is a two-hop architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User types in CF7 field
        |
        | fetch() to your WordPress AJAX endpoint
        v
WordPress handles the request server-side
        |
        | wp_remote_get() with your API key (never visible to browser)
        v
External API returns data
        |
        | WordPress passes sanitized response back to browser
        v
JavaScript populates the other CF7 fields
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API key lives only in &lt;code&gt;wp-config.php&lt;/code&gt;. The browser never sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Register the WordPress AJAX Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register for both logged-in and non-logged-in users&lt;/span&gt;
&lt;span class="c1"&gt;// (CF7 forms are typically on public pages)&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_ajax_cf7_api_lookup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'cf7_api_lookup_handler'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_ajax_nopriv_cf7_api_lookup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cf7_api_lookup_handler'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cf7_api_lookup_handler&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Verify the nonce to prevent CSRF abuse of your proxy&lt;/span&gt;
    &lt;span class="nf"&gt;check_ajax_referer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cf7_api_lookup_nonce'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'nonce'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'query'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;wp_send_json_error&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Query is required'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate format if needed (e.g. postcode pattern, company number format)&lt;/span&gt;
    &lt;span class="c1"&gt;// if (!preg_match('/^\d{5}$/', $query)) {&lt;/span&gt;
    &lt;span class="c1"&gt;//     wp_send_json_error(['message' =&amp;gt; 'Invalid format'], 422);&lt;/span&gt;
    &lt;span class="c1"&gt;// }&lt;/span&gt;

    &lt;span class="nv"&gt;$api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EXTERNAL_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;EXTERNAL_API_KEY&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$api_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add_query_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$api_key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'https://api.example.com/v1/lookup'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$api_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Accept'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;wp_send_json_error&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'API request failed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_retrieve_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$body&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wp_remote_retrieve_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;wp_send_json_error&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'No results found'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Return only the fields you need — do not expose raw API response to browser&lt;/span&gt;
    &lt;span class="nf"&gt;wp_send_json_success&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'company_name'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'company'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'street_address'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'street'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'city'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'city'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'postcode'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'postcode'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your API key in &lt;code&gt;wp-config.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EXTERNAL_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-api-key-here'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Pass the Nonce to JavaScript
&lt;/h2&gt;

&lt;p&gt;WordPress AJAX requires a nonce for &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt; endpoints. Pass it to your script using &lt;code&gt;wp_localize_script&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_enqueue_scripts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cf7_api_lookup_enqueue'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cf7_api_lookup_enqueue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only enqueue on pages that have the CF7 form&lt;/span&gt;
    &lt;span class="c1"&gt;// Adjust the condition to match where your form appears&lt;/span&gt;
    &lt;span class="nf"&gt;wp_enqueue_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'cf7-api-lookup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;get_template_directory_uri&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/js/cf7-api-lookup.js'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jquery'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'1.0.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;wp_localize_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cf7-api-lookup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CF7ApiLookup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'ajaxUrl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin-ajax.php'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'nonce'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;wp_create_nonce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cf7_api_lookup_nonce'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The JavaScript
&lt;/h2&gt;

&lt;p&gt;This is where the CF7 field triggers the lookup and the other fields get populated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// js/cf7-api-lookup.js&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Debounce: wait until the user stops typing before firing the API call&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// type: 'loading' | 'success' | 'error' | ''&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;statusEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.cf7-lookup-status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;statusEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;statusEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;statusEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf7-lookup-status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;statusEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cssText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;display:block;font-size:0.85em;margin-top:4px;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;inputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusEl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#888&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2a7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#c33&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#888&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nx"&gt;statusEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#888&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;statusEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clearFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fieldNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;fieldNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;populateFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Map API response keys to CF7 field names&lt;/span&gt;
        &lt;span class="c1"&gt;// Adjust these to match your actual CF7 field names&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fieldMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;company-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;street_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;street-address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fieldMap&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fieldName&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fieldName&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="c1"&gt;// Trigger change event so CF7 validation re-evaluates&lt;/span&gt;
                &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Looking up...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;clearFields&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;company-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;street-address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf7_api_lookup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nonce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;CF7ApiLookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CF7ApiLookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ajaxUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;populateFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Details found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No results found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lookup failed. Please fill in manually.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Replace 'company-number' with your actual CF7 trigger field name&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triggerInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="company-number"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedLookup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;doLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fires 600ms after the user stops typing&lt;/span&gt;

        &lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;debouncedLookup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the 600ms Debounce Does
&lt;/h2&gt;

&lt;p&gt;Without debouncing, every keystroke fires an AJAX request. A user typing "12345" produces five requests, four of which are abandoned as the user keeps typing. Debouncing waits until the user stops for 600ms before firing. This reduces API calls from one-per-keystroke to one-per-completed-entry.&lt;/p&gt;

&lt;p&gt;For postcode or company number lookups where users typically paste the value rather than type it, you can also listen on &lt;code&gt;blur&lt;/code&gt; (when the field loses focus) instead of &lt;code&gt;input&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;doLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;triggerInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;blur&lt;/code&gt; fires exactly once when the user leaves the field, regardless of how many characters they typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Populated Fields Required in CF7
&lt;/h2&gt;

&lt;p&gt;If you want CF7's built-in validation to treat auto-populated fields as required, they need to be marked as required in your CF7 form builder using &lt;code&gt;[text* field-name]&lt;/code&gt;. The &lt;code&gt;*&lt;/code&gt; makes the field required. Since your JavaScript populates them before submission, CF7 will validate them as filled.&lt;/p&gt;

&lt;p&gt;If a user skips the trigger field and the populated fields remain empty, CF7 blocks the form submission with a validation error on the required fields. This is the correct behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Contact Form to API for the Submission Side
&lt;/h2&gt;

&lt;p&gt;This pattern handles the lookup before submission. For sending the submitted data (including the populated fields) to an external API after the user clicks submit, &lt;a href="https://www.contactformtoapi.com/connect-wpform-to-any-api/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; handles the outbound POST to your CRM or backend without custom PHP on the submission side. The two approaches work together: JavaScript for the lookup, Contact Form to API for the outbound submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AJAX endpoint registered for both &lt;code&gt;wp_ajax_&lt;/code&gt; and &lt;code&gt;wp_ajax_nopriv_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Nonce verified in the handler with &lt;code&gt;check_ajax_referer()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API key in &lt;code&gt;wp-config.php&lt;/code&gt; constants, never in JavaScript&lt;/li&gt;
&lt;li&gt;Response sanitized before sending back to browser&lt;/li&gt;
&lt;li&gt;Debounce set on the trigger field (600ms for typing, or use &lt;code&gt;blur&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dispatchEvent(new Event('change'))&lt;/code&gt; after populating each field so CF7 validation re-evaluates&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>javascript</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>"Invalid OAuth2 Access Token" in CF7 Google Sheets Connector: What the Stack Trace Actually Tells You</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Thu, 11 Jun 2026 11:23:15 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/invalid-oauth2-access-token-in-cf7-google-sheets-connector-what-the-stack-trace-actually-tells-4jap</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/invalid-oauth2-access-token-in-cf7-google-sheets-connector-what-the-stack-trace-actually-tells-4jap</guid>
      <description>&lt;p&gt;A developer posted this error on the WordPress forums after connecting their CF7 form to Google Sheets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ERROR_MSG] =&amp;gt; Auth, Invalid OAuth2 access token
[TRACE_STK] =&amp;gt; #0 class-gs-service.php(182): CF7GSC_googlesheet-&amp;gt;auth()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin author replied: probably a Google library conflict with another plugin. Deactivate everything else and test.&lt;/p&gt;

&lt;p&gt;That is one of three possible causes. The stack trace tells you much more if you know how to read it. This post breaks down what is actually happening, why this error appears even when your connection was working fine, and how to fix each cause.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading the Stack Trace
&lt;/h2&gt;

&lt;p&gt;The stack trace is not noise. It is a precise execution path. Here is what it tells you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#0  class-gs-service.php(182): CF7GSC_googlesheet-&amp;gt;auth()
#1  class-wp-hook.php(308):    Gs_Connector_Service-&amp;gt;cf7_save_to_google_sheets()
#4  submission.php(119):       do_action('wpcf7_mail_sent', ...)
#10 rest-api.php(357):         WPCF7_ContactForm-&amp;gt;submit()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading bottom-up (which is how stack traces work):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user submits a CF7 form via the REST API (&lt;code&gt;rest-api.php&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CF7 processes the submission (&lt;code&gt;submission.php&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CF7 fires the &lt;code&gt;wpcf7_mail_sent&lt;/code&gt; action after the email is sent (&lt;code&gt;submission.php:119&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;GSheetConnector catches that hook and calls &lt;code&gt;cf7_save_to_google_sheets()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;That function calls &lt;code&gt;auth()&lt;/code&gt; to validate the Google OAuth token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth()&lt;/code&gt; fails with &lt;code&gt;Invalid OAuth2 access token&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The form submitted successfully. The email was sent. The Google Sheets write is what failed. This is important: the form is not broken, only the Sheets logging is broken.&lt;/p&gt;

&lt;p&gt;The failure happens at &lt;code&gt;class-gs-service.php(182)&lt;/code&gt; inside the &lt;code&gt;auth()&lt;/code&gt; method. That method calls Google's OAuth token validation endpoint. The token it holds is being rejected by Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Causes of "Invalid OAuth2 Access Token"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cause 1: Google OAuth Token Expired (Most Common)
&lt;/h3&gt;

&lt;p&gt;Google OAuth access tokens issued through the standard authorization flow expire. When GSheetConnector connects to Google, it stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;access token&lt;/strong&gt; (short-lived, expires in 1 hour)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;refresh token&lt;/strong&gt; (long-lived, used to get new access tokens)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin should automatically use the refresh token to get a new access token before each Sheets write. If the refresh token itself has expired or been revoked, no new access token can be generated and every write fails with this error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When do refresh tokens expire or get revoked?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google revokes refresh tokens after &lt;strong&gt;7 days&lt;/strong&gt; if the OAuth app is in "Testing" status in Google Cloud Console (not published/verified)&lt;/li&gt;
&lt;li&gt;Refresh tokens are revoked when the user changes their Google account password&lt;/li&gt;
&lt;li&gt;Refresh tokens are revoked when the user revokes app access in their Google Account &amp;gt; Security &amp;gt; Third-party apps&lt;/li&gt;
&lt;li&gt;Google revokes tokens for apps that have been inactive for 6 months&lt;/li&gt;
&lt;li&gt;Each user account is limited to 50 active refresh tokens per app. The 51st token revokes the oldest one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 7-day expiry is the most common cause&lt;/strong&gt; for developers using GSheetConnector. If you connected successfully, it worked for a week, then broke, this is almost certainly what happened. Your app is in Testing status in Google Cloud Console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Publish your OAuth app in Google Cloud Console (change from Testing to Production), then reconnect the plugin. Or reconnect every 7 days if you intentionally keep it in Testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause 2: Google Client Library Version Conflict
&lt;/h3&gt;

&lt;p&gt;This is what the plugin author pointed to. The GSheetConnector plugin bundles a version of Google's PHP client library (&lt;code&gt;google/apiclient&lt;/code&gt;). Other plugins that integrate with Google services (Google Analytics, Google Ads, Google Tag Manager plugins, WooCommerce Google integrations) may bundle a different version of the same library.&lt;/p&gt;

&lt;p&gt;When two plugins load different versions of &lt;code&gt;Google_Client&lt;/code&gt;, &lt;code&gt;Google_Service_Sheets&lt;/code&gt;, or related classes, PHP class conflicts occur. Depending on which plugin loads first, the wrong version of the auth class runs. The token validation logic in the wrong version may reject tokens it should accept, or fail silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deactivate all other Google-related plugins one by one. If the error disappears after deactivating a specific plugin, that is the conflicting one.&lt;/p&gt;

&lt;p&gt;You can also check for class conflicts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add temporarily to functions.php&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'plugins_loaded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;class_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Google_Client'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$reflection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReflectionClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Google_Client'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Google_Client loaded from: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$reflection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFileName&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the path shown does not point to GSheetConnector's directory, another plugin loaded its version first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use a plugin that handles Google library loading with proper namespacing or version isolation. Or use &lt;a href="https://www.contactformtoapi.com/contact-form-7-third-party-integration-guide/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; to send CF7 data to Google Sheets via the Sheets API directly with a Bearer token, bypassing the shared library problem entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause 3: Wrong Google Account or Revoked Access
&lt;/h3&gt;

&lt;p&gt;If the Google account used to authorize GSheetConnector has had its access revoked (either manually by the user or automatically by Google), the stored tokens are invalid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common scenarios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Google account used for authorization belongs to a team member who left the organization and had their account suspended&lt;/li&gt;
&lt;li&gt;The Google Workspace admin revoked third-party app access across the organization&lt;/li&gt;
&lt;li&gt;The specific Google Sheet was moved to a different Drive account that the authorized user cannot access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Reconnect the plugin using the correct Google account that has access to the target spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the Token State Without the Plugin
&lt;/h2&gt;

&lt;p&gt;You can test whether a stored token is valid directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test if an access token is valid&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=YOUR_ACCESS_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid token returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issued_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-client-id.apps.googleusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audience"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-client-id.apps.googleusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.googleapis.com/auth/spreadsheets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"access_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"online"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An invalid or expired token returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Token has been expired or revoked."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get &lt;code&gt;invalid_token&lt;/code&gt; here, the stored access token is genuinely expired. The question is whether the refresh token is also expired. If it is, reconnecting the plugin from scratch is the only fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Google Cloud Console OAuth App Status
&lt;/h2&gt;

&lt;p&gt;This is the step most developers miss. Go to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Select your project&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;APIs and Services &amp;gt; OAuth consent screen&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;Publishing status&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it says &lt;strong&gt;Testing&lt;/strong&gt;, your refresh tokens expire after 7 days and only test users you have explicitly added can authorize the app. Change to &lt;strong&gt;Production&lt;/strong&gt; (you do not need Google verification for internal tools used by your own account) to get non-expiring refresh tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Direct API Alternative
&lt;/h2&gt;

&lt;p&gt;GSheetConnector's OAuth dependency is the source of all three failure modes above. The plugin needs a valid access token every time a form submits. If the token lifecycle breaks for any reason, all Sheets writes fail silently (or with this error logged to PHP error logs that most site owners never check).&lt;/p&gt;

&lt;p&gt;An alternative is to connect CF7 directly to the Google Sheets API using a &lt;strong&gt;Service Account&lt;/strong&gt; instead of OAuth user authorization. Service accounts use a JSON key file for authentication and do not have token expiry issues in the same way.&lt;/p&gt;

&lt;p&gt;The Google Sheets API endpoint for appending a row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{range}:append
Authorization: Bearer SERVICE_ACCOUNT_ACCESS_TOKEN
Content-Type: application/json

{
  "values": [["John Smith", "john@example.com", "2024-01-15"]]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-third-party-integration-guide/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; can send CF7 form data to this endpoint directly, with the service account token handled separately from the plugin's OAuth flow. This removes the shared Google library dependency and the 7-day token expiry problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosis Checklist
&lt;/h2&gt;

&lt;p&gt;Work through these in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check Google Cloud Console OAuth consent screen — is the app in &lt;strong&gt;Testing&lt;/strong&gt; status?&lt;/li&gt;
&lt;li&gt;Has it been more than 7 days since you last connected the plugin?&lt;/li&gt;
&lt;li&gt;Did the Google account owner change their password recently?&lt;/li&gt;
&lt;li&gt;Are there other Google-related plugins active? Deactivate them one by one.&lt;/li&gt;
&lt;li&gt;Does the authorized Google account still have access to the target spreadsheet?&lt;/li&gt;
&lt;li&gt;Test the access token directly with the &lt;code&gt;tokeninfo&lt;/code&gt; endpoint above&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>wordpress</category>
      <category>googlesheets</category>
      <category>oauth</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why CF7 Plugin Settings Stop Saving After a CF7 Update (And How to Fix It)</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Wed, 10 Jun 2026 08:12:00 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/why-cf7-plugin-settings-stop-saving-after-a-cf7-update-and-how-to-fix-it-2h7i</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/why-cf7-plugin-settings-stop-saving-after-a-cf7-update-and-how-to-fix-it-2h7i</guid>
      <description>&lt;p&gt;A developer posted on the WordPress support forums:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The API settings via the plugin cannot be saved. Whenever I click Save after completing the form, the text fields are empty again."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another user confirmed the same issue and traced it to a CF7 version update:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm noticing the same issue for sites that updated Contact Form 7 to version 5.5.3. Rolling back to 5.5.2 fixed it for me."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thread closed. Not resolved.&lt;/p&gt;

&lt;p&gt;Rolling back is not a fix. It is a temporary workaround that leaves your site running an outdated plugin version indefinitely. The actual problem is a CF7 architectural change in 5.5.x that broke how certain third-party plugins registered and saved their settings. Understanding what changed, why it breaks settings saving, and how to build integrations that survive CF7 version bumps is what this post covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed in CF7 5.5.x
&lt;/h2&gt;

&lt;p&gt;CF7 version 5.5 introduced significant internal changes to how the plugin manages its admin pages and settings. Specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Menu registration changed.&lt;/strong&gt; CF7 moved from using &lt;code&gt;add_menu_page()&lt;/code&gt; with its own top-level admin menu to a restructured settings architecture. Third-party plugins that hooked into CF7's admin menu using the old approach found their settings pages either disappearing or failing to process &lt;code&gt;$_POST&lt;/code&gt; correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nonce handling tightened.&lt;/strong&gt; CF7 5.5.x tightened nonce verification on its settings save handlers. Third-party plugins that piggy-backed on CF7's &lt;code&gt;options.php&lt;/code&gt; flow without registering their own nonces correctly started failing silently on save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings API registration assumptions broke.&lt;/strong&gt; Some CF7 add-on plugins assumed CF7's internal action names and page slugs were stable. When CF7 renamed internal hooks and page identifiers in 5.5, any plugin that referenced those by name stopped working.&lt;/p&gt;

&lt;p&gt;The result: you click Save, the page reloads, and the fields are empty. No error message. No debug log entry. The settings were never written to the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Settings Appear to Save But Do Not
&lt;/h2&gt;

&lt;p&gt;When a WordPress settings form submits and the data does not persist, one of four things is happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Nonce verification failed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress settings forms include a nonce field for CSRF protection. If the nonce is invalid, expired, or missing, WordPress rejects the &lt;code&gt;$_POST&lt;/code&gt; data silently and redirects back to the settings page. The fields appear empty because the data was never saved.&lt;/p&gt;

&lt;p&gt;Check by temporarily adding to &lt;code&gt;wp-config.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_DEBUG_LOG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then save the settings and check &lt;code&gt;wp-content/debug.log&lt;/code&gt; for nonce-related warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The settings were not registered with &lt;code&gt;register_setting()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress's Settings API requires that any option saved via &lt;code&gt;options.php&lt;/code&gt; is registered with &lt;code&gt;register_setting($option_group, $option_name)&lt;/code&gt;. If a plugin saves settings through &lt;code&gt;options.php&lt;/code&gt; but did not call &lt;code&gt;register_setting()&lt;/code&gt;, WordPress 4.9+ silently discards the unregistered options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The form &lt;code&gt;action&lt;/code&gt; attribute points to the wrong handler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a plugin's settings form submits to &lt;code&gt;options.php&lt;/code&gt; but the plugin's &lt;code&gt;$option_group&lt;/code&gt; does not match what &lt;code&gt;settings_fields()&lt;/code&gt; outputs, the nonce check fails and nothing saves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A PHP error occurs during the save handler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the plugin's &lt;code&gt;register_setting()&lt;/code&gt; sanitize callback throws a PHP error or warning, WordPress may abort the save process mid-execution. With error display off (production sites), this looks identical to a nonce failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct Pattern for CF7 Add-On Settings That Survive Version Changes
&lt;/h2&gt;

&lt;p&gt;The mistake most CF7 add-on plugins make is coupling their settings registration to CF7's internal page hooks. When CF7 changes those hooks, the add-on breaks.&lt;/p&gt;

&lt;p&gt;The correct pattern is to register settings completely independently of CF7's internals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// 1. Register your own settings page independently
add_action('admin_menu', 'myplugin_register_settings_page');

function myplugin_register_settings_page() {
    add_options_page(
        'My CF7 Integration Settings',
        'CF7 Integration',
        'manage_options',
        'myplugin-cf7-settings',
        'myplugin_render_settings_page'
    );
}

// 2. Register your options with the Settings API
add_action('admin_init', 'myplugin_register_settings');

function myplugin_register_settings() {
    register_setting(
        'myplugin_cf7_settings_group',   // option group
        'myplugin_api_endpoint',          // option name
        [
            'type'              =&amp;gt; 'string',
            'sanitize_callback' =&amp;gt; 'esc_url_raw',
            'default'           =&amp;gt; '',
        ]
    );

    register_setting(
        'myplugin_cf7_settings_group',
        'myplugin_api_username',
        [
            'type'              =&amp;gt; 'string',
            'sanitize_callback' =&amp;gt; 'sanitize_text_field',
            'default'           =&amp;gt; '',
        ]
    );

    register_setting(
        'myplugin_cf7_settings_group',
        'myplugin_api_password',
        [
            'type'              =&amp;gt; 'string',
            'sanitize_callback' =&amp;gt; 'sanitize_text_field',
            'default'           =&amp;gt; '',
        ]
    );
}

// 3. Render the settings form correctly
function myplugin_render_settings_page() {
    if (!current_user_can('manage_options')) {
        wp_die('Insufficient permissions');
    }
    ?&amp;gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;CF7 Integration Settings&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"options.php"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;?php
            // This outputs the nonce, action, and option_page fields correctly
            settings_fields('myplugin_cf7_settings_group');
            ?&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-table"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;API Endpoint&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;
                               &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myplugin_api_endpoint"&lt;/span&gt;
                               &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr(get_option('myplugin_api_endpoint')); ?&amp;gt;"&lt;/span&gt;
                               &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"regular-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Username&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                               &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myplugin_api_username"&lt;/span&gt;
                               &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr(get_option('myplugin_api_username')); ?&amp;gt;"&lt;/span&gt;
                               &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"regular-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Password / API Key&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
                               &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myplugin_api_password"&lt;/span&gt;
                               &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr(get_option('myplugin_api_password')); ?&amp;gt;"&lt;/span&gt;
                               &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"regular-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;?php submit_button(); ?&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach has zero dependency on CF7's internal page hooks, menu structure, or version-specific action names. If CF7 completely rewrites its admin in a future version, these settings still save correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing API Credentials: &lt;code&gt;get_option&lt;/code&gt; vs &lt;code&gt;wp-config.php&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Storing API usernames and passwords in the WordPress database via &lt;code&gt;get_option&lt;/code&gt; is convenient but not ideal for credentials. The database is accessible to any code running on your WordPress installation, including other plugins.&lt;/p&gt;

&lt;p&gt;For better security, store credentials in &lt;code&gt;wp-config.php&lt;/code&gt; as constants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wp-config.php&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_ENDPOINT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://api.example.com/v1/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-username'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-api-key'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_ENDPOINT'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CF7_API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin_api_endpoint'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CF7_API_USERNAME&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin_api_username'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CF7_API_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin_api_password'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern lets you use &lt;code&gt;wp-config.php&lt;/code&gt; constants on servers where you control the environment (staging, production) and fall back to the database UI for development environments where constants are not defined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Auth: What It Is and When It Is Acceptable
&lt;/h2&gt;

&lt;p&gt;The plugin in this thread is named "CF7 to API + Basic Auth," so it is worth being precise about what Basic Auth is and its limitations.&lt;/p&gt;

&lt;p&gt;Basic Auth sends credentials as a Base64-encoded string in the Authorization header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That encoded string is &lt;code&gt;username:password&lt;/code&gt; in Base64. It is trivially decodable. Base64 is encoding, not encryption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Auth is acceptable when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connection is over HTTPS (TLS encrypts the header in transit)&lt;/li&gt;
&lt;li&gt;The API has no more secure auth option available&lt;/li&gt;
&lt;li&gt;The credentials have minimal scope (read-only or limited write access)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Basic Auth is not acceptable when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connection is over plain HTTP (credentials are visible to any network observer)&lt;/li&gt;
&lt;li&gt;The API supports OAuth 2.0 or API key-based auth (use those instead)&lt;/li&gt;
&lt;li&gt;The credentials are high-privilege account credentials rather than scoped API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CF7 integrations sending form data to an external API, Bearer token auth is preferable to Basic Auth when the API supports it. Bearer tokens can be scoped, rotated, and revoked without changing account passwords.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-api-authentication-error/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; supports both Basic Auth and Bearer token authentication, and stores credentials in a way that survives CF7 version updates without breaking the settings page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing Settings That Do Not Save
&lt;/h2&gt;

&lt;p&gt;Run this checklist before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add temporarily to functions.php to debug settings saves&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'updated_option'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$old_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$new_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'myplugin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Option updated: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$option_name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' = '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$new_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'update_option'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$old_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$new_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'myplugin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Attempting to update: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;update_option&lt;/code&gt; fires but &lt;code&gt;updated_option&lt;/code&gt; does not, the value being saved matches the existing value (no change, so WordPress skips the write). If neither fires, the &lt;code&gt;$_POST&lt;/code&gt; data never reached the options handler, which points to a nonce failure or unregistered option.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix vs Rolling Back
&lt;/h2&gt;

&lt;p&gt;Rolling back CF7 to 5.5.2 stops the breakage temporarily. It does not fix the underlying issue and leaves you running an outdated plugin with unpatched security issues.&lt;/p&gt;

&lt;p&gt;The real fix is either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update the broken add-on plugin to use CF7-version-agnostic settings registration (as shown above)&lt;/li&gt;
&lt;li&gt;Replace the broken plugin with one that does not rely on CF7's internal admin architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-api-authentication-error/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; registers its settings independently of CF7's internals and has maintained compatibility across CF7 major versions without requiring rollbacks.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why CF7 Plugin Settings Stop Saving After a CF7 Update (And How to Fix It)</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Mon, 08 Jun 2026 05:34:21 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/why-cf7-plugin-settings-stop-saving-after-a-cf7-update-and-how-to-fix-it-1don</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/why-cf7-plugin-settings-stop-saving-after-a-cf7-update-and-how-to-fix-it-1don</guid>
      <description>&lt;p&gt;A developer posted on the WordPress support forums:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The API settings via the plugin cannot be saved. Whenever I click Save after completing the form, the text fields are empty again."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another user confirmed the same issue and traced it to a CF7 version update:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm noticing the same issue for sites that updated Contact Form 7 to version 5.5.3. Rolling back to 5.5.2 fixed it for me."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thread closed. Not resolved.&lt;/p&gt;

&lt;p&gt;Rolling back is not a fix. It is a temporary workaround that leaves your site running an outdated plugin version indefinitely. The actual problem is a CF7 architectural change in 5.5.x that broke how certain third-party plugins registered and saved their settings. Understanding what changed, why it breaks settings saving, and how to build integrations that survive CF7 version bumps is what this post covers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed in CF7 5.5.x
&lt;/h2&gt;

&lt;p&gt;CF7 version 5.5 introduced significant internal changes to how the plugin manages its admin pages and settings. Specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Menu registration changed.&lt;/strong&gt; CF7 moved from using &lt;code&gt;add_menu_page()&lt;/code&gt; with its own top-level admin menu to a restructured settings architecture. Third-party plugins that hooked into CF7's admin menu using the old approach found their settings pages either disappearing or failing to process &lt;code&gt;$_POST&lt;/code&gt; correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nonce handling tightened.&lt;/strong&gt; CF7 5.5.x tightened nonce verification on its settings save handlers. Third-party plugins that piggy-backed on CF7's &lt;code&gt;options.php&lt;/code&gt; flow without registering their own nonces correctly started failing silently on save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings API registration assumptions broke.&lt;/strong&gt; Some CF7 add-on plugins assumed CF7's internal action names and page slugs were stable. When CF7 renamed internal hooks and page identifiers in 5.5, any plugin that referenced those by name stopped working.&lt;/p&gt;

&lt;p&gt;The result: you click Save, the page reloads, and the fields are empty. No error message. No debug log entry. The settings were never written to the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Settings Appear to Save But Do Not
&lt;/h2&gt;

&lt;p&gt;When a WordPress settings form submits and the data does not persist, one of four things is happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Nonce verification failed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress settings forms include a nonce field for CSRF protection. If the nonce is invalid, expired, or missing, WordPress rejects the &lt;code&gt;$_POST&lt;/code&gt; data silently and redirects back to the settings page. The fields appear empty because the data was never saved.&lt;/p&gt;

&lt;p&gt;Check by temporarily adding to &lt;code&gt;wp-config.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_DEBUG_LOG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then save the settings and check &lt;code&gt;wp-content/debug.log&lt;/code&gt; for nonce-related warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The settings were not registered with &lt;code&gt;register_setting()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress's Settings API requires that any option saved via &lt;code&gt;options.php&lt;/code&gt; is registered with &lt;code&gt;register_setting($option_group, $option_name)&lt;/code&gt;. If a plugin saves settings through &lt;code&gt;options.php&lt;/code&gt; but did not call &lt;code&gt;register_setting()&lt;/code&gt;, WordPress 4.9+ silently discards the unregistered options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The form &lt;code&gt;action&lt;/code&gt; attribute points to the wrong handler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a plugin's settings form submits to &lt;code&gt;options.php&lt;/code&gt; but the plugin's &lt;code&gt;$option_group&lt;/code&gt; does not match what &lt;code&gt;settings_fields()&lt;/code&gt; outputs, the nonce check fails and nothing saves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A PHP error occurs during the save handler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the plugin's &lt;code&gt;register_setting()&lt;/code&gt; sanitize callback throws a PHP error or warning, WordPress may abort the save process mid-execution. With error display off (production sites), this looks identical to a nonce failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct Pattern for CF7 Add-On Settings That Survive Version Changes
&lt;/h2&gt;

&lt;p&gt;The mistake most CF7 add-on plugins make is coupling their settings registration to CF7's internal page hooks. When CF7 changes those hooks, the add-on breaks.&lt;/p&gt;

&lt;p&gt;The correct pattern is to register settings completely independently of CF7's internals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// 1. Register your own settings page independently
add_action('admin_menu', 'myplugin_register_settings_page');

function myplugin_register_settings_page() {
    add_options_page(
        'My CF7 Integration Settings',
        'CF7 Integration',
        'manage_options',
        'myplugin-cf7-settings',
        'myplugin_render_settings_page'
    );
}

// 2. Register your options with the Settings API
add_action('admin_init', 'myplugin_register_settings');

function myplugin_register_settings() {
    register_setting(
        'myplugin_cf7_settings_group',   // option group
        'myplugin_api_endpoint',          // option name
        [
            'type'              =&amp;gt; 'string',
            'sanitize_callback' =&amp;gt; 'esc_url_raw',
            'default'           =&amp;gt; '',
        ]
    );

    register_setting(
        'myplugin_cf7_settings_group',
        'myplugin_api_username',
        [
            'type'              =&amp;gt; 'string',
            'sanitize_callback' =&amp;gt; 'sanitize_text_field',
            'default'           =&amp;gt; '',
        ]
    );

    register_setting(
        'myplugin_cf7_settings_group',
        'myplugin_api_password',
        [
            'type'              =&amp;gt; 'string',
            'sanitize_callback' =&amp;gt; 'sanitize_text_field',
            'default'           =&amp;gt; '',
        ]
    );
}

// 3. Render the settings form correctly
function myplugin_render_settings_page() {
    if (!current_user_can('manage_options')) {
        wp_die('Insufficient permissions');
    }
    ?&amp;gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;CF7 Integration Settings&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"options.php"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;?php
            // This outputs the nonce, action, and option_page fields correctly
            settings_fields('myplugin_cf7_settings_group');
            ?&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-table"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;API Endpoint&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;
                               &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myplugin_api_endpoint"&lt;/span&gt;
                               &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr(get_option('myplugin_api_endpoint')); ?&amp;gt;"&lt;/span&gt;
                               &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"regular-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Username&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                               &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myplugin_api_username"&lt;/span&gt;
                               &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr(get_option('myplugin_api_username')); ?&amp;gt;"&lt;/span&gt;
                               &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"regular-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;Password / API Key&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
                               &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myplugin_api_password"&lt;/span&gt;
                               &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr(get_option('myplugin_api_password')); ?&amp;gt;"&lt;/span&gt;
                               &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"regular-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;?php submit_button(); ?&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach has zero dependency on CF7's internal page hooks, menu structure, or version-specific action names. If CF7 completely rewrites its admin in a future version, these settings still save correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing API Credentials: &lt;code&gt;get_option&lt;/code&gt; vs &lt;code&gt;wp-config.php&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Storing API usernames and passwords in the WordPress database via &lt;code&gt;get_option&lt;/code&gt; is convenient but not ideal for credentials. The database is accessible to any code running on your WordPress installation, including other plugins.&lt;/p&gt;

&lt;p&gt;For better security, store credentials in &lt;code&gt;wp-config.php&lt;/code&gt; as constants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wp-config.php&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_ENDPOINT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://api.example.com/v1/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-username'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-api-key'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_ENDPOINT'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CF7_API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin_api_endpoint'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CF7_API_USERNAME&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin_api_username'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CF7_API_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CF7_API_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'myplugin_api_password'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern lets you use &lt;code&gt;wp-config.php&lt;/code&gt; constants on servers where you control the environment (staging, production) and fall back to the database UI for development environments where constants are not defined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Auth: What It Is and When It Is Acceptable
&lt;/h2&gt;

&lt;p&gt;The plugin in this thread is named "CF7 to API + Basic Auth," so it is worth being precise about what Basic Auth is and its limitations.&lt;/p&gt;

&lt;p&gt;Basic Auth sends credentials as a Base64-encoded string in the Authorization header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That encoded string is &lt;code&gt;username:password&lt;/code&gt; in Base64. It is trivially decodable. Base64 is encoding, not encryption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Auth is acceptable when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connection is over HTTPS (TLS encrypts the header in transit)&lt;/li&gt;
&lt;li&gt;The API has no more secure auth option available&lt;/li&gt;
&lt;li&gt;The credentials have minimal scope (read-only or limited write access)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Basic Auth is not acceptable when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connection is over plain HTTP (credentials are visible to any network observer)&lt;/li&gt;
&lt;li&gt;The API supports OAuth 2.0 or API key-based auth (use those instead)&lt;/li&gt;
&lt;li&gt;The credentials are high-privilege account credentials rather than scoped API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CF7 integrations sending form data to an external API, Bearer token auth is preferable to Basic Auth when the API supports it. Bearer tokens can be scoped, rotated, and revoked without changing account passwords.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-api-authentication-error/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; supports both Basic Auth and Bearer token authentication, and stores credentials in a way that survives CF7 version updates without breaking the settings page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing Settings That Do Not Save
&lt;/h2&gt;

&lt;p&gt;Run this checklist before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add temporarily to functions.php to debug settings saves&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'updated_option'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$old_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$new_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'myplugin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Option updated: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$option_name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' = '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$new_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'update_option'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$old_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$new_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'myplugin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Attempting to update: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$option_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;update_option&lt;/code&gt; fires but &lt;code&gt;updated_option&lt;/code&gt; does not, the value being saved matches the existing value (no change, so WordPress skips the write). If neither fires, the &lt;code&gt;$_POST&lt;/code&gt; data never reached the options handler, which points to a nonce failure or unregistered option.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix vs Rolling Back
&lt;/h2&gt;

&lt;p&gt;Rolling back CF7 to 5.5.2 stops the breakage temporarily. It does not fix the underlying issue and leaves you running an outdated plugin with unpatched security issues.&lt;/p&gt;

&lt;p&gt;The real fix is either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update the broken add-on plugin to use CF7-version-agnostic settings registration (as shown above)&lt;/li&gt;
&lt;li&gt;Replace the broken plugin with one that does not rely on CF7's internal admin architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/contact-form-7-api-authentication-error/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; registers its settings independently of CF7's internals and has maintained compatibility across CF7 major versions without requiring rollbacks.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Trigger a Mailchimp Drip Sequence from a CF7 Form (Without a Checkbox)</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Fri, 05 Jun 2026 13:06:24 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/how-to-trigger-a-mailchimp-drip-sequence-from-a-cf7-form-without-a-checkbox-44na</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/how-to-trigger-a-mailchimp-drip-sequence-from-a-cf7-form-without-a-checkbox-44na</guid>
      <description>&lt;p&gt;A developer posted this on the WordPress support forums:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We'd like to send the auto-reply email after a visitor submits a CF7 form directly from Mailchimp. We have a drip campaign set up and a dedicated Mailchimp list. Is it possible to sign up everyone that submits a contact form to this list without displaying a checkbox?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reply was a link to a code snippet for setting a Mailchimp tag on CF7 submission. One sentence. Thread closed.&lt;/p&gt;

&lt;p&gt;That answer is technically correct but skips everything that matters: how Mailchimp's automation system actually works, what the consent implications of silent opt-in are, and how to build the full CF7 to Mailchimp automation flow properly.&lt;/p&gt;

&lt;p&gt;This post covers all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Developer Is Actually Trying to Build
&lt;/h2&gt;

&lt;p&gt;The goal is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visitor submits CF7 contact form&lt;/li&gt;
&lt;li&gt;Visitor is added to a Mailchimp audience silently (no checkbox)&lt;/li&gt;
&lt;li&gt;A tag is applied to that contact&lt;/li&gt;
&lt;li&gt;A Mailchimp automation triggers based on that tag&lt;/li&gt;
&lt;li&gt;The automation sends a sequence of emails (drip campaign)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a legitimate use case. It is used for post-inquiry follow-up sequences where the "subscription" is incidental to the contact request, not a marketing list signup.&lt;/p&gt;

&lt;p&gt;Before building it, the consent question matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Consent Question: Can You Silent-Subscribe Someone?
&lt;/h2&gt;

&lt;p&gt;Mailchimp's terms of service require that contacts added to an audience have given consent to receive marketing emails. Adding someone to a Mailchimp audience without their knowledge and then sending them a marketing drip sequence is a violation of Mailchimp's terms and, depending on jurisdiction, of GDPR, CAN-SPAM, or CASL.&lt;/p&gt;

&lt;p&gt;However, the developer's use case is transactional auto-replies, not marketing. This is a meaningful distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transactional emails&lt;/strong&gt; are messages sent in direct response to an action the user took (submitting a form, making a purchase, requesting information). These do not require marketing consent under most regulations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marketing emails&lt;/strong&gt; are messages sent to promote products, services, or content. These require explicit consent.&lt;/p&gt;

&lt;p&gt;If your Mailchimp drip sequence is genuinely transactional (confirmation of their inquiry, information they requested, follow-up on their specific request), the consent requirement is lower. If it promotes your services beyond what was requested, it is marketing and requires consent.&lt;/p&gt;

&lt;p&gt;Mailchimp itself does not distinguish transactional from marketing in its standard Audiences. For genuine transactional email, Mailchimp Transactional (formerly Mandrill) is the correct product. For a marketing automation triggered by a form submission with implied consent, a clear notice in your form's privacy policy link is the minimum acceptable approach.&lt;/p&gt;

&lt;p&gt;For this post, we will assume the use case is legitimate auto-replies where the contact has reasonable expectation of receiving follow-up communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Mailchimp Automations and Tags Work
&lt;/h2&gt;

&lt;p&gt;Understanding the Mailchimp data model is required before writing any code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audience (formerly List):&lt;/strong&gt; The top-level container for contacts in Mailchimp. Every contact belongs to one or more audiences. You pay per contact per audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag:&lt;/strong&gt; A label applied to a contact within an audience. Tags do not add costs. A contact can have multiple tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automation (Customer Journey):&lt;/strong&gt; A sequence of emails and actions triggered by a specific event. The event that triggers the developer's drip sequence is "contact is added to audience" or "tag is applied to contact."&lt;/p&gt;

&lt;p&gt;The correct architecture for this use case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CF7 submission
    |
    v
Add contact to Mailchimp audience via API
    |
    v
Apply specific tag (e.g. "contact-form-inquiry")
    |
    v
Mailchimp automation triggers on tag applied
    |
    v
Drip email 1 sent immediately
    |
    v (7 days later)
Drip email 2 sent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tag is the trigger, not the audience membership alone. This lets you have one audience with multiple automations triggered by different tags from different forms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation: Three Approaches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Approach 1: MC4WP Plugin + Code Snippet (What the Forum Suggested)
&lt;/h3&gt;

&lt;p&gt;The MC4WP plugin connects CF7 to Mailchimp. By default it shows a checkbox. To remove the checkbox and subscribe silently, add a tag based on which form was submitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add to functions.php&lt;/span&gt;
&lt;span class="c1"&gt;// Source: ibericode/mailchimp-for-wordpress sample snippets&lt;/span&gt;

&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mc4wp_integration_cf7_tags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Apply different tags based on form ID&lt;/span&gt;
    &lt;span class="nv"&gt;$form_tag_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'contact-form-inquiry'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;    &lt;span class="c1"&gt;// replace with your form IDs&lt;/span&gt;
        &lt;span class="mi"&gt;456&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quote-request'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="mi"&gt;789&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'newsletter-signup'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nv"&gt;$form_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form_tag_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$form_tag_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$tags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For silent subscription (no checkbox), configure MC4WP's CF7 integration with "Implicit sign-up" enabled in the plugin settings. This subscribes the contact on every form submission without showing a UI element.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; MC4WP's implicit signup adds contacts to whichever audience you configure globally. If you need different audiences per form, you need the approach below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 2: Direct Mailchimp API Call from CF7 Hook
&lt;/h3&gt;

&lt;p&gt;This gives full control: which audience, which tags, which status, per form. No MC4WP required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wpcf7_before_send_mail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cf7_to_mailchimp_silent'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cf7_to_mailchimp_silent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$contact_form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$form_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$contact_form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Map form IDs to Mailchimp audience IDs and tags&lt;/span&gt;
    &lt;span class="nv"&gt;$form_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'audience_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'abc123def4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// your Mailchimp audience/list ID&lt;/span&gt;
            &lt;span class="s1"&gt;'tags'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'contact-form-inquiry'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="mi"&gt;456&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'audience_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'abc123def4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'tags'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quote-request'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$config&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$submission&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WPCF7_Submission&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_instance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$submission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$data&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$submission&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_posted_data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$email&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'your-email'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$name_parts&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sanitize_text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'your-name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$first_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$name_parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$last_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$name_parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$api_key&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAILCHIMP_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;MAILCHIMP_API_KEY&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$server&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "us21"&lt;/span&gt;
    &lt;span class="nv"&gt;$audience_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'audience_id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$member_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Add or update the contact (PUT is idempotent - updates if exists)&lt;/span&gt;
    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.api.mailchimp.com/3.0/lists/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$audience_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/members/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$member_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'method'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'PUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'Authorization'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Basic '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anystring:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$api_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'body'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'email_address'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'status_if_new'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'subscribed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// only sets status for NEW contacts&lt;/span&gt;
                &lt;span class="s1"&gt;'merge_fields'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'FNAME'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'LNAME'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[CF7-&amp;gt;Mailchimp] Member upsert failed: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_error_message&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$status_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_retrieve_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[CF7-&amp;gt;Mailchimp] API error '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$status_code&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;': '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_retrieve_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Apply tags separately (tag endpoint accepts array of tag operations)&lt;/span&gt;
    &lt;span class="nv"&gt;$tags_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nf"&gt;wp_remote_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.api.mailchimp.com/3.0/lists/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$audience_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/members/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$member_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/tags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'Authorization'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Basic '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anystring:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$api_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'body'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$tags_payload&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your API key in &lt;code&gt;wp-config.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MAILCHIMP_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-key-here-us21'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key decisions in this code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PUT&lt;/code&gt; to the member endpoint instead of &lt;code&gt;POST&lt;/code&gt;. PUT is idempotent: if the contact already exists, it updates them. POST on an existing contact returns a 400 error. Using PUT prevents duplicates.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status_if_new: subscribed&lt;/code&gt; sets the status only for brand-new contacts. It does not change an existing contact's subscription status (so someone who previously unsubscribed stays unsubscribed).&lt;/li&gt;
&lt;li&gt;Tags are applied in a separate API call via the &lt;code&gt;/tags&lt;/code&gt; endpoint because the main PUT endpoint does not accept tag modifications directly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;member_hash&lt;/code&gt; is &lt;code&gt;md5(lowercase email)&lt;/code&gt;, which is how Mailchimp identifies members in its API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Approach 3: Contact Form to API Plugin
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/mailchimp-integration-with-contact-form-to-any-api/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; handles the Mailchimp API call from a configuration UI. Set the Mailchimp audience endpoint, configure the Authorization header with your API key, and map CF7 fields to the Mailchimp payload. No PHP deployment required, and the field mapping updates happen in the WordPress dashboard when form fields change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Mailchimp Automation to Trigger on Tag
&lt;/h2&gt;

&lt;p&gt;Once contacts are being added and tagged, configure the Mailchimp automation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Automations &amp;gt; Customer Journeys&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Journey&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set the starting point to &lt;strong&gt;"Tag is added"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the tag you are applying from CF7 (e.g. &lt;code&gt;contact-form-inquiry&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Add your email steps with delays between them (e.g. immediate email, then one week later)&lt;/li&gt;
&lt;li&gt;Activate the journey&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When CF7 applies the tag to a new contact, Mailchimp's automation fires immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; the automation only fires for contacts who receive the tag while the journey is active. Contacts tagged before the journey was activated do not enter the journey retroactively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audience ID: Where to Find It
&lt;/h2&gt;

&lt;p&gt;The audience ID is not the same as the audience name. Find it at:&lt;/p&gt;

&lt;p&gt;Mailchimp &amp;gt; Audience &amp;gt; Manage Audience &amp;gt; Settings &amp;gt; Audience name and defaults&lt;/p&gt;

&lt;p&gt;The ID is shown in the URL and in the settings panel. It looks like &lt;code&gt;abc123def4&lt;/code&gt; (10 characters, alphanumeric).&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>mailchimp</category>
      <category>emailmarketing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Contact Form 7 Breaks on Static Sites (And What to Do About It)</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Thu, 04 Jun 2026 14:02:20 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/why-contact-form-7-breaks-on-static-sites-and-what-to-do-about-it-jg5</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/why-contact-form-7-breaks-on-static-sites-and-what-to-do-about-it-jg5</guid>
      <description>&lt;p&gt;A developer posted this on the WordPress support forums:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"When we deploy to our live instance, the form is not working and gives a JSON error. This path has a 404 not found error: &lt;code&gt;/wp-json/contact-form-7/v1/contact-forms/978/feedback/schema&lt;/code&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They were using Simply Static to export their WordPress site to static files and deploying the output to a live host. The plugin author's reply pointed to a CORS issue. The developer pushed back. The thread went unresolved for weeks before being closed with "please contact Pro support."&lt;/p&gt;

&lt;p&gt;The actual problem is more fundamental than CORS. Contact Form 7 is a dynamic plugin. It processes form submissions via a WordPress REST API endpoint. When you export a WordPress site to static HTML, that REST API does not exist on the static host. You cannot fix this with a CORS header. You need to rearchitect how the form submits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Actually Happening
&lt;/h2&gt;

&lt;p&gt;When a user fills out a CF7 form and clicks submit, CF7's JavaScript sends a POST request to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /wp-json/contact-form-7/v1/contact-forms/{form_id}/feedback
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint is registered by the CF7 plugin and handled by PHP running on your WordPress server. It validates the submission, sends the email, and returns a JSON response that CF7's JS uses to show the success or error message.&lt;/p&gt;

&lt;p&gt;On a static site deployment, there is no PHP. There is no WordPress. There is no REST API. The URL &lt;code&gt;/wp-json/contact-form-7/v1/contact-forms/978/feedback&lt;/code&gt; simply does not exist on the static host. The browser gets a 404.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/feedback/schema&lt;/code&gt; 404 the developer saw is CF7 pre-fetching the form's validation schema before submission. That 404 is a symptom, not the root cause. The root cause is that the entire CF7 PHP backend is missing on the static host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Specific Failures in This Thread
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure 1: REST API Endpoint Returns 404
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;/wp-json/contact-form-7/v1/contact-forms/978/feedback/schema
404 Not Found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No PHP runtime on the static host means no WordPress REST API. Every request to &lt;code&gt;/wp-json/&lt;/code&gt; returns 404. This cannot be fixed by configuring Simply Static differently because there is nothing to copy. The endpoint is not a file, it is a dynamic PHP route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 2: Plugin JS Asset Not Readable
&lt;/h3&gt;

&lt;p&gt;The developer found this in Simply Static's diagnostic output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/html/wp-content/plugins/simply-static-pro/assets/ssp-form-webhook-public.js
Status: Not readable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JavaScript file handles CF7's form submission flow on the static site. Simply Static Pro replaces CF7's default AJAX submission with a webhook-based flow specifically to work around the REST API problem. If this file has incorrect permissions (not world-readable), the static export cannot include it and the form falls back to the broken default behavior.&lt;/p&gt;

&lt;p&gt;Fix for this specific issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;644 /var/www/html/wp-content/plugins/simply-static-pro/assets/ssp-form-webhook-public.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then re-run the Simply Static export.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 3: CORS Error in Browser Console
&lt;/h3&gt;

&lt;p&gt;The plugin author diagnosed a CORS issue. This is partially correct but only as a secondary problem.&lt;/p&gt;

&lt;p&gt;If Simply Static Pro is configured to proxy CF7 submissions to a webhook on a different domain from the static site, the browser enforces CORS policy on that cross-origin request. The static site is on &lt;code&gt;static-domain.com&lt;/code&gt;, the webhook receiver is on &lt;code&gt;wp-domain.com&lt;/code&gt;, the browser blocks the request.&lt;/p&gt;

&lt;p&gt;The CORS error in the browser console looks like:&lt;/p&gt;

&lt;p&gt;Access to fetch at '&lt;a href="https://wp-domain.com/wp-json/contact-form-7/.." rel="noopener noreferrer"&gt;https://wp-domain.com/wp-json/contact-form-7/..&lt;/a&gt;.' &lt;br&gt;
from origin '&lt;a href="https://static-domain.com" rel="noopener noreferrer"&gt;https://static-domain.com&lt;/a&gt;' has been blocked by CORS policy: &lt;br&gt;
No 'Access-Control-Allow-Origin' header is present on the requested resource.&lt;/p&gt;

&lt;p&gt;To add CORS headers to your WordPress origin, add this to your &lt;code&gt;.htaccess&lt;/code&gt; on the WordPress server (not the static host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_headers.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Origin "https://static-domain.com"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Methods "POST, GET, OPTIONS"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Headers "Content-Type"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or programmatically in WordPress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;remove_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_pre_serve_request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rest_send_cors_headers'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_pre_serve_request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_http_origin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'https://static-domain.com'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Origin: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$origin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Methods: POST, GET, OPTIONS'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Headers: Content-Type'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only resolves CORS. It does not fix the 404 unless your WordPress installation is still running and accessible at the origin URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Architectures for CF7 on Static Sites
&lt;/h2&gt;

&lt;p&gt;There are two patterns that actually work. Everything else is a workaround for a mismatched architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Keep WordPress Running as the Form Backend
&lt;/h3&gt;

&lt;p&gt;Your static site is served from a CDN or static host. Your WordPress installation stays live at a separate URL (e.g., &lt;code&gt;api.yourdomain.com&lt;/code&gt; or a subdomain). CF7 submits to the live WordPress REST API on that subdomain.&lt;/p&gt;

&lt;p&gt;Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress must remain running and publicly accessible&lt;/li&gt;
&lt;li&gt;CORS headers must be configured on the WordPress origin to allow requests from your static domain&lt;/li&gt;
&lt;li&gt;The form action in the CF7 JS must point to the correct WordPress URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what Simply Static Pro's webhook configuration enables when working correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Replace CF7 with a Third-Party Form Endpoint
&lt;/h3&gt;

&lt;p&gt;Remove the CF7 REST API dependency entirely. Use a form backend service that accepts POST submissions directly from a static HTML form without requiring a PHP backend.&lt;/p&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Formspree, Netlify Forms, Getform (managed form endpoints)&lt;/li&gt;
&lt;li&gt;A webhook receiver you control (your own API, n8n, Make)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.contactformtoapi.com/contact-form-7-api-timeout/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; configured to forward submissions to an external endpoint before the static export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your form needs to reach a CRM or send an email, Contact Form to API can handle that forwarding from within WordPress during the submission event, before Simply Static exports the site. The static site only needs to trigger the submission. The WordPress plugin processes and forwards it while WordPress is still the active runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostic Steps for CF7 on a Static Site
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Confirm whether your WordPress backend is still running&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open your browser and go to &lt;code&gt;https://yourwordpressurl.com/wp-json/&lt;/code&gt;. If you get a JSON object describing the REST API, WordPress is live. If you get a 404 or your static site's 404 page, the WordPress PHP backend is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Check the browser console on form submission&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open DevTools (F12), go to the Console tab, and submit the form. You will see one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;404 Not Found&lt;/code&gt; on the feedback endpoint - WordPress REST API is not reachable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CORS error&lt;/code&gt; - WordPress is reachable but blocking cross-origin requests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net::ERR_CONNECTION_REFUSED&lt;/code&gt; - WordPress server is offline or not accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Verify the forms.json configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Simply Static Pro stores form routing config at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;/wp-content/uploads/simply-static/configs/forms.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A correctly configured entry looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"978"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cf7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-wordpress-url.com/?mailme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redirect_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;endpoint&lt;/code&gt; points to your static domain instead of your WordPress domain, submissions will never reach the PHP backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Check JS asset file permissions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/www/html/wp-content/plugins/simply-static-pro/assets/ssp-form-webhook-public.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file needs to be readable by the web server user (typically &lt;code&gt;www-data&lt;/code&gt; on Ubuntu/Debian). If permissions are &lt;code&gt;600&lt;/code&gt; or owned by root, the file is not readable during the export process.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;CF7 on a static site breaks because CF7 needs PHP to process submissions. Static sites have no PHP. The 404 on &lt;code&gt;/wp-json/contact-form-7/v1/contact-forms/978/feedback/schema&lt;/code&gt; is not a file missing from the export. It is a REST API route that cannot exist without WordPress running.&lt;/p&gt;

&lt;p&gt;The CORS diagnosis was correct as a secondary issue but missed the primary one. You cannot CORS-header your way around a missing PHP backend.&lt;/p&gt;

&lt;p&gt;Fixes in order of reliability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep WordPress running at a subdomain as the form backend, configure CORS headers&lt;/li&gt;
&lt;li&gt;Fix the Simply Static Pro JS asset permissions and correct &lt;code&gt;forms.json&lt;/code&gt; endpoint URL&lt;/li&gt;
&lt;li&gt;Replace the CF7 REST API dependency with a direct external API using Contact Form to API before the static export&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>wordpress</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>CF7 Says "null" on Zoho Flow Connection: mod_security, REST API Locks, and Host Blocks Explained</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Fri, 29 May 2026 13:33:04 +0000</pubDate>
      <link>https://dev.to/rahul_sharma_15bd129bc69e/cf7-says-null-on-zoho-flow-connection-modsecurity-rest-api-locks-and-host-blocks-explained-adg</link>
      <guid>https://dev.to/rahul_sharma_15bd129bc69e/cf7-says-null-on-zoho-flow-connection-modsecurity-rest-api-locks-and-host-blocks-explained-adg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fif4s1r0n8xph548yz70t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fif4s1r0n8xph548yz70t.png" alt="CF7 Says "&gt;&lt;/a&gt;
"/&amp;gt;&lt;br&gt;
A developer posted this on the WordPress support forums:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Was able to create the API in WordPress, but the Zoho Flow app registration screen is getting Contact Form 7 says 'null' as an error."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Seven replies later, the resolution was: "The issue has been resolved, it was related to the hosting provider."&lt;/p&gt;

&lt;p&gt;That tells you nothing. Three completely different server-level problems were identified in that thread, each producing the same symptom: a null or broken response when Zoho Flow tries to read your CF7 forms during the connection setup. This post breaks down each one, how to identify which you are hitting, and how to fix it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What "null" Actually Means in This Context
&lt;/h2&gt;

&lt;p&gt;When Zoho Flow connects to your WordPress site, it calls a REST API endpoint registered by the Zoho Flow plugin to fetch a list of your CF7 forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /wp-json/zoho-flow/contact-form-7/v1/forms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If something intercepts that request before it reaches the plugin, Zoho Flow receives a malformed or empty response, which surfaces as "null" or a generic error in the Zoho Flow UI. The connection appears to fail, but the failure is happening at a layer completely below the plugin.&lt;/p&gt;

&lt;p&gt;Three things intercept this request in the thread:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;mod_security blocking the keyword "contact" in the API path&lt;/li&gt;
&lt;li&gt;A REST API lockdown plugin requiring authentication&lt;/li&gt;
&lt;li&gt;A hosting provider level block&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cause 1: mod_security Blocking "contact" in the API Path
&lt;/h2&gt;

&lt;p&gt;This was Zoho Flow's first diagnosis in the thread. The actual API response they received was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;humans_21909=1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a PHP error. It is a bot-challenge response injected by a Web Application Firewall (WAF), specifically Apache mod_security or a similar layer. The WAF detected a pattern in the request (the word "contact" in the path &lt;code&gt;/zoho-flow/contact-form-7/v1/forms&lt;/code&gt;) and flagged it as potentially malicious, returning a bot-challenge instead of passing the request through to WordPress.&lt;/p&gt;

&lt;p&gt;The Zoho Flow plugin never even receives this request. WordPress never sees it. The challenge script is the server's response, not WordPress's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why "contact" triggers it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;mod_security rule sets (particularly OWASP CRS) include rules that flag certain keywords in URL paths and query strings that are commonly associated with spam bots and form abuse. "contact-form" in an API path can trip these rules depending on the active ruleset version and configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm this is your issue:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check the response your server sends to that endpoint. From a terminal or Postman:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"https://yoursite.com/wp-json/zoho-flow/contact-form-7/v1/forms"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the response body contains &lt;code&gt;document.cookie&lt;/code&gt; or &lt;code&gt;document.location.reload&lt;/code&gt;, mod_security is intercepting the request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Option A: Ask your hosting provider to whitelist the path &lt;code&gt;/wp-json/zoho-flow/contact-form-7/&lt;/code&gt; in their mod_security configuration. This is a standard hosting support request on dedicated or VPS servers.&lt;/p&gt;

&lt;p&gt;Option B: If you have server access, add a mod_security exception in your &lt;code&gt;.htaccess&lt;/code&gt; or Apache virtual host config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;LocationMatch&lt;/span&gt;&lt;span class="sr"&gt; "/wp-json/zoho-flow/contact-form-7/"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    SecRuleEngine &lt;span class="ss"&gt;Off&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;LocationMatch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Nginx with ModSecurity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/wp-json/zoho-flow/contact-form-7/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;ModSecurityEnabled&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of your location block&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option C: Switch to a direct CF7-to-Zoho integration that does not register a REST API endpoint, removing the path that triggers mod_security entirely. Covered at the end of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: REST API Locked Down by a Security Plugin
&lt;/h2&gt;

&lt;p&gt;The developer in the thread then tested the endpoint in Postman and got a different error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rest_cannot_access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DRA: Only authenticated users can access the REST API."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prefix &lt;code&gt;DRA:&lt;/code&gt; identifies this as coming from the "Disable REST API" plugin (or a similar plugin such as "WP Hide &amp;amp; Security Enhancer"). This plugin blocks all unauthenticated REST API requests, returning a 401 before any registered route handler runs.&lt;/p&gt;

&lt;p&gt;When Zoho Flow tries to read your CF7 forms via the REST API, it is making an unauthenticated GET request. A REST API lockdown plugin blocks this before the Zoho Flow plugin ever sees it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look for these plugins in your WordPress admin under Plugins &amp;gt; Installed Plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disable REST API&lt;/li&gt;
&lt;li&gt;WP Hide and Security Enhancer&lt;/li&gt;
&lt;li&gt;Perfmatters (has a "Disable REST API" option)&lt;/li&gt;
&lt;li&gt;iThemes Security (has REST API restrictions)&lt;/li&gt;
&lt;li&gt;All In One WP Security (has REST API options)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check each one for a setting that restricts REST API access to logged-in users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Option A: Add the Zoho Flow route to the plugin's allowlist. Most REST API restriction plugins have a whitelist field where you can specify paths to exempt. Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wp-json/zoho-flow/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option B: Temporarily disable the REST API restriction plugin, complete the Zoho Flow connection setup, then re-enable it. This works if the restriction only affects the connection setup step and not the live webhook trigger path.&lt;/p&gt;

&lt;p&gt;Option C: Configure the restriction plugin to only block REST API access from the frontend (non-admin requests) rather than all unauthenticated requests globally. This is usually a setting in the plugin's options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: Hosting Provider Level Block
&lt;/h2&gt;

&lt;p&gt;The final resolution in the thread was "related to the hosting provider." This is the least diagnosable from your end because the block can happen at multiple layers that you do not control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared hosting WAF rules (different from mod_security, often proprietary)&lt;/li&gt;
&lt;li&gt;Load balancer or reverse proxy rules&lt;/li&gt;
&lt;li&gt;Cloudflare or similar CDN firewall rules&lt;/li&gt;
&lt;li&gt;Country-based IP filtering blocking Zoho Flow's server IPs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to identify this:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have ruled out mod_security and REST API restriction plugins, and the endpoint still returns a non-WordPress response, check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://yoursite.com/wp-json/zoho-flow/contact-form-7/v1/forms"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the response headers. If you see &lt;code&gt;cf-ray&lt;/code&gt; headers, the request is going through Cloudflare. A Cloudflare WAF rule may be blocking it. If you see non-standard server headers that do not match your known stack, a hosting-level proxy is involved.&lt;/p&gt;

&lt;p&gt;For Cloudflare specifically: check Security &amp;gt; WAF &amp;gt; Firewall Events in your Cloudflare dashboard for blocked requests to that path around the time of your test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Contact your hosting provider with the specific API path and response you are receiving. Give them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The endpoint: &lt;code&gt;/wp-json/zoho-flow/contact-form-7/v1/forms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The HTTP method: GET&lt;/li&gt;
&lt;li&gt;The response status code and body you receive&lt;/li&gt;
&lt;li&gt;The time of your test request (helps them check their firewall logs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Identifying Which Cause You Are Hitting
&lt;/h2&gt;

&lt;p&gt;Run this sequence before contacting anyone:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Test the endpoint directly with curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"https://yoursite.com/wp-json/zoho-flow/contact-form-7/v1/forms"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script&amp;gt;document.cookie...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mod_security WAF challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"code":"rest_cannot_access"...}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;REST API lockdown plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"code":"rest_no_route"...}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zoho Flow plugin not active or not registered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection timeout or refused&lt;/td&gt;
&lt;td&gt;Server or network level block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valid JSON array of forms&lt;/td&gt;
&lt;td&gt;Integration should work, problem is elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Check whether other WordPress REST API routes work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://yoursite.com/wp-json/wp/v2/posts"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this also returns a 401 or bot-challenge, the block is global (REST API restriction plugin or WAF). If this returns posts normally but the Zoho Flow route fails, the block is path-specific (mod_security keyword rule).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Test from a different IP or network. If it works from your local machine but fails when Zoho Flow's servers call it, the block is IP-based on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alternative: Skip the REST API Dependency Entirely
&lt;/h2&gt;

&lt;p&gt;All three causes in this thread exist because Zoho Flow's connection setup requires an unauthenticated REST API call to your WordPress site. Any server security layer that touches that request path can break it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.contactformtoapi.com/automate-wordpress-form-submissions/" rel="noopener noreferrer"&gt;Contact Form to API&lt;/a&gt; takes the opposite architectural approach: the plugin makes outbound requests from WordPress to your destination API (Zoho CRM, etc.) rather than requiring Zoho to make inbound requests to WordPress. This removes the inbound REST API dependency and the entire class of problems above. There is no API path for mod_security to block, no REST API lockdown plugin to route around, and no hosting provider firewall to negotiate with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;All three problems produce the same symptom: "null" or broken response during Zoho Flow connection setup. The diagnostic step that separates them is a direct curl call to the endpoint with verbose output.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Response you see&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mod_security keyword block&lt;/td&gt;
&lt;td&gt;HTML script bot-challenge&lt;/td&gt;
&lt;td&gt;Whitelist path in mod_security config or ask host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST API lockdown plugin&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rest_cannot_access&lt;/code&gt; 401 JSON&lt;/td&gt;
&lt;td&gt;Whitelist route in plugin settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting provider block&lt;/td&gt;
&lt;td&gt;Timeout, non-standard response, or CDN block&lt;/td&gt;
&lt;td&gt;Contact host with path and response details&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>wordpress</category>
      <category>zoho</category>
      <category>security</category>
      <category>api</category>
    </item>
  </channel>
</rss>
