Skip to main content

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

CredentialWhere to find it
GOCARDLESS_ACCESS_TOKENGoCardless Dashboard → Developers → Access Tokens
GOCARDLESS_WEBHOOK_SECRETDashboard → 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

  1. Go to Developers → Webhooks
  2. Click Create endpoint
  3. Enter the URL above
  4. Copy the Webhook signing secret that is generated
  5. Set GOCARDLESS_WEBHOOK_SECRET in .env to this value
  6. 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:

FlagDefaultMeaning
--age=N72Only poll payments created within the last N hours
--limit=N50Maximum number of payments to poll in a single run
--dry-runoffLog 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=100 to 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

  1. Is the webhook endpoint registered?

    php artisan route:list --name=gocardless.webhook

    Expected: POST | payment/gocardless/webhook | payment.gocardless.webhook

  2. 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).

  3. Is the webhook secret correct?
    Check GOCARDLESS_WEBHOOK_SECRET in .env matches the secret shown in the GoCardless Dashboard under your webhook endpoint.

  4. Is the queue worker running?
    Jobs are dispatched asynchronously. Without a running queue worker, jobs queue up but never execute:

    php artisan queue:work
  5. Check the failed jobs table

    php artisan queue:failed
  6. Switch to polling as a temporary fix
    While investigating, enable polling mode to unblock payments:

    php artisan gocardless:poll