IPN, Routes & Settings
Step 4: IPN/Callback Controller
<?php
declare(strict_types=1);
namespace MyPaymentGateway\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class MyIpnController extends Controller
{
public function handle(Request $request)
{
// 1. Verify authenticity
if (!MyGateway::verifyWebhook($request)) {
report('MyPaymentGateway: Invalid webhook signature');
return response()->json(['success' => false], 401);
}
// 2. Parse provider data
$parsed = MyGateway::parseWebhook($request);
if ($parsed['record_id'] <= 0 || $parsed['payment_type'] === '') {
return response()->json(['success' => false], 400);
}
// 3. Delegate to core completion service
$result = function_exists('plugin_apply_filters')
? plugin_apply_filters('payment.complete', ['ok' => false], [
'type' => $parsed['payment_type'],
'id' => $parsed['record_id'],
'transaction_id' => $parsed['transaction_id'],
])
: ['ok' => false];
if (is_array($result) && !empty($result['ok'])) {
return response()->json(['success' => true], 200);
}
return response()->json(['success' => false], 422);
}
}
Step 5: Routes
routes/api.php — Public IPN endpoint (no CSRF, no session):
use MyPaymentGateway\Http\Controllers\MyIpnController;
Route::post('mygateway/ipn', [MyIpnController::class, 'handle'])
->name('mygateway.ipn');
routes/web.php — Protected routes:
use MyPaymentGateway\Http\Controllers\MyCheckoutController;
use MyPaymentGateway\Http\Controllers\MyAdminController;
Route::group(['middleware' => ['auth', 'setlang']], function () {
Route::get('mygateway/pay', [MyCheckoutController::class, 'pay'])
->name('mygateway.pay')->middleware('signed');
});
Route::group([
'prefix' => 'admin/mygateway',
'as' => 'mygateway.admin.',
'middleware' => ['auth:admin', 'setlang'],
], function () {
Route::match(['get', 'post'], '/', [MyAdminController::class, 'settings'])
->name('settings')->permission('mygateway-settings');
});
Step 6: Admin Settings Controller
<?php
declare(strict_types=1);
namespace MyPaymentGateway\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class MyAdminController extends Controller
{
private const FIELDS = [
'mygateway_gateway' => 'nullable|string|max:10',
'mygateway_test_mode' => 'nullable|string|max:10',
'mygateway_api_key' => 'nullable|string|max:1000',
'mygateway_secret_key' => 'nullable|string|max:1000',
'mygateway_preview_logo' => 'nullable|string|max:255',
];
public function settings(Request $request)
{
if ($request->isMethod('post')) {
$request->validate(self::FIELDS);
foreach (array_keys(self::FIELDS) as $field) {
update_static_option($field, $request->input($field));
}
toastr_success(__('Settings updated.'));
return back();
}
$settings = [];
foreach (array_keys(self::FIELDS) as $field) {
$settings[$field] = get_static_option($field, '');
}
return view('my-payment-gateway::admin.settings', compact('settings'));
}
}
Step 7: Admin Settings View
@extends('backend.layout.master-tailwind')
@section('content')
<div class="card">
<div class="card-header">
<h5>{{ __('My Gateway Settings') }}</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ route('mygateway.admin.settings') }}">
@csrf
<div class="mb-3">
<label class="form-label">{{ __('Enable Gateway') }}</label>
<input type="checkbox" name="mygateway_gateway" value="on"
{{ ($settings['mygateway_gateway'] ?? '') === 'on' ? 'checked' : '' }}>
</div>
<div class="mb-3">
<label class="form-label">{{ __('Test Mode') }}</label>
<input type="checkbox" name="mygateway_test_mode" value="on"
{{ ($settings['mygateway_test_mode'] ?? '') === 'on' ? 'checked' : '' }}>
</div>
<div class="mb-3">
<label class="form-label">{{ __('API Key') }}</label>
<input type="text" name="mygateway_api_key" class="form-control"
value="{{ $settings['mygateway_api_key'] ?? '' }}">
</div>
<div class="mb-3">
<label class="form-label">{{ __('Secret Key') }}</label>
<input type="password" name="mygateway_secret_key" class="form-control"
value="{{ $settings['mygateway_secret_key'] ?? '' }}">
</div>
<button type="submit" class="btn btn-primary">{{ __('Save Settings') }}</button>
</form>
</div>
</div>
@endsection
Settings Naming Convention
All settings use the static_options table with consistent naming:
| Key | Purpose |
|---|---|
{prefix}_gateway | Enable/disable ('on'/empty) |
{prefix}_test_mode | Test mode toggle |
{prefix}_api_key | Provider API key |
{prefix}_secret_key | Provider secret key |
{prefix}_preview_logo | Gateway logo attachment ID |
The gateway only appears in checkout when {prefix}_gateway is non-empty.
Last updated: September 2026
Still stuck?
Our support team is ready to help you get set up.

