Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/own-pay/OwnPay-Documentation/llms.txt

Use this file to discover all available pages before exploring further.

Hooks are the primary integration surface of OwnPay’s plugin system. They let your plugin modify data flowing through the core (filter hooks) and react to events that happen inside it (action hooks) without changing any core file. Every hook in OwnPay is fired by EventManager, which handles error isolation, brand-context scoping, and priority ordering.

Hook Types

Action Hooks

Fire-and-forget. The core fires an action at a defined point; any listener registered on that hook runs. The return value is discarded. Use actions to send notifications, write logs, update external systems, or enqueue background jobs.

Filter Hooks

Pipeline mutation. Each listener receives the current value, may transform it, and must return it. After all listeners run, the final value is returned to the caller. Use filters to modify data before it is saved or sent, override templates, or augment context arrays.

Registration API

All hook interaction happens through the EventManager instance injected into PluginInterface::register(). Never instantiate EventManager directly.

addAction() — Register an Action Hook

$events->addAction(string $hook, callable $callback, int $priority = 10): void
public function register(EventManager $events, Container $container): void
{
    $events->addAction('payment.transaction.completed', [$this, 'onPaymentCompleted'], priority: 10);
}

public function onPaymentCompleted(array $txn): void
{
    // $txn contains all columns from op_transactions
    // Send SMS, dispatch webhook, etc.
}

addFilter() — Register a Filter Hook

$events->addFilter(string $hook, callable $callback, int $priority = 10): void
The first argument to the callback is always the value being filtered. The callback must return a value of the same type it received.
$events->addFilter('payment.fee.calculate', function (string $fee, array $context): string {
    // Waive the fee for transactions over 50,000 BDT
    if (bccomp($context['amount'] ?? '0', '50000.00', 2) >= 0) {
        return '0.00';
    }
    return $fee;
}, priority: 5); // lower priority number = runs earlier

removeAction() / removeFilter() — Deregister a Callback

$removed = $events->removeAction('payment.transaction.completed', [$this, 'onPaymentCompleted']);
$removed = $events->removeFilter('payment.fee.calculate', $myCallback);
// Returns true if found and removed

Inspection and Debugging

// Check if any listener is registered
$events->hasAction('payment.transaction.completed'); // bool
$events->hasFilter('payment.fee.calculate');          // bool
$events->hasHook('checkout.head');                    // bool (action or filter)

// Count how many times a hook has fired this request
$events->getFireCount('payment.transaction.completed'); // int

// Identify which plugin is currently executing
$events->getActiveOwner(); // string — plugin slug or 'core'

// Get all registered hooks and listener counts
$events->getRegisteredHooks();
// ['payment.transaction.completed' => ['actions' => 3, 'filters' => 0], ...]

// Inspect listeners on a specific hook
$events->inspectHook('payment.transaction.completed');
// [['callable' => ..., 'priority' => 10, 'owner' => 'my-plugin'], ...]

Priority and Ordering

Listeners run in ascending priority order. A listener with priority 5 runs before one with priority 10. Multiple listeners at the same priority run in registration order.
// This runs first (priority 5)
$events->addFilter('payment.amount.calculate', $earlyModifier, priority: 5);

// This runs second (priority 10, the default)
$events->addFilter('payment.amount.calculate', $defaultModifier);

// This runs last (priority 20)
$events->addFilter('payment.amount.calculate', $lateAudit, priority: 20);
All hook registrations must happen inside register(), not boot() or the constructor. Registrations outside register() are not attributed to your plugin slug and bypass brand-context activation checks — meaning they may fire even when your plugin is disabled.

Execution Model and Error Isolation

Every listener invocation is wrapped in a try/catch(Throwable). If a plugin listener throws any exception, EventManager:
  1. Logs the error: [OwnPay] Hook error in "{hook}" (owner: {slug}): {message} in {file}:{line}.
  2. Continues to the next listener without crashing.
A plugin failure will never crash a critical transaction flow. Exception: If a listener modifies the db.query.before filter and the resulting SQL fails PluginSandbox::validateSql(), a RuntimeException is re-thrown and halts the request. This is intentional — a plugin attempting to access core tables must be stopped.

