GoCardless Direct Debit — Payment Flow & Timeline
Module: Payment Gateway Integration
Document Type: Explanation (Flow-Oriented)
Audience: Backend developers, tech leads, and operations staff who need a mental model of exactly what happens, when, and in which system
Last Updated: 2026-07-08
What This Document Is
This is the single source of truth for the end-to-end GoCardless payment journey in Waste Vantage. It combines the architectural overview from the Reference, the conceptual background from the Explanation, and the step-by-step walkthrough from the Tutorial into one readable flow document with approximate timelines.
If you want to know why something works the way it does, read the Explanation.
If you want to know how to do a specific task, read the How-To Guides.
If you want to look up a method signature or route, read the Reference.
If you want to follow along with your first integration, read the Tutorial.
The Big Picture
GoCardless Direct Debit is fundamentally slower than card payments. A Stripe charge is authorised in real-time; a GoCardless payment moves through a banking network that takes 1 to 3 business days per leg. Waste Vantage accounts for this by:
- Giving the customer immediate feedback — "Your mandate is set up; we will notify you when payment completes."
- Processing events asynchronously — webhooks (preferred) and API polling (fallback) both feed into the same
ProcessGoCardlessPaymentjob. - Providing an admin override — the "Check GoCardless Status" button lets staff force a reconciliation on demand.
The Payment Flow in Four Acts
Act 1 — Checkout & Billing Request Creation
| Actor | Action | System | Approximate Time |
|---|---|---|---|
| Customer | Clicks Pay Now on invoice page | Waste Vantage | Immediate |
| Waste Vantage | GoCardlessController::checkout() creates a BillingRequest with mandate + payment details | Waste Vantage → GoCardless | < 2 seconds |
| Waste Vantage | Creates a BillingRequestFlow and gets an authorisation_url | Waste Vantage → GoCardless | < 2 seconds |
| Waste Vantage | Saves order_payments row with transaction_status = Not Processed | Waste Vantage database | Immediate |
| Waste Vantage | Redirects customer (303) to GoCardless hosted page | Customer browser | < 1 second |
| Customer | Enters bank account details and confirms mandate on GoCardless page | GoCardless hosted page | 1–3 minutes |
| GoCardless | Redirects customer back to /payment/choose/gocardless/confirm | GoCardless → Waste Vantage | < 1 second |
| Waste Vantage | Redirects customer to loading page | Waste Vantage | < 1 second |
What the customer sees: A loading/processing page with a polite message that payment confirmation may take a few days.
What Waste Vantage knows at this point:
charge_id= the Billing Request ID (BRQ…)transaction_type=GoCardlesstransaction_status=Not Processed- Everything else is
null(GoCardless has not confirmed the mandate yet)
Timeline summary: The customer checkout and redirect happen in under a minute. The banking process that follows is what takes days.
Act 2 — Mandate Fulfilment (billing_requests.fulfilled)
After the customer completes the hosted page, GoCardless internally creates the mandate and links the payment. This triggers the billing_requests.fulfilled event.
| Actor | Action | System | Approximate Time |
|---|---|---|---|
| GoCardless | Creates mandate (MD…) and customer record (CU…) | GoCardless | Hours to 1 business day |
| GoCardless | Creates payment record (PM…) | GoCardless | Hours to 1 business day |
| GoCardless | Fires billing_requests.fulfilled webhook | GoCardless → Waste Vantage | 1–3 business days total from checkout |
| Waste Vantage | GoCardlessWebhookController receives and validates HMAC | Waste Vantage | < 1 second |
| Waste Vantage | Dispatches ProcessGoCardlessPayment job | Waste Vantage queue | < 1 second |
| Waste Vantage | Job updates: uid_customer, charge_id, uid_accounting_system = PM…, status → Processing | Waste Vantage database | < 1 second |
Fallback: If the webhook is not received (private network, firewall, misconfigured endpoint), the 5-minute poller or the admin button will eventually fetch the Billing Request from the GoCardless API and synthesize the same billing_requests.fulfilled event.
What the customer sees: Nothing yet. They are still on the loading page, which polls every 8 seconds via AJAX.
What Waste Vantage knows now:
uid_accounting_system= the GoCardless Payment ID (PM…)transaction_status=Processing- The payment is queued in the banking system
Act 3 — Payment Confirmation (payments.confirmed)
The bank has received the Direct Debit instruction and confirmed that funds will be transferred on the settlement date. GoCardless fires payments.confirmed.
| Actor | Action | System | Approximate Time |
|---|---|---|---|
| Customer bank | Receives and accepts the Direct Debit instruction | Banking system | 1–3 business days after mandate fulfilment |
| GoCardless | Fires payments.confirmed webhook | GoCardless → Waste Vantage | 1–3 business days after Act 2 |
| Waste Vantage | ProcessGoCardlessPayment updates transaction_status → Confirmed | Waste Vantage database | < 1 second |
What the customer sees: Still the loading page. The AJAX poll has not detected a change yet because the final status is still succeeded, not Confirmed.
What Waste Vantage knows now:
transaction_status=Confirmed- Funds are promised but have not yet arrived in the GoCardless account
Why do we not mark the invoice paid at
Confirmed?
Confirmedmeans the bank has agreed to transfer funds, but the money has not yet arrived in the GoCardless holding account. Marking the invoicepaidat this stage would be premature — the funds could still fail to settle.
Act 4 — Funds Settled (payments.paid_out)
The funds have arrived in the Waste Vantage GoCardless account. GoCardless fires payments.paid_out.
| Actor | Action | System | Approximate Time |
|---|---|---|---|
| GoCardless | Transfers funds to Waste Vantage bank account | GoCardless → Waste Vantage bank | 1–3 business days after Act 3 |
| GoCardless | Fires payments.paid_out webhook | GoCardless → Waste Vantage | Same day as transfer |
| Waste Vantage | ProcessGoCardlessPayment updates: transaction_status → succeeded, date_payment = now(), order_invoices.status = paid | Waste Vantage database | < 1 second |
| Waste Vantage | Customer loading page AJAX detects status = succeeded and redirects to order confirmation | Customer browser | < 8 seconds (poll interval) |
What the customer sees: The loading page redirects to the order confirmation screen with a success message.
What Waste Vantage knows now:
transaction_status=succeededdate_payment= timestamp ofpaid_outorder_invoices.status=paidtransaction_id= the GoCardless Payout ID (PO…)
Total Timeline at a Glance
| Step | GoCardless Status | Waste Vantage Status | Typical Elapsed Time |
|---|---|---|---|
| 1. Customer completes checkout | — | Not Processed | Day 0, ~5 minutes |
| 2. Mandate & payment created | billing_requests.fulfilled | Processing | Day 0–1 |
| 3. Bank accepts Direct Debit | payments.confirmed | Confirmed | Day 1–3 |
| 4. Funds arrive in our account | payments.paid_out | succeeded + invoice paid | Day 2–5 |
Note: The timeline above assumes normal BECS processing in Australia. GoCardless sandbox testing simulates these stages instantly or on demand. Real-world times depend on the customer's bank and can vary by plus or minus one business day.
The Three Resolution Methods
Because GoCardless events are asynchronous and can be delayed or lost, Waste Vantage uses three complementary methods to detect status changes. All three converge on the same ProcessGoCardlessPayment job.
Method 1 — Webhook (Preferred)
- Direction: Push (GoCardless calls us)
- Latency: Seconds to minutes after the status changes
- Reliability: High, but requires a public HTTPS URL reachable by GoCardless
- Security: HMAC-SHA256 signature validated using
hash_equals() - Code path:
GoCardlessWebhookController→ProcessGoCardlessPayment::dispatch()
Method 2 — Server-Side Polling (Fallback)
- Direction: Pull (Waste Vantage calls GoCardless)
- Schedule: Every 5 minutes (plus a nightly 06:00 catch-up run)
- Reliability: 100% — no external dependency on inbound connectivity
- Code path:
PollGoCardlessPaymentscommand →GoCardlessPollingService::pollSinglePayment()→ProcessGoCardlessPayment::dispatch()
The nightly run at 06:00 uses --age=168 --limit=100 to catch payments that fell out of the 5-minute poller's 72-hour window. This is the long-tail safety net.
Method 3 — Admin Button (On-Demand)
- Direction: Pull (Admin clicks button)
- Reliability: Instant, one-shot
- Code path:
OrderController::gocardlessCheckStatus()→GoCardlessPollingService::pollSinglePayment()→ProcessGoCardlessPayment::dispatchSync()
This is the only method that runs synchronously — the admin waits for the result. It is the fastest way to reconcile a stuck payment without SSH access or waiting for the cron.
How Idempotency Prevents Double Processing
Running multiple resolution methods against the same payment is safe. Three layers prevent double-processing:
- Database terminal-state check: The poller only queries payments that are not already
Paid,Failed, orCancelled. A completed payment is never fetched again. - Already-applied guard: Before dispatching a job, the code checks whether the action would change anything (e.g., do not dispatch
confirmedwhen status is alreadyConfirmed). - Database transaction: Every job handler wraps updates in
DB::transaction(). If two jobs race, the second sees the already-updated record and performs no meaningful change.
The result: webhook + poller + admin button can all fire for the same payment in the same minute, and the database will end up in the correct state exactly once.
The Customer Experience
From the customer's perspective, the payment feels like this:
- Clicks Pay Now → redirected to GoCardless hosted page (same tab)
- Enters bank details → confirms mandate on GoCardless page
- Redirected back → lands on loading page with message: "Your Direct Debit is being set up. This may take a few days."
- Waits → loading page polls silently every 8 seconds
- Receives email → invoice marked as paid (automated or manual notification, depending on setup)
- Sees confirmation → if they return to the site, the order page shows the paid status
If the customer closes the browser after Step 3, they can safely return later. The polling and webhook mechanisms continue in the background.
What Happens on the Admin Side
Admins see the payment row in admin/order/show/payment.blade.php with a dynamic status badge:
| Waste Vantage Status | GoCardless Reality | Admin Action Available |
|---|---|---|
Not Processed | Billing request created; awaiting customer | Check GoCardless Status button visible |
Processing | Mandate fulfilled; payment queued with bank | Check GoCardless Status button visible |
Pending | payments.confirmed received; funds transferring | Check GoCardless Status button visible |
Confirmed | Bank has accepted the instruction | Check GoCardless Status button visible |
succeeded | Funds received; invoice marked paid | Button hidden (terminal state) |
Failed | Payment failed (insufficient funds, etc.) | Button hidden (terminal state) |
Cancelled | Customer cancelled before completion | Button hidden (terminal state) |
The Check GoCardless Status button performs an on-demand reconciliation. If the status has changed since the last check, the page reloads to show the fresh badge.
Key Terms
| Term | Meaning |
|---|---|
Billing Request (BRQ…) | GoCardless object that bundles a mandate request and a payment request. Created by Waste Vantage at checkout. |
| Billing Request Flow | GoCardless hosted session where the customer enters bank details. Created by Waste Vantage; returns an authorisation_url. |
Mandate (MD…) | The customer's pre-authorisation to debit their bank account. Created by GoCardless after the hosted page is completed. |
Customer (CU…) | GoCardless customer record. Linked to the mandate. |
Payment (PM…) | GoCardless payment object. Created after the mandate is fulfilled. This is the object whose status moves from pending → confirmed → paid_out. |
Payout (PO…) | GoCardless's transfer of collected funds to Waste Vantage's bank account. Stored in transaction_id after paid_out. |
| Webhook | Push notification from GoCardless to Waste Vantage when a status changes. Preferred resolution method. |
| Poller | Waste Vantage's scheduled command that pulls status from GoCardless every 5 minutes. Fallback for when webhooks are unavailable. |
| Event | A structured array describing a GoCardless state change. Both webhooks and polling produce the same event shape. |
Quick Reference: Event → Status Mapping
| GoCardless Event | Waste Vantage Action | New Status |
|---|---|---|
billing_requests.fulfilled | Populate uid_accounting_system, uid_customer, mandate IDs | Processing |
payments.confirmed | No additional fields | Confirmed |
payments.paid_out | Set date_payment, transaction_id = PO…, mark invoice paid | succeeded |
payments.failed | (handler pending — see Known Issues) | Failed (desired) |
payments.cancelled | (handler pending — see Known Issues) | Cancelled (desired) |
Known Gaps
payments.failedandpayments.cancelledevents are received but not yet handled inProcessGoCardlessPayment. The admin button will return a misleading "No change" for those events until the handler is implemented.- Webhook delivery is not yet implemented in the current deployment for all environments. Polling remains the primary resolution method until webhooks are confirmed working in production.