Payment Gateway Hooks
The eight hooks talking to a payment gateway: settlement, callbacks, stored cards and subscription collection.
Overview
Talking to a gateway runs both ways. We ask for a charge, and the gateway answers either at once or later as a callback. That second route can arrive from a browser or straight from their servers.
On stored cards the card number never reaches us: the gateway keeps a token and we hold only the display details (last four digits, brand, expiry). Those are all the hooks receive.
Reference
Following a settlement
Runs after the settlement with the gateway completed.
settled_status can be pending, meaning the money is not final yet.Hook::add('action:payment.settled', 10,
function ($module, $checkout, $result, $response) {
// It can be PENDING: open nothing before the money is final.
if (($checkout['data']['settled_status'] ?? '') !== 'successful') return;
Accounting::gatewaySettled($module->name, $checkout);
});Following a callback
Runs after the gateway's callback was interpreted.
already mark. already means "this callback was handled before".false the customer's browser brought it, and they may never have opened that page.Hook::add('action:payment.callback_returned', 10,
function ($module, $checkout, $settle, $is_s2s) {
// An empty checkout is an unmatched callback; check the repeat mark too.
if (!$checkout || !empty($settle['already'])) return;
Ops::note('gateway-callback', $module->name, $is_s2s ? 'server' : 'browser');
});Stopping a card being stored
Runs while a customer stores a card. The card number never reaches this hook; you decide on the account and module alone.
false where the module does not support it.Hook::add('gate:payment.card_add', 10, function ($uid, $moduleName, $autoPay) {
// The card number NEVER arrives here: decide on the account.
if (Acme::cardCount($uid) >= 5) return 'At most 5 cards are kept.';
return null;
});Following a card stored
Runs after the card was stored at the gateway.
Hook::add('action:payment.card_stored', 10, function ($userId, $cardId, $module, $card) {
// Display details only; never the number or the token.
Notify::securityEvent($userId, 'card-added', $card['ln4'] ?? '');
});Following the default card
Runs after a card was made the default.
Hook::add('action:payment.card_default_set', 10, function ($uid, $cardId) {
Audit::note('card-default', (string) $uid, (string) $cardId);
});Following a card removed
Runs after a stored card was removed.
Hook::add('action:payment.card_removed', 10, function ($uid, $cardId) {
// With the last card gone auto-payment stops quietly: tell the customer.
if (Acme::cardCount($uid) === 0) Notify::noCardLeft($uid);
});Stopping a subscription cancellation
Runs while a customer cancels the recurring agreement at the gateway.
Hook::add('gate:payment.subscription_cancel', 10, function ($uid, $id) {
// Cancelling the agreement closes no service, it stops the payments: warn first.
if (Acme::activeServices($uid) > 0 && !Acme::confirmed($uid))
return 'You have live services; please confirm the cancellation.';
return null;
});Following a subscription poll
Runs after the scheduled task queried the agreements at the gateway.
Hook::add('action:payment.subscription_polled', 10, function ($subscription, $result) {
// It runs on a schedule: react to a CHANGE, not to every poll.
Acme::syncSubscription($subscription, $result);
});Following gateway settings being saved
Runs after the settings of a payment module are saved.
Hook::add('action:payment.settings_saved', 10, function ($module_name, $config) {
// Carry the fact that it changed, NOT the configuration itself.
Acme::notifyOps('payment settings changed: ' . $module_name);
});Adding a bank logo
Runs while the logo of the detected bank is chosen. Add your own bank list here.
debit or credit.Hook::add('filter:payment.bank_logo', 10,
function ($bank_name, $card_brand, $card_type) {
// The key is a substring search: a short one catches other banks too.
return ['Acme Bank' => Acme::assetUrl('acme-bank.svg')];
});Taking over the card lookup
Runs while the bank and type are resolved from the first digits of a card. Fill it in and the outside service is never called.
Hook::add('filter:payment.bin_lookup', 10, function (&$result, $bin_number) {
// With a cache of your own there is no need to go outside.
$cached = Acme::binCache($bin_number);
if ($cached) $result = $cached;
});Replacing the payment pane with your own
Runs while the gateway pane is prepared at the payment step. Put your own screen here and the classic payment page is skipped.
Hook::add('filter:payment.gateway_pane', 10, function (&$html, &$ctx) {
// Filled means your screen, empty means the classic page.
$html = Acme::renderPane($ctx);
});Following an agreement being cancelled
Runs after a payment agreement is cancelled. No further charge arrives from it.
Hook::add('action:payment.subscription_cancelled', 10, function ($subscription_id, $sub) {
// The services behind it now have no automatic charge.
Acme::warnUnpaidRisk((int) ($sub['user_id'] ?? 0));
});Stopping a member being taken off an agreement
Runs before a service or add-on is taken off a payment agreement.
service or addon.Hook::add('gate:payment.subscription_member_remove', 10,
function ($uid, $subscription_id, $type, $mid, $member, $sub) {
// No removal while the commitment period still runs.
if (Acme::underCommitment($mid, $type)) return 'It cannot leave before the commitment ends.';
return null;
});Following a member being taken off
Runs after the member is taken off the agreement.
service or addon.Hook::add('action:payment.subscription_member_removed', 10,
function ($type, $id, $member, $sub, $history) {
// "Which agreement did it leave" lives in the old value.
Acme::detached($type, $id, (int) ($member['subscription_id'] ?? 0));
});Pitfalls
The settlement record can carry a pending status: the gateway took the transaction and has not finalised it. A listener opening a service without checking hands out a free service for a payment that later fails.
Where the module cannot tie a callback to a checkout, the hook receives an empty array. That marks a forged or unmatched callback. The same callback can also arrive again; a listener ignoring the "handled before" mark does its work twice.
The stored-card hooks carry display details only: the last four digits, the brand, the expiry. The number, the security code and the token never reach us. You cannot write a rule that reads the card; build the decision on the account.
Where a callback came from their servers, the gateway is speaking. From a browser, the customer may never have opened that page — closed it, lost the network, missed the redirect. Rest work that involves money on the server callback.
Related Articles
- Payment Hooks
- Invoice Lifecycle Hooks
- Order Flow Hooks
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.