Hook Catalogue

The following is the complete hook registry from config/hooks.php. Hooks are verified against every call site in the codebase.

System Lifecycle

HookTypeSourceDescription
system.bootActionKernelFired on every request after the DI container boots, before middleware.
system.shutdownActionKernelFired after the response is transmitted. Flush caches, close cursors.
system.requestFilterKernelFilter the inbound Request object. Return a Request. Rewrite paths or enforce rate limits.
system.responseFilterKernelFilter the outbound Response before it is sent. Parameters: Response, Request.
system.middleware.pipelineFilterKernelFilter the global middleware class array. Append custom firewall middleware.
system.route.matchedActionRouterFired when a route is resolved.
system.routes.registerActionRouterFired when all routes are registered. Add dynamic routes here.
system.cron.beforeActionCronJobRunnerFired before cron execution starts.
system.cron.afterActionCronJobRunnerFired after cron execution completes.

Database

HookTypeDescription
db.query.beforeFilterFilter ['sql' => string, 'params' => array] before PDO execution. Security: plugin-modified SQL is validated by PluginSandbox::validateSql(). Blocked queries throw a re-thrown RuntimeException.
db.query.afterActionFired after a query executes. Useful for query logging.

Authentication & Session

HookTypeParametersDescription
auth.login.beforeFilterbool $allowed, string $email, string $ipReturn false to block the login before credentials are validated.
auth.login.attemptActionstring $email, string $ipFired when a login is attempted.
auth.login.successActionarray $user, string $ipFired on successful login. Dispatch security notifications or reset brute-force counters.
auth.login.failedActionstring $email, string $ipFired on credential mismatch. Trigger account lockout logic.
auth.logoutActionint $userIdFired when a session is terminated.
auth.forgot_passwordActionFired when a password reset is requested.
Example — Block a login from a specific IP:
$events->addFilter('auth.login.before', function (bool $allowed, string $email, string $ip): bool {
    $blocklist = ['192.168.1.100', '10.0.0.55'];
    if (in_array($ip, $blocklist, true)) {
        return false;
    }
    return $allowed;
});

Payment & Transactions

HookTypeParametersDescription
payment.transaction.before_createFilterarray $data, int $merchantIdModify column map before op_transactions insert.
payment.transaction.createdActionarray $transactionFired when a transaction enters pending status.
payment.transaction.completedActionarray $transactionFired on successful payment. Primary hook for SMS, email, webhooks, order fulfilment.
payment.transaction.failedActionarray $transactionFired on decline or timeout.
payment.transaction.cancelledActionarray $transactionFired when the buyer cancels checkout.
payment.amount.calculateFilterstring $amount, array $contextFilter currency and rounding before processor payload. Must return a BCMath-safe string — never cast to float.
payment.fee.calculateFilterstring $fee, array $contextApply custom tiered fee rules. Context includes amount, currency, merchant_id, gateway_slug.
payment.intent.createdActionarray $intentFired when a checkout intent session is generated.
payment.intent.expiredActionarray $intentFired when checkout intents time out.
refund.createdActionFired when a refund is initiated.
transaction.status.changedActionFired when a transaction status changes.
ledger.entry.createdActionarray $entryFired after a balanced double-entry journal is committed. Do not post additional ledger entries from inside this hook.
dispute.openedActionarray $disputeFired when a chargeback registers. Place merchant balance on hold.
dispute.resolvedActionarray $disputeFired when dispute resolves. Release or debit funds.
payment.refund.reconciliation_failedActionFired when a refund reconciliation job encounters an error.
Example — Add a fee surcharge:
$events->addFilter('payment.amount.calculate', function (string $amount, array $context): string {
    // Add a $1.00 surcharge to all transactions
    return bcadd($amount, '1.00', 2);
}, priority: 10);

Gateway & Checkout

