Build a Failed Payment Recovery Workflow in GoHighLevel

by Fahim

When a recurring client subscription fails, nobody wants to chase them down manually over Slack or send awkward billing emails. We are going to build an automated dunning engine inside GoHighLevel that fires instantly on failed charges, dispatches a timed email and SMS recovery sequence with self-service update links, and cuts access automatically if the invoice is still unpaid after 7 days.

Build a Failed Payment Recovery Workflow in GoHighLevel
Build a Failed Payment Recovery Workflow in GoHighLevel

What happens when a subscription payment fails in HighLevel

Most SaaS and agency churn isn’t intentional. It’s usually an expired card, a temporary fraud flag from the bank, or a low balance on billing day. When you bill recurring subscriptions through Stripe or HighLevel’s native invoicing, a declined charge fires a webhook payload right away.

If you don’t catch that event with a workflow, Stripe will just silently trigger its built-in Smart Retries (roughly 4 attempts over 2 to 3 weeks). The customer gets zero context from your brand, has no idea their card is failing, and suddenly loses access out of nowhere. Or worse: they keep using your app or services for a month without paying. This workflow grabs the failure immediately, gives the client a direct update link, and syncs their status tags across the CRM.

Prerequisites before building the dunning workflow

Make sure you have these in place inside your sub-account before touching the workflow builder:

  • A connected Stripe account or native gateway. If you need to set this up first, check our guide on building a 2-step order form in GoHighLevel.
  • The Customer Portal turned on under Payments > Settings > Customer Portal so clients can swap out cards on their own.
  • A contact tag named status: past-due to mark delinquent accounts.
  • A contact tag named status: active for healthy paying accounts.

Step 1: Configure the Payment Failed workflow trigger

Go to Automation > Workflows and hit Create Workflow. Start with a blank canvas.

Add a new trigger and pick Payment Failed from the list. Leave the product filter empty if you want this recovery flow to catch every recurring product in your sub-account. Filter by specific subscriptions only if you need custom messaging for higher tiers.

Here’s what the incoming webhook payload looks like when HighLevel catches the failure:

{ "event": "payment_failed", "contact_id": "CNTC_892347102", "invoice_id": "in_1N4m2sK1X0pq9", "amount_due": 9700, "currency": "usd", "failure_reason": "card_declined", "attempt_count": 1
}

Turn on Allow Re-entry in your workflow settings. If a user fails a payment today, fixes it, and fails another payment six months down the line, disabling re-entry means they’ll completely bypass this recovery flow next time.

Step 2: Build the immediate response action block

The first 15 minutes after a declined transaction are your best window for recovery. People are usually still sitting at their desks or looking at their phones when the bank alert pings them.

Stack these immediate actions right below the trigger:

  1. Add Contact Tag: Apply status: past-due.
  2. Remove Contact Tag: Strip status: active.
  3. Send Email: Subject line: “Action required: Update payment method for {{contact.company_name}}”. Drop in your portal merge tag: {{custom_values.customer_portal_url}}. Check out our walkthrough on using custom values in GoHighLevel to manage this URL from one spot.
  4. Send SMS: Keep it short with a direct link. Stay under 160 characters so it doesn’t split into multiple billable segments.

Here’s the HTML template I use across our production accounts:

Hey { {contact.first_name} }, We were unable to process your scheduled subscription payment of { {payment.amount_formatted} } for your account. Banks decline recurring charges for basic reasons like card renewals or security blocks. You can update your payment details in 30 seconds below:

Update Payment Details Your account remains open for the next 7 days while our system retries the charge. Best,
Support Team

Make sure your sending sub-domain has valid DKIM and SPF records so billing notices actually hit the primary inbox. And if you’re sending texts, make sure you configure SMS quiet hours in GoHighLevel workflows so you aren’t texting clients at 3:00 AM.

Step 3: Add conditional wait loops and retries

Don’t blast the customer every single morning. Set up a paced 7-day dunning sequence that checks if the payment cleared before dispatching the next reminder.

This timing structure consistently recovers around 68% of failed charges in our accounts:

  • Day 0 (Instant): Email #1 + SMS #1
  • Day 2 (48 Hours): Wait 2 days → If/Else check (Has tag status: active?) → If No, send Email #2
  • Day 5 (120 Hours): Wait 3 days → If/Else check → If No, send SMS #2 + trigger an internal alert to their account manager
  • Day 7 (168 Hours): Final notice email → revoke access

To wire this up, drop in a Wait step configured for Time Delay: 2 Days. Right after that, add an If/Else condition called “Did they pay?”.

Set the rule to check if Contact Tag includes status: active. If true, route them straight to a Remove from Workflow step. If false, route them into Email #2.

Step 4: Cancel or lock access on Day 7

If a customer sits at Day 7 without updating their billing info, you need to cut off access and mark the account suspended.

In the final branch of the flow, add these steps:

  1. Add Contact Tag: status: suspended.
  2. Remove Contact Tag: status: past-due.
  3. Membership / SaaS Action: If you use SaaS Mode or Memberships, add the Revoke Access action to lock the sub-account or revoke their course access.
  4. Send Webhook: If you run an external app or database, use an outgoing webhook to notify your API. Here is how to set up authenticated webhooks with custom headers in HighLevel to keep that endpoint secure.
  5. Create Task: Assign a manual task to your billing manager: “Call {{contact.name}} – Account Suspended”.

If you’re syncing an external backend with Node.js or a serverless worker, your endpoint can catch that suspension event like this:

import express from 'express';
const app = express(); app.use(express.json()); app.post('/api/ghl-dunning-suspended', async (req, res) => { const { contact_id, email, status } = req.body; if (status === 'suspended') { // Lock access in external database or application backend await db.users.update({ where: { email }, data: { subscriptionActive: false, accessLevel: 'locked' } }); console.log(`User ${email} (${contact_id}) locked due to dunning failure.`); } return res.status(200).json({ received: true });
}); app.listen(3000, () => console.log('Dunning webhook listener running on port 3000'));

That keeps external databases and your CRM state aligned without requiring anyone on your team to flip manual switches.

Step 5: The Payment Received reset workflow

A dunning workflow is a trap unless you pair it with a companion reset flow. When the client updates their card and Stripe successfully charges it, you need to pull them out of the failure sequence immediately so they stop getting dunning alerts.

Create a second workflow named [Billing] Payment Succeeded - Reset Dunning:

  1. Trigger: Payment Received or Order Submitted.
  2. Action 1: Remove from Workflow → Select your Payment Failed Recovery workflow. This kills any active wait steps instantly.
  3. Action 2: Remove Contact Tag: status: past-due and status: suspended.
  4. Action 3: Add Contact Tag: status: active.
  5. Action 4: Restore Access: Re-grant any membership or SaaS privileges that were locked.
  6. Action 5: Send Email: A quick confirmation: “Payment received! Your account is active.”

For more edge cases around retry behavior, take a look at the Stripe Subscription Lifecycle Documentation and the GoHighLevel Payments Help Center.

Testing the entire sequence safely

Do not publish this straight to production without validating both branches. Here is how to verify everything in test mode:

  1. Flip your funnel or order form into Stripe Test Mode.
  2. Use Stripe’s decline test card (4000 0000 0000 0002) to force an initial payment failure.
  3. Confirm the contact receives the Day 0 email and SMS, and verify that status: past-due appears on their record.
  4. Open the Execution Logs to ensure the contact is currently paused at the first 2-day Wait step.
  5. Submit a successful charge using Stripe’s standard test card (4242 4242 4242 4242) for that same contact.
  6. Check that your reset workflow fired, pulled the contact out of the dunning workflow, and switched the tag back to status: active.

Frequently Asked Questions

Can I send the customer directly to update their card without logging into the portal?

Yes. HighLevel supports one-click magic links using {{customer_portal.login_url}}. When the client clicks it, they bypass the manual login prompt and land directly on their payment methods screen.

What happens if Stripe retries automatically and succeeds while the workflow is running?

When Stripe’s Smart Retries land a successful charge, it dispatches an invoice.payment_succeeded event to HighLevel. That triggers your companion reset workflow, which ejects the contact from the recovery sequence before the next reminder email goes out.

Should I send SMS recovery messages outside of normal business hours?

No. Firing automated collection texts in the middle of the night will destroy your opt-in rates and violate carrier compliance rules. Add an Advance Window setting to your Wait steps or SMS actions so messages only send between 8:00 AM and 8:00 PM in the contact’s local timezone.

Will this workflow cancel the subscription in Stripe automatically?

This workflow manages tags and CRM permissions. If you want Stripe to formally cancel the subscription after all attempts fail, go to your Stripe Dashboard under Settings > Subscriptions and emails > Manage failed payments and set the final failure action to Cancel subscription.

all_in_one_marketing_tool