GoCardless Direct Debit — How-To Guides
Module: Payment Gateway Integration
Document Type: How-To Guides (Problem-Oriented)
Audience: Backend developers maintaining or extending this integration
How to Configure GoCardless for a New Environment
Problem
You need to point the integration at a GoCardless sandbox (for testing) or live account (for production).
Steps
1. Add credentials to .env
# GoCardless Direct Debit
GOCARDLESS_ACCESS_TOKEN=your_token_here
GOCARDLESS_ENVIRONMENT=sandbox # Change to 'live' for production
GOCARDLESS_WEBHOOK_SECRET=your_secret
# Polling mode (set false if webhooks are reliably reachable)
GOCARDLESS_ENABLE_POLLING=true
GOCARDLESS_POLL_INTERVAL_MINUTES=5
2. Get your credentials from GoCardless
| Credential | Where to find it |
|---|---|
GOCARDLESS_ACCESS_TOKEN | GoCardless Dashboard → Developers → Access Tokens |
GOCARDLESS_WEBHOOK_SECRET | Dashboard → Developers → Webhooks → Create endpoint → copy secret |
Use the Sandbox dashboard at https://manage-sandbox.gocardless.com/ for development.
Use the Live dashboard at https://manage.gocardless.com/ for production.
3. Clear config cache
php artisan config:clear
4. Verify the connection is working
Run a dry-run poll — if credentials are invalid, you will see an ApiException in the output:
php artisan gocardless:poll --dry-run
How to Register a GoCardless Webhook Endpoint
Problem
GoCardless needs to know the URL to send payment events to. Without this, no payments.confirmed or payments.paid_out events will be received.
Steps
1. Determine your public webhook URL
https://your-domain.com.au/payment/gocardless/webhook
If the server is not publicly reachable, use API Polling mode instead (see How to Switch to Polling-Only Mode).
2. Register in the GoCardless Dashboard
- Go to Developers → Webhooks
- Click Create endpoint
- Enter the URL above
- Copy the Webhook signing secret that is generated
- Set
GOCARDLESS_WEBHOOK_SECRETin.envto this value - Run
php artisan config:clear
3. Test the webhook
GoCardless provides a Send test event button on the webhook endpoint page. Click it and verify that a 200 OK response is returned. Check storage/logs/laravel.log for the log entry:
[INFO] GoCardless Webhook: Received 1 actionable event(s).
How to Switch to Polling-Only Mode
Problem
The client's server is behind a firewall or on a private network, so GoCardless cannot deliver webhooks.
Steps
1. Update .env
GOCARDLESS_ENABLE_POLLING=true
GOCARDLESS_WEBHOOK_SECRET= # Leave empty — webhooks are not being used
You do not need to remove the webhook route. Leaving it in place has no negative effect.
2. Ensure the Laravel scheduler is running
The scheduler must be running for the poll to fire automatically:
# On Linux/macOS — add to crontab
* * * * * cd /path/to/project && php artisan schedule:run >> /dev/null 2>&1
# On Windows — verify Task Scheduler is set up, or run manually:
php artisan schedule:run
3. Verify polling is working
php artisan gocardless:poll --dry-run
Expected output for a pending payment:
GoCardless Poller starting [DRY RUN]…
Found 2 pending GoCardless payment(s) to check.
[→] OrderPayment #5 — dispatching [billing_requests.fulfilled]
[SKIP] OrderPayment #6 — no status change detected.
+---------+------------+---------+--------+
| Polled | Dispatched | Skipped | Errors |
+---------+------------+---------+--------+
| 2 | 0 | 2 | 0 |
+---------+------------+---------+--------+
Note that --dry-run shows Dispatched: 0 regardless — that is correct.
4. Check the poll log
tail -f storage/logs/gocardless-poll.log
How to Add the GoCardless Payment Button to a New Page
Problem
You have a new payment page (not confirm-to-payment.blade.php) and need to add a GoCardless payment option.
Steps
1. Ensure the $payments collection is passed to the view
In the controller, pass the payments table collection (same as existing payment pages):
$payments = \App\Payment::where('status', 1)->get();
return view('your.view', compact('invoice', 'payments', ...));
2. Add the GoCardless block to the blade template
Copy this block and place it within your payment options section:
@if (@$payments->where('name', 'GoCardless')->first()->status)
<hr class="mt-1 mb-2 custom-hr">
<div class="row">
<div class="col-12 col-md-8">
<p class="mb-0 title-payment">
<strong>GoCardless Direct Debit</strong> <br>
<span class="subtitle">Authorise a Direct Debit mandate via GoCardless</span>
</p>
</div>
<div class="col-12 col-md-4 d-flex text-right p-2">
<form action="{{ route('payment.choose.gocardless') }}" method="post">
@csrf
<input type="hidden" name="invoice_id" value="{{ $invoice->id }}">
<button class="btn btn-success btn-paynow" type="submit"
onclick="disableButtonAndSubmit(this)">
Pay Now
</button>
</form>
</div>
</div>
@endif
3. Seed the payments table
The button only renders when a row with name = 'GoCardless' and status = 1 exists in the payments table. Verify this row exists:
SELECT id, name, status FROM payments WHERE name = 'GoCardless';
How to Add Frontend AJAX Polling to a Loading Page
Problem
After a customer completes the GoCardless flow, they land on the loading page. You want the page to auto-redirect when payment is confirmed, without requiring a manual refresh.
Steps
1. Add the polling script to the loading blade view
Inside the loading page Blade view (e.g., resources/views/orders/loading-payment.blade.php), add:
@if (isset($gcInvoiceId))
<script>
(function () {
'use strict';
const invoiceId = {{ $gcInvoiceId }};
const statusUrl = "{{ route('payment.gocardless.status', ['invoiceId' => '__ID__']) }}"
.replace('__ID__', invoiceId);
const pollEvery = 8000; // ms — poll every 8 seconds
const maxAttempts = 45; // stop after ~6 minutes
let attempts = 0;
function poll() {
if (attempts >= maxAttempts) {
// Give up silently — Kernel poller will still resolve it in the background
return;
}
attempts++;
axios.get(statusUrl)
.then(function (response) {
const data = response.data;
if (data.status === 'succeeded' && data.redirect) {
window.location.href = data.redirect;
return;
}
// Not paid yet — schedule next check
setTimeout(poll, pollEvery);
})
.catch(function () {
// Network error — retry silently
setTimeout(poll, pollEvery);
});
}
// Start first poll after 5 seconds (give GC a moment to process)
setTimeout(poll, 5000);
}());
</script>
@endif
2. Pass $gcInvoiceId from the controller
In the controller that renders the loading page, detect whether the payment type is GoCardless and pass the invoice ID:
// Detect if the latest payment for this invoice is GoCardless
$latestPayment = OrderPayment::where('order_invoice_id', $invoice->id)
->latest()->first();
$gcInvoiceId = $latestPayment?->transaction_type === 'GoCardless'
? $invoice->id
: null;
return view('orders.loading-payment', compact('invoice', 'gcInvoiceId', ...));
How to Manually Trigger a Payment Status Check
Problem
A payment is stuck in Pending and you need to force an immediate status check without waiting for the next scheduled poll.
Steps
Option A — Run the Artisan command manually
The gocardless:poll command supports two flags that control its query scope:
| Flag | Default | Meaning |
|---|---|---|
--age=N | 72 | Only poll payments created within the last N hours |
--limit=N | 50 | Maximum number of payments to poll in a single run |
--dry-run | off | Log what would be dispatched without actually dispatching jobs |
# Check all recent pending payments now (defaults: 72h window, 50 records)
php artisan gocardless:poll
# Check a specific age window with a dry run first
php artisan gocardless:poll --age=24 --dry-run
php artisan gocardless:poll --age=24
# Replicate the nightly 06:00 reconciliation run on demand
# (7-day lookback, 100 records) — useful for ad-hoc investigations
php artisan gocardless:poll --age=168 --limit=100 --dry-run
php artisan gocardless:poll --age=168 --limit=100
The nightly run uses
--age=168 --limit=100to catch payments that fell out of the 5-minute poller's 72h window.
Option B — Call the status endpoint directly (cURL)
curl -X GET "https://your-domain.com.au/payment/gocardless/status/{invoiceId}" \
-H "Accept: application/json"
Expected responses:
// Still pending
{ "success": true, "status": "pending", "change_detected": false, "data": null }
// Fulfilled — job dispatched
{ "success": true, "status": "processing", "change_detected": true, "data": null }
// Fully paid
{ "success": true, "status": "succeeded", "redirect": "https://...", "data": null }
Option C — Check the log
# Live webhook events
tail -f storage/logs/laravel.log | grep GoCardless
# Scheduled poll output
tail -f storage/logs/gocardless-poll.log
How to Debug a Webhook Not Being Received
Problem
Payment status is not updating even though the customer completed the GoCardless flow.
Checklist
-
Is the webhook endpoint registered?
php artisan route:list --name=gocardless.webhookExpected:
POST | payment/gocardless/webhook | payment.gocardless.webhook -
Is the webhook URL reachable?
GoCardless requires a public HTTPS URL. Test with:curl -X POST https://your-domain.com.au/payment/gocardless/webhook \-H "Content-Type: application/json" \-d '{"events":[]}'Expected response:
422(no events, but reachable). -
Is the webhook secret correct?
CheckGOCARDLESS_WEBHOOK_SECRETin.envmatches the secret shown in the GoCardless Dashboard under your webhook endpoint. -
Is the queue worker running?
Jobs are dispatched asynchronously. Without a running queue worker, jobs queue up but never execute:php artisan queue:work -
Check the failed jobs table
php artisan queue:failed -
Switch to polling as a temporary fix
While investigating, enable polling mode to unblock payments:php artisan gocardless:poll