HookTypeParametersDescription
gateway.capture.beforeFilterarray $params, string $gatewaySlug, int $merchantIdFilter transaction params sent to gateway adapters. Return shape must preserve amount, currency, trx_id, redirect_url, cancel_url.
gateway.capture.afterActionstring $slug, array $resultFired after initiate(). Log the payment attempt.
gateway.webhook.{slug}ActionWebhookPayload $payloadDynamic hook fired after webhook signature verification. {slug} is the gateway slug (e.g., gateway.webhook.stripe).
gateway.manual.renderFilterstring $html, array $gatewayFilter manual payment gateway HTML markup.
gateway.manual.verifyFilterarray $result, array $gateway, array $submittedDataFilter manual slip verification results.
checkout.beforeActionarray $transactionFired before checkout template is parsed.
checkout.renderFilterarray $dataFilter Twig context before checkout renders.
checkout.templateFilterstring $templatePathOverride the checkout template path.
checkout.status.templateFilterstring $templatePathOverride the result page template path.
checkout.payment_link.templateFilterstring $templatePathOverride the payment link page template path.
checkout.intent.renderFilterarray $dataFilter variables before payment intent screen.
checkout.gateway.selectedActionarray $transaction, string $gatewayFired when the buyer selects a gateway.
checkout.cancelledActionFired when checkout is cancelled.
checkout.manual_verify.submittedActionarray $transaction, array $proofFired when a manual receipt slip is submitted.
checkout.headActionNoneInject CSS/fonts into the checkout <head>.
checkout.footerActionNoneInject JS/tracking scripts into checkout footer.
checkout.csp.sourcesFilterarray $sourcesAppend gateway domains to CSP. Keys: script_src, style_src, frame_src, connect_src, font_src, img_src.

Admin Panel

HookTypeDescription
admin.headActionInject CSS/JS into the admin <head>.
admin.footerActionInject scripts into the admin </body>.
admin.menu.registerActionEcho HTML link tags to inject sidebar navigation entries.
admin.dashboard.beforeActionFired before the dashboard body renders.
admin.dashboard.bottomActionFired at the bottom of the dashboard. Inject widget HTML.
admin.dashboard.statsFilterarray $stats — Inject custom metrics into dashboard stat cards.
admin.page.before_renderFilterarray $data, string $templatePath — Augment Twig context before any admin page renders.
admin.page.after_renderFilterstring $html, string $templatePath — Modify final rendered HTML before transmission.
admin.template.resolveFilterstring $template, array $data — Override admin templates (advanced).
admin.template.dataFilterarray $data, string $template — Filter admin page context variables.
admin.settings.tabsActionEcho an HTML <li> tab trigger to add a custom settings tab.
admin.settings.generalActionInject form fields into the General settings tab.
admin.settings.brandingActionInject form fields into the Branding tab.
admin.settings.paymentActionInject form fields into the Payment tab.
admin.settings.emailActionInject form fields into the Email tab.
settings.savedActionFired when settings are saved. Clear plugin caches here.
Example — Inject a dashboard metric:
$events->addFilter('admin.dashboard.stats', function (array $stats): array {
    $stats['emails_sent_today'] = $this->countEmailsToday();
    return $stats;
});

Communication & Messaging

HookTypeParametersDescription
communication.sms.sendActionint $merchantId, string $recipientNumber, array $deliveryResultFired after outbound SMS delivery.
communication.mail.sendActionint $merchantId, array $messagePayload, array $deliveryResultFired after outbound email delivery.
communication.channelsFilterarray $channelsRegister custom channels (Telegram, Slack, push). Return the modified channels array.
communication.template.renderFilterstring $html, array $variablesIntercept SMS/email template compilation before delivery.
mfs.templatesFilterarray $templatesAdd custom regex schemas for MFS SMS parsing.

Customers & Invoices

HookTypeDescription
customer.createdActionFired when a customer is created in CustomerPiiService.
customer.updatedActionFired when customer data is updated.
customer.deletedActionFired when a customer is deleted (PII purge).
invoice.createdActionFired when an invoice is created.
invoice.updatedActionFired when an invoice is updated.
payment_link.createdActionFired when a payment link is created.
payment_link.updatedActionFired when a payment link is updated.

Mobile & SMS

