Contact Form 7 Stripe Webhook Payment Status Failed: Fix Guide
Step-by-step fixes for the most common reasons your Stripe payments show “failed” status in Contact Form 7 — even when the customer was charged.
The most common cause is a missing or incorrect Stripe webhook endpoint in your dashboard. Go to Stripe Dashboard → Developers → Webhooks, add your site URL (https://yoursite.com/?wpcf7-stripe=callback), enable the payment_intent.succeeded event, and copy the Signing Secret into your CF7 Stripe plugin settings. That alone fixes roughly 70% of failed payment status issues.
You’ve set up Contact Form 7 with Stripe, everything looks fine on the surface — and then you get an email from a confused customer saying they were charged but nothing happened on your end. You check the form submission log and see “Payment Status: Failed.” That’s the webhook problem, and it’s more common than you’d think.
I’ve dealt with this three separate times across different WordPress installs — once on a client’s booking site, once on a digital product store, and once on my own membership form. The root causes are almost always the same handful of things. This guide covers all of them with exact steps to fix each one.
How the CF7 + Stripe Webhook Flow Works
Before jumping into fixes, here’s what’s supposed to happen when a customer pays through your form:
When the webhook fails at step 3 or 4, CF7 marks the payment as “Failed” regardless of what Stripe actually processed.
The critical thing to understand: CF7 doesn’t directly know whether Stripe charged the card. It only knows what the webhook tells it. If Stripe can’t reach your site, or your site rejects the webhook, CF7 logs “failed.” The money could still be sitting in your Stripe account.
All Common Causes at a Glance
| Cause | Frequency | Difficulty to Fix | Status |
|---|---|---|---|
| Webhook endpoint not registered in Stripe | Very Common | Easy | ❌ Breaks everything |
| Wrong Signing Secret in CF7 plugin settings | Very Common | Easy | ❌ 401 errors on webhook |
| SSL certificate issue (site not HTTPS) | Common | Medium | ❌ Stripe won’t send events |
| Firewall/security plugin blocking Stripe IP | Common | Medium | ⚠️ Intermittent failures |
| Wrong webhook event types selected | Moderate | Easy | ⚠️ Events received but ignored |
| Test mode / live mode key mismatch | Moderate | Easy | ❌ Auth errors |
| Server timeout on webhook response | Less Common | Medium | ⚠️ Stripe retries but CF7 loses sync |
| CF7 Stripe plugin version conflict | Less Common | Medium | ⚠️ Unpredictable behavior |
| Custom permalink structure breaking webhook URL | Rare | Easy | ❌ 404 on webhook endpoint |
Failure Rate by Cause (Based on Community Reports)
*Percentages reflect overlap — multiple causes can apply simultaneously.
Fix 1: Register the Correct Webhook Endpoint
This is where most people go wrong. The CF7 Stripe plugin expects Stripe to POST webhook events to a specific URL on your site. If that URL isn’t registered in your Stripe dashboard, Stripe never sends anything — and CF7 has nothing to work with.
In your WordPress admin, go to CF7 → Integration → Stripe. Your webhook endpoint URL is shown there — it typically looks like https://yoursite.com/?wpcf7-stripe=callback or a path-based version depending on your permalink structure.
Log into your Stripe account, click Developers in the top navigation, then select Webhooks from the left sidebar.
Paste your webhook URL. Under “Events to send,” add payment_intent.succeeded, payment_intent.payment_failed, and checkout.session.completed (add all three to be safe).
After saving the endpoint, click on it and reveal the Signing Secret (starts with whsec_). Copy the full string — you’ll need it in the next step.
Back in WordPress, go to CF7 → Integration → Stripe and paste the signing secret into the Webhook Secret field. Save.
In the Stripe webhook dashboard, use the “Send test webhook” button and send a payment_intent.succeeded event. Check whether your site returns a 200 response. If it does, you’re good.
Need a Stripe-compatible contact form solution that handles payments seamlessly?
Try CF7 Stripe Integration →Fix 2: Test Mode vs. Live Mode Key Mismatch
This one trips up developers constantly. You test with Stripe’s test mode, everything works — then you switch to live mode but forget to update the Signing Secret in the plugin settings. Test mode and live mode have completely separate webhook endpoints, separate API keys, and separate signing secrets. They don’t share anything.
| Setting | Test Mode Value Starts With | Live Mode Value Starts With |
|---|---|---|
| Publishable Key | pk_test_ |
pk_live_ |
| Secret Key | sk_test_ |
sk_live_ |
| Webhook Signing Secret | whsec_ (test endpoint) |
whsec_ (live endpoint) |
The signing secrets use the same whsec_ prefix regardless of mode, which is exactly why people mix them up. Make sure you’re copying the signing secret from the correct webhook endpoint — the live one if your site is live, the test one if you’re still testing.
If you have separate test and live webhook endpoints registered in Stripe, do not paste the test signing secret into a live form. The signature verification will fail every time, returning a 400 error and triggering the “Payment Status: Failed” state in CF7.
Fix 3: Security Plugin or Firewall Blocking Stripe
Wordfence, Sucuri, Cloudflare, and similar tools sometimes block incoming webhook POST requests from Stripe’s servers. To them, an external POST request to your site looks suspicious. The fix is to whitelist Stripe’s IP ranges.
Stripe’s Webhook IP Ranges (as of 2025)
Stripe sends webhooks from a specific set of IP addresses. You can find the current list at stripe.com/files/request-ip-addresses. Common ranges include:
For Wordfence Users
Look for any rules that might block POST requests from external IPs to your WordPress URL patterns.
Add Stripe’s IP ranges here. Changes take effect immediately.
For Cloudflare Users
If your site is behind Cloudflare, go to Security → WAF → Tools and create an IP Access Rule that allows Stripe’s IP ranges. Make sure “Under Attack” mode is off when testing — it aggressively challenges all incoming requests including legitimate webhooks.
Fix 4: Wrong Webhook Event Types
The CF7 Stripe plugin specifically listens for certain event types. If you registered the webhook endpoint but selected the wrong events, Stripe sends data that CF7 doesn’t know how to process — and logs the payment as failed.
| Event Type | Required? | What It Does in CF7 |
|---|---|---|
payment_intent.succeeded |
✅ Yes | Marks payment as succeeded, triggers form confirmation |
payment_intent.payment_failed |
✅ Yes | Logs the specific failure reason from Stripe |
checkout.session.completed |
⚠️ Depends on plugin version | Required if using Stripe Checkout flow instead of Elements |
charge.succeeded |
⚠️ Some older plugin versions | Legacy event — some older CF7 Stripe versions use this |
charge.failed |
⚠️ Optional | Provides failure details for declined cards |
When in doubt, select “Receive all events” in the Stripe webhook configuration. It adds minor overhead but ensures no relevant events are missed during troubleshooting. You can narrow it down later once payments work correctly.
Fix 5: SSL and HTTPS Issues
Stripe will not send webhooks to HTTP endpoints in live mode. Full stop. Your site must have a valid, trusted SSL certificate. Self-signed certificates don’t count — Stripe validates the SSL chain.
The errors you’ll see in Stripe’s webhook log when this is the issue:
Fix options depend on your host. Most modern hosts offer free Let’s Encrypt certificates through their control panel. If your certificate is expired, renew it immediately through your host or through a service like Certbot. After renewal, try the Stripe “Send test webhook” again to confirm delivery.
Fix 6: Permalink Structure Breaking the Webhook URL
The CF7 Stripe webhook URL is generated based on your permalink settings. If you recently changed your permalink structure (from plain to “Post name,” for example), the webhook URL changed — but your Stripe dashboard still has the old one registered.
Click “Save Changes” (even without changing anything) to flush and regenerate your rewrite rules.
Navigate to CF7 → Integration → Stripe. Copy the displayed webhook URL — it may have changed.
In Stripe dashboard, edit your existing webhook endpoint and paste the updated URL. The signing secret stays the same — you don’t need to update that.
Fix 7: Server Timeout on Webhook Response
Stripe expects your server to respond to webhook events within 30 seconds. If your server is slow — heavy plugins, poor hosting, database bottlenecks — the webhook handler might not respond in time. Stripe will mark the delivery as failed and retry, but CF7 may log the original attempt as a failure.
Signs this is your problem: Stripe shows the webhook was “delivered” eventually (after retries), but CF7 still shows “Payment Failed” for that submission.
More practically: move to faster hosting, or look into object caching (Redis/Memcached) if you’re on a high-traffic site. The webhook handler code itself is lightweight — if it’s timing out, the issue is almost always server-wide slowness.
CF7 Stripe Plugin: Honest Pros and Cons
✅ What Works Well
- Tight integration with CF7’s form builder
- Supports both one-time and recurring payments
- Stripe Elements for secure card capture
- Decent webhook handling when configured correctly
- Free version covers basic payment collection
- Works with Stripe’s test mode out of the box
❌ Known Pain Points
- Webhook setup is not intuitive for non-developers
- Error messages in CF7 are vague (“Payment Failed” tells you nothing)
- No built-in retry mechanism for failed webhook deliveries
- Plugin conflicts with certain caching plugins
- Limited official documentation on webhook troubleshooting
- Test/live mode switching requires manual key swapping
How to Read Stripe Webhook Logs to Diagnose the Problem
Instead of guessing, go directly to Stripe’s webhook delivery logs. They show you exactly what happened and why.
Click on your registered endpoint, then look at the “Recent deliveries” section.
Stripe shows the full request payload and the HTTP response your server returned. A 200 means success. A 400 usually means signature verification failed (wrong signing secret). A 404 means the webhook URL doesn’t exist on your server.
See the response code table below to know exactly what to do next.
| HTTP Response Code | What It Means | Fix |
|---|---|---|
200 OK |
Webhook received successfully | Webhook isn’t the issue — check CF7 plugin configuration |
400 Bad Request |
Signature verification failed | Wrong signing secret — recopy from Stripe dashboard |
401 Unauthorized |
Authentication failure | API key mismatch between test and live modes |
404 Not Found |
Webhook URL doesn’t exist on your server | Flush permalinks; check plugin is active; verify URL |
500 Server Error |
PHP error on your server during processing | Check WordPress error logs; plugin conflict possible |
| No response / timeout | Server didn’t respond within 30 seconds | Optimize server performance; check for blocking firewall |
Want a more robust payment integration for WordPress with built-in webhook retry logic?
Explore Better Payment Plugins for WordPress →Caching Plugins That Break CF7 Stripe Webhooks
This one catches people out. Some page caching plugins intercept all incoming requests to your site — including Stripe’s webhook POST requests — and try to serve a cached response. That’s useless for a webhook; Stripe gets a cached HTML page back instead of a proper 200 OK from your PHP handler.
| Plugin | Conflict Risk | Fix |
|---|---|---|
| WP Rocket | Medium | Exclude /?wpcf7-stripe=callback from cache in WP Rocket settings |
| W3 Total Cache | High | Add webhook URL to the “Never cache” list under Page Cache settings |
| LiteSpeed Cache | Medium | Add webhook URL to the cache exclusion list |
| WP Super Cache | High | Add webhook URL pattern to “Rejected URIs” in advanced settings |
| Cloudflare Cache | Medium | Create a Page Rule to bypass cache for the webhook URL |
Related Reading from Our Blog
CF7 Stripe vs. Alternative Payment Form Plugins
| Plugin | Webhook Reliability | Setup Difficulty | Price | Best For |
|---|---|---|---|---|
| CF7 + Stripe Integration | Medium | Moderate | Free / Paid add-ons | Existing CF7 users |
| WPForms + Stripe | High | Easy | $49.50/yr | Non-developers |
| Gravity Forms + Stripe | Very High | Moderate | $59/yr + $59 add-on | Complex workflows |
| Formidable Forms + Stripe | High | Moderate | $79/yr | Data-heavy forms |
| WooCommerce + Stripe | Very High | Complex | Free (plugin) / Fees on transactions | Full e-commerce |
Quick Diagnostic Checklist — Run Through This First
Before spending hours debugging, verify these 9 things:
☐ Webhook endpoint URL is registered in Stripe dashboard
☐ Signing secret (whsec_) is correctly pasted into CF7 settings
☐ You’re using live keys for live mode (not test keys)
☐ Site is on HTTPS with a valid SSL certificate
☐ payment_intent.succeeded event is enabled on the webhook
☐ Stripe webhook log shows a 200 response from your server
☐ Webhook URL is excluded from page caching
☐ Stripe IPs are whitelisted in any security plugins
☐ Permalinks have been flushed (Settings → Permalinks → Save)
Frequently Asked Questions
This almost always means the webhook event never reached your site (or was rejected). CF7 doesn’t poll Stripe — it only updates the payment status when a webhook tells it to. The charge can be successful in Stripe while CF7 still shows “failed” because it never received the payment_intent.succeeded event.
Not through the CF7 interface directly. Your best option is to use Stripe’s webhook dashboard to manually resend the failed event — click on the failed delivery and hit “Resend.” This triggers CF7 to process it again and update the status, assuming the underlying issue (wrong URL, missing signing secret) has been fixed.
A payment failure means the card was declined or the payment couldn’t be processed — the money was never taken. A webhook delivery failure means the payment succeeded but Stripe couldn’t notify your site about it. These are completely different problems with different fixes. Always check Stripe’s webhook logs to distinguish between the two.
Stripe retries failed webhooks over a period of 72 hours using an exponential backoff schedule — approximately at 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and so on. After 72 hours, Stripe gives up and marks the event as failed permanently. You’ll need to manually resend it if it’s been longer than that.
Yes. Test mode webhooks and live mode webhooks are completely separate in Stripe. You need to register an endpoint in your test mode dashboard for development, and a separate endpoint in your live mode dashboard for production. Each has its own distinct signing secret.
If the webhook is being delivered successfully but CF7 isn’t updating, the issue is likely in the plugin’s internal processing. Check for plugin conflicts by temporarily deactivating other plugins and testing again. Also verify that the CF7 Stripe plugin is on its latest version — some older versions had bugs in how they processed certain Stripe event payloads.
Fix It Once, Then Future-Proof It
The webhook setup takes about 10 minutes when you know what you’re doing. Go through the diagnostic checklist, check your Stripe webhook logs for the actual error code, and follow the matching fix above. Most CF7 Stripe webhook failures come down to either a missing endpoint or a wrong signing secret — both are five-minute fixes.
Get Started with a Reliable WordPress Payment Setup →