Payment Hooks
The seven hooks where money truly arrives: the payment gates, the recorded payment row and automatic collection.
Overview
An invoice does not have to be paid in one go: part payments add up and the invoice turns paid on its own once the balance clears. So the payment hooks run more often than the invoice status hook.
The second point is who paid. On a payment made through a share link the payer has no identity; the account id the hook hands you is the invoice's owner, not the person paying.
Reference
Stopping a customer payment
Runs while a customer pays an invoice, before any money moves.
true there is no session and the payer is unknown.Hook::add('gate:invoice.client_pay', 10, function ($invoice, $uid, $method, $share) {
// On a shared payment the payer is unknown: shape your risk rules for that.
if ($share && (float) ($invoice['total'] ?? 0) > 5000)
return 'This amount cannot be paid through a share link.';
return null;
});Stopping a bulk payment
Runs while a customer pays several invoices together.
Hook::add('gate:invoice.bulk_pay', 10, function ($invoices, $cid, $method) {
if (count($invoices) > 50) return 'At most 50 invoices are paid at once.';
return null;
});Changing the payment row before it is written
Runs before the payment row is written. After that row the invoice can turn paid on its own.
owner_id, amount_in, currency, rate, fees, pmethod, transaction_id, paid_at.Hook::add('filter:invoice.payment_data', 10, function (&$payment_row, $invoice) {
// Carry your own reference; changing the amount decides the paid outcome.
$payment_row['transaction_id'] = Acme::ref($payment_row['transaction_id'] ?? '');
});Learning that a payment was recorded
Runs after the payment row was written. The invoice can still be unpaid at this point.
Hook::add('action:invoice.payment_recorded', 10,
function ($payment_id, $invoice_id, $payment) {
// It can be partial: check the balance before saying "invoice closed".
Accounting::received($invoice_id, (float) ($payment['amount_in'] ?? 0));
});Following a payment added
Runs after a payment was added to an invoice. Unlike the previous hook the values arrive separately.
Hook::add('action:invoice.payment_added', 10,
function ($invoice_id, $amount, $currencyId, $pmethod, $txn_id) {
Accounting::line($invoice_id, (float) $amount, (int) $currencyId, $pmethod);
});Following a payment deleted
Runs after a payment record was deleted.
0.Hook::add('action:invoice.payment_deleted', 10, function ($payment_id, $invoice_id) {
if ($invoice_id === 0) return; // tied to no invoice
Accounting::reversed($invoice_id, (int) $payment_id);
});Following an automatic collection
Runs after an automatic payment was attempted — whether it worked or not.
paid or unpaid. Read the attempt's success from here.outcome, amount, error), plus the starting and closing balance. Why a card failed is answered here.Hook::add('action:invoice.auto_payment_attempted', 10,
function ($invoice_id, $final_status, $result) {
// The card step's error arrives apart: tell the customer the real reason.
if ($final_status !== 'paid')
Dunning::failed($invoice_id, $result['card_step']['error'] ?? '');
});Following a bank transfer notice
Runs when a customer says they sent a transfer. No money has arrived: this is a claim, and the invoice stays unpaid.
Hook::add('action:invoice.bank_transfer_notified', 10, function ($id, $uid, $transfer) {
// No money yet, only a claim: match the statement on the reference.
Acme::watchStatement($transfer['rce'] ?? '', $id);
});Following a cash record
Runs when an income or expense record is entered into the books by hand.
income or expense.Hook::add('action:invoice.cash_recorded', 10,
function ($inex_id, $type, $amount, $currency) {
Acme::postToLedger($inex_id, $type, $amount, $currency);
});Following a refund through the gateway
Runs when an invoice is refunded through the payment gateway. The money really went back.
Hook::add('action:invoice.refunded_via_module', 10, function ($invoice, $pmethod) {
Acme::recordRefund((int) ($invoice['id'] ?? 0), $pmethod);
});Following a payment method change
Runs after the payment method on an invoice changes.
Hook::add('action:invoice.gateway_changed', 10,
function ($id, $oldPmethod, $newPmethod, $invoice) {
// Close the pending session at the old gateway.
if ($oldPmethod !== '') Acme::dropPendingSession($id, $oldPmethod);
});Following the auto-payment order of a card
Runs when a card moves within the auto-payment chain. That chain decides which card is tried first at renewal.
on joined the chain, off left it, promote moved to the front.Hook::add('action:payment.card_autopay_changed', 10, function ($uid, $cardId, $action) {
// An empty chain leaves the customer with no way to renew.
if ($action === 'off') Acme::warnIfChainEmpty($uid);
});Following a subscription charge
Runs after a subscription charge from the gateway is processed. The result need not be a success: rejected and repeated notices land here too.
paid, but it may equally be partial, duplicated or rejected. Do not assume success.Hook::add('action:subscription.payment_recorded', 10, function ($identifier, $result) {
// Do not assume success: the status may be a rejection.
if (($result['status'] ?? '') !== 'paid') return;
Acme::confirmCharge($identifier, (int) ($result['invoice_id'] ?? 0));
});Pitfalls
Part payments add up as separate rows and the hook runs on each. The invoice turns paid only once the balance clears, and that moment shows on the status hook. Writing "payment arrived, open the service" here opens it on an underpayment.
The account id on the payment gate is always the invoice owner. The person paying through a share link can be somebody else entirely, with no session. Answering "who paid this" from that id points at the wrong person.
One hands the payment over in a single array, the other gives the amount, currency, method and transaction number as separate parameters. Writing the wrong one drops the listener and looks like "it does not work".
The balance is tried first, then the card. The result array reports both separately. Before telling a customer "your card was declined", read what the balance step did: the invoice may have been partly cleared from it.
Related Articles
- Invoice Lifecycle Hooks
- Invoice and Payment Hooks
- Order Flow Hooks
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.