HookTypeDescription
mobile.device.pairedActionFired when a mobile app pairs with a brand.
mobile.device.revokedActionFired when a device’s API access is revoked.
sms.received.beforeActionFired when an inbound SMS arrives.
sms.received.afterActionFired after inbound SMS processing completes.
mobile.sms.matchedActionFired when an SMS matches a transaction pattern.

Webhook Delivery

HookTypeDescription
webhook.delivery.successActionFired when an outbound merchant IPN completes successfully.
webhook.delivery.failedActionFired when outbound IPN delivery fails or reaches retry limits.
domain.mappedActionFired when a white-label custom domain is linked.
domain.verifiedActionFired when a custom domain passes DNS verification.
domain.removedActionFired when a custom domain mapping is deleted.
domain.resolveFilterFilter resolved domain mapping details.

Reporting & Export

HookTypeParametersDescription
report.dataFilterarray $reportData, array $queryParamsFilter financial grid data before rendering.
export.rowFilterarray $rowTransform or redact individual CSV/export rows.
audit.log.createdActionarray $entryFired when an admin audit log entry is created.

Plugin Lifecycle

HookTypeParametersDescription
plugin.before_installActionstring $zipPathFired before a plugin ZIP is extracted.
plugin.installedActionstring $slug, array $manifestFired after successful installation.
plugin.before_activateActionstring $slug, int $brandIdFired before activation and migrations.
plugin.activatedActionstring $slug, int $migrationsCount, int $brandIdFired after activation completes.
plugin.before_deactivateActionstring $slug, int $brandIdFired before the plugin is suspended.
plugin.deactivatedActionstring $slug, int $brandIdFlush caches here. Do not drop tables.
plugin.before_uninstallActionstring $slugFired before plugin files are deleted.
plugin.uninstalledActionstring $slugFired after deletion and settings purge.
plugin.settings.savedActionFired when plugin settings are saved. Clear caches.
plugins.before_loadActionFired before plugin loading begins.
plugins.after_loadActionFired after all plugins have loaded.

Auto-Updater

HookTypeDescription
update.availableActionFired when a newer OwnPay core version is detected.
update.beforeActionFired before a platform update starts. Trigger data backups.
update.afterActionFired after a successful update.
update.failedActionFired when an update execution exception occurs.
update.rollbackActionFired when the system rolls back to a restoration point.

Complete Working Examples

Sending an Outbound HTTP Request on Payment

public function register(EventManager $events, Container $container): void
{
    $events->addAction('payment.transaction.completed', function (array $txn) use ($container): void {
        $settings   = $container->get(\OwnPay\Repository\SettingsRepository::class);
        $webhookUrl = $settings->get('plugin.my-plugin', 'webhook_url') ?? '';

        if ($webhookUrl === '') {
            return;
        }

        $payload = json_encode([
            'event'    => 'payment.completed',
            'trx_id'   => $txn['trx_id']   ?? '',
            'amount'   => $txn['amount']    ?? '',
            'currency' => $txn['currency']  ?? '',
        ]);

        $ch = curl_init($webhookUrl);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 5,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS     => $payload,
        ]);
        curl_exec($ch);
        curl_close($ch);
    }, priority: 15);
}

Adding a Custom Settings Tab

$events->addAction('admin.settings.tabs', function (): void {
    echo '<li><button data-tab="my-plugin-tab">My Plugin</button></li>';
});

Hook Usage Guidelines

RuleDetail
Register in register() onlyHooks registered in boot() bypass brand-context activation checks.
BCMath for financial valuespayment.amount.calculate and payment.fee.calculate filters must use bcadd()/bcmul()/bcdiv(). Never cast to float.
Keep filters pureFilter callbacks should take input and return output. Avoid mutating $this state.
Respect filter return typeReturning null from a string filter corrupts downstream logic.
Escape echoed HTMLAction hooks that echo must escape user-sourced values with htmlspecialchars($val, ENT_QUOTES, 'UTF-8').
Never post to the ledger from hooksDo not call LedgerService::postEntries() from inside a hook callback.

Build docs developers (and LLMs) love