Payment failures & restrictions
What happens when a payment fails — the grace and restriction ladder, what stays open at each stage, and how recovery works.
Payment failures & restrictions
When a client's AutoPay payment fails, the platform does not cut them off instantly. It walks a ladder: friendly → firm → blocked, with time (and clear messages) at every step. This page explains the ladder, exactly what a client can still do at each stage, and how they get restored.
Who this is for
Super Admins and support staff handling a client whose payment failed.
- Difficulty: Medium
- Time: 10 min
- Prerequisites: A configured provider (Payment providers) and the grace period setting on that page.
The ladder
Active → Payment failed → Grace period → Past due → Restricted → Suspended
(hours) (default 7 days) (7 more days) (7 more days)
| Stage | Triggered by | What the client sees |
|---|---|---|
| Payment failed | A provider payment.failed event, or a revoked mandate |
Failure message; retry is still possible |
| Grace period | The grace job (ApplyGracePeriod, daily 07:15) after a failed payment |
Reminder messaging; everything still works |
| Past due | The catch-up sweep when the grace window is exhausted | Stronger reminder; still no restriction yet |
| Restricted | ApplyRestriction (daily 07:30) once Past due is older than the grace period |
Only four core features remain |
| Suspended | The same job once Restricted is older than 7 days (SUSPEND_AFTER_DAYS) |
Every feature denied |
Times are configurable: the grace period (default 7 days, max 90) is set at Billing → Providers & Settings. The 7-day Restricted-to-Suspended window is fixed.
What "Restricted" actually leaves open RESTRICTED allows only: CRM, lead management, follow-ups and reports. Everything else — Meta, WhatsApp, automations, AI, mobile, API — is denied, with the message: "This feature is not included in your current service agreement. Please contact your account manager."
SUSPENDED denies everything — every feature check returns false.
Two different "suspended" ideas — don't mix them up A commercial Suspended agreement blocks features through entitlements. A manually suspended workspace (the Suspend button under Businesses) sets the workspace inactive and bounces its users to /admin/subscription-required. They are independent mechanisms — a workspace can be active while its agreement is Restricted, and vice versa.
Step 1 — See a failure early
Failures surface in three places:
- Billing → Commercial Billing (
/super-admin/billing/commercial) — the Payment failures / Past due / Grace / Restricted / Suspended cards. - The payment-failure job runs hourly: it finds overdue invoices, notifies the client once (deduplicated, so no spam), marks invoices Overdue, and advances the state ladder based on how old the failure is.
- Reconciliation alerts (every 30 minutes) — when the provider's record and the platform's record disagree, an alert is raised for a human instead of silently editing financial data. Providers unreachable or an unknown provider status also raise alerts. When the provider later confirms the local state, the alert is resolved.
Step 2 — Contact and retry
Before the ladder bites, the best fix is a retry or a manual payment:
- The client's successfully authorized mandate can be charged again by the provider; the platform books the payment when the webhook arrives.
- Offline arrangements: Record Manual Payment on the agreement (bank transfer / UPI / cheque with a reference).
Step 3 — Recovery
Recovery is driven by the provider confirming the subscription is healthy again:
- The client re-authorizes / the provider retries and succeeds.
- The platform verifies with the provider — a verified authorization (
BillingService::verifyAuthorization()) or asubscription.authorizedwebhook moves the agreement back to Active from any recoverable state. - Every entitlement check re-reads the active agreement, so access is restored immediately — no cache to clear.
If instead the provider and local records disagree, the reconciliation alert stays open until a human resolves it. Never "fix" a mismatch by editing payment rows.
Expected result
Failures are noticed the day they happen, the client hears from you before restrictions hit, and a client who pays is back to full service the moment the provider confirms.
Common mistakes
- Restoring a client by editing their workspace/plan instead of the agreement. Entitlements come from the agreement; payment restores the agreement, not the schedule row.
- Assuming Restricted = no login. It is a feature-level squeeze, not a login block.
- Setting the grace period to 0 or 1. That escalates to restriction almost immediately — use the default 7 unless the contract says otherwise.
- Ignoring reconciliation alerts. They exist precisely because money must not be guessed at.
- Telling the client "you'll be cut off today" at Payment failed stage. The ladder buys time; communicate the actual stage.
Troubleshooting
| Symptom | What to check |
|---|---|
| Client paid but still Restricted | Was the authorization verified with the provider? Check the agreement Events/Timeline; provider confirmation restores Active |
| No overdue notifications sent | Dedup may have already sent one for that invoice; check the client's notifications |
| Ladder not advancing | Cron: the grace/restriction jobs run daily at 07:15 / 07:30 — see Queues, jobs & scheduler |
| Reconciliation alert stuck open | Provider still shows a mismatch; resolve at the provider, then the next pass closes it |
| Client claims they never gave AutoPay consent | Check the mandate row and the agreement's authorization history |
Technical details
- Ladder jobs:
App\Jobs\Billing\CheckPaymentFailures(hourly, no-overlap 20 min),ApplyGracePeriod(daily 07:15),ApplyRestriction(daily 07:30,SUSPEND_AFTER_DAYS = 7). - Statuses:
App\Models\CommercialAgreement— PAYMENT_FAILED, GRACE_PERIOD, PAST_DUE, RESTRICTED, SUSPENDED; transitions inApp\Services\CommercialBilling\AgreementStateMachine. - Entitlements:
EntitlementService—RESTRICTED_ALLOWED_FEATURES = ['crm','lead_management','followups','reports']; SUSPENDED denies all. - Recovery:
BillingService::verifyAuthorization()+CommercialAgreementService::markActivated()(idempotent, allowed from recoverable states). - Reconciliation:
App\Services\CommercialBilling\BillingReconciliationService,ReconcileProviderStatusjob every 30 min; alerts inbilling_reconciliation_alerts. - Notifications:
BillingNotifier::paymentFailed / gracePeriodStarted / accountRestricted / accountSuspended.
Related guides
Was this page helpful?
Thanks — noted. ✦