Skip to main content

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:

  1. Giving the customer immediate feedback — "Your mandate is set up; we will notify you when payment completes."
  2. Processing events asynchronously — webhooks (preferred) and API polling (fallback) both feed into the same ProcessGoCardlessPayment job.
  3. 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

ActorActionSystemApproximate Time
CustomerClicks Pay Now on invoice pageWaste VantageImmediate
Waste VantageGoCardlessController::checkout() creates a BillingRequest with mandate + payment detailsWaste Vantage → GoCardless< 2 seconds
Waste VantageCreates a BillingRequestFlow and gets an authorisation_urlWaste Vantage → GoCardless< 2 seconds
Waste VantageSaves order_payments row with transaction_status = Not ProcessedWaste Vantage databaseImmediate
Waste VantageRedirects customer (303) to GoCardless hosted pageCustomer browser< 1 second
CustomerEnters bank account details and confirms mandate on GoCardless pageGoCardless hosted page1–3 minutes
GoCardlessRedirects customer back to /payment/choose/gocardless/confirmGoCardless → Waste Vantage< 1 second
Waste VantageRedirects customer to loading pageWaste 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 = GoCardless
  • transaction_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.

ActorActionSystemApproximate Time
GoCardlessCreates mandate (MD…) and customer record (CU…)GoCardlessHours to 1 business day
GoCardlessCreates payment record (PM…)GoCardlessHours to 1 business day
GoCardlessFires billing_requests.fulfilled webhookGoCardless → Waste Vantage1–3 business days total from checkout
Waste VantageGoCardlessWebhookController receives and validates HMACWaste Vantage< 1 second
Waste VantageDispatches ProcessGoCardlessPayment jobWaste Vantage queue< 1 second
Waste VantageJob updates: uid_customer, charge_id, uid_accounting_system = PM…, status → ProcessingWaste 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.

ActorActionSystemApproximate Time
Customer bankReceives and accepts the Direct Debit instructionBanking system1–3 business days after mandate fulfilment
GoCardlessFires payments.confirmed webhookGoCardless → Waste Vantage1–3 business days after Act 2
Waste VantageProcessGoCardlessPayment updates transaction_statusConfirmedWaste 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?
Confirmed means the bank has agreed to transfer funds, but the money has not yet arrived in the GoCardless holding account. Marking the invoice paid at 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.

ActorActionSystemApproximate Time
GoCardlessTransfers funds to Waste Vantage bank accountGoCardless → Waste Vantage bank1–3 business days after Act 3
GoCardlessFires payments.paid_out webhookGoCardless → Waste VantageSame day as transfer
Waste VantageProcessGoCardlessPayment updates: transaction_statussucceeded, date_payment = now(), order_invoices.status = paidWaste Vantage database< 1 second
Waste VantageCustomer loading page AJAX detects status = succeeded and redirects to order confirmationCustomer 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 = succeeded
  • date_payment = timestamp of paid_out
  • order_invoices.status = paid
  • transaction_id = the GoCardless Payout ID (PO…)

Total Timeline at a Glance

StepGoCardless StatusWaste Vantage StatusTypical Elapsed Time
1. Customer completes checkoutNot ProcessedDay 0, ~5 minutes
2. Mandate & payment createdbilling_requests.fulfilledProcessingDay 0–1
3. Bank accepts Direct Debitpayments.confirmedConfirmedDay 1–3
4. Funds arrive in our accountpayments.paid_outsucceeded + invoice paidDay 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: GoCardlessWebhookControllerProcessGoCardlessPayment::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: PollGoCardlessPayments command → 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:

  1. Database terminal-state check: The poller only queries payments that are not already Paid, Failed, or Cancelled. A completed payment is never fetched again.
  2. Already-applied guard: Before dispatching a job, the code checks whether the action would change anything (e.g., do not dispatch confirmed when status is already Confirmed).
  3. 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:

  1. Clicks Pay Now → redirected to GoCardless hosted page (same tab)
  2. Enters bank details → confirms mandate on GoCardless page
  3. Redirected back → lands on loading page with message: "Your Direct Debit is being set up. This may take a few days."
  4. Waits → loading page polls silently every 8 seconds
  5. Receives email → invoice marked as paid (automated or manual notification, depending on setup)
  6. 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 StatusGoCardless RealityAdmin Action Available
Not ProcessedBilling request created; awaiting customerCheck GoCardless Status button visible
ProcessingMandate fulfilled; payment queued with bankCheck GoCardless Status button visible
Pendingpayments.confirmed received; funds transferringCheck GoCardless Status button visible
ConfirmedBank has accepted the instructionCheck GoCardless Status button visible
succeededFunds received; invoice marked paidButton hidden (terminal state)
FailedPayment failed (insufficient funds, etc.)Button hidden (terminal state)
CancelledCustomer cancelled before completionButton 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

TermMeaning
Billing Request (BRQ…)GoCardless object that bundles a mandate request and a payment request. Created by Waste Vantage at checkout.
Billing Request FlowGoCardless 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 pendingconfirmedpaid_out.
Payout (PO…)GoCardless's transfer of collected funds to Waste Vantage's bank account. Stored in transaction_id after paid_out.
WebhookPush notification from GoCardless to Waste Vantage when a status changes. Preferred resolution method.
PollerWaste Vantage's scheduled command that pulls status from GoCardless every 5 minutes. Fallback for when webhooks are unavailable.
EventA structured array describing a GoCardless state change. Both webhooks and polling produce the same event shape.

Quick Reference: Event → Status Mapping

GoCardless EventWaste Vantage ActionNew Status
billing_requests.fulfilledPopulate uid_accounting_system, uid_customer, mandate IDsProcessing
payments.confirmedNo additional fieldsConfirmed
payments.paid_outSet date_payment, transaction_id = PO…, mark invoice paidsucceeded
payments.failed(handler pending — see Known Issues)Failed (desired)
payments.cancelled(handler pending — see Known Issues)Cancelled (desired)

Known Gaps

  • payments.failed and payments.cancelled events are received but not yet handled in ProcessGoCardlessPayment. 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.