Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Payments Laravel Package

tomatophp/filament-payments

Filament Payment Manager lets you manage payments inside FilamentPHP apps with multi-gateway integration. Includes payments list, views, filters, gates, gate options, actions, a payment page, and drivers like Stripe, PayPal, Paymob, Tap, and more.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the package and its hard dependency

composer require tomatophp/filament-payments

# Spatie MediaLibrary is a required dependency (receipts/logos on payments)
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"

php artisan filament-payments:install

The filament-payments:install command is the single most important step — it publishes the payments migration, seeds the payment gate records, and registers the plugin assets. Run it after MediaLibrary is migrated, otherwise the install command can fail on the media table relationship.

2. Register the plugin on your panel

// app/Providers/Filament/AdminPanelProvider.php
->plugin(\TomatoPHP\FilamentPayments\FilamentPaymentsPlugin::make())

3. Where to look first

After install, before writing a single line of code, open the Payment Gates resource in your panel. That is where the package actually lives day-to-day: each gateway (StripeV3, Paypal, Paymob, Tap, Myfatoorah, Plisio, Cryptomus) is a row, and each row has Gate Options — the API keys, mode (test/live), and gateway-specific flags. A payment will not go through until a gate is is_active and its options are filled. Most "the package doesn't work" reports are empty gate options.

4. First use case — a one-off checkout link

The lowest-friction entry point is the Facade, because pay() returns a URL you can redirect to or hand to someone:

use TomatoPHP\FilamentPayments\Facades\FilamentPayments;
use TomatoPHP\FilamentPayments\Services\Contracts\{
    PaymentRequest, PaymentCustomer, PaymentBillingInfo, PaymentShippingInfo
};

return redirect()->to(
    FilamentPayments::pay(
        data: PaymentRequest::make(Order::class)
            ->model_id($order->id)
            ->currency('USD')
            ->amount($order->total)
            ->details('Order #'.$order->id)
            ->success_url(url('/success'))
            ->cancel_url(url('/cancel'))
            ->customer(PaymentCustomer::make($order->name)->email($order->email)->mobile($order->phone))
            ->billing_info(PaymentBillingInfo::make($order->address)->city($order->city)->country($order->country))
    )
);

PaymentRequest::make(Model::class) + model_id() is the morph pair the package uses to attach the payment back to your record. Get that right on day one — it's what makes the Payments resource useful later and what your webhooks/observers will query against.

Implementation Patterns

Table action instead of a custom page (the common case). For "pay this invoice row" flows, don't build a controller — attach the shipped action to any Filament table. The ->request() closure is evaluated per record, so you get the full DTO with row data:

use TomatoPHP\FilamentPayments\Filament\Actions\PaymentAction;

->actions([
    PaymentAction::make('payment')
        ->request(fn ($record) => PaymentRequest::make(Order::class)
            ->model_id($record->id)
            ->currency('USD')
            ->amount($record->total)
            ->details($record->items->pluck('product_id')->implode(', '))
            ->success_url(url('/success'))
            ->cancel_url(url('/cancel'))
            ->customer(PaymentCustomer::make($record->name)->email($record->account->email))
        )
        ->pay(),
])

Note the ->pay() — without it you get the action but no redirect handling. The action handles the redirect itself, so it works inside modals without extra wiring.

Subscriptions integration via hook, not controller. If you use tomatophp/filament-subscriptions, wire payment in boot() with FilamentSubscriptions::afterSubscription() and return the redirect. This keeps the payment gate check after the subscription row is created, so the payment's model_id points at a real subscription rather than a pending plan.

Treat the returned pay() value as the only legal source of payment links. Since v1.0.21, /pay/{trx} and /pay/{trx}/cancel are signed and expiring. Your patterns should be:

  • Persist nothing about the URL; generate it on demand via FilamentPayments::pay(...), or $payment->payUrl() / $payment->cancelUrl() if you already hold the model.
  • Never build /pay/{trx} by hand in a Blade template, email, or notification — it will 403.
  • Cancellation is a CSRF-protected POST /pay/{trx}/cancel (route payment.cancel.submit), not a GET. Any "cancel" button you write must be a form with @csrf.

Return JSON for API/SPA callers. FilamentPayments::pay(data: $request, json: true) gives you the URL as data instead of a redirect — the right pattern for an Inertia/axios frontend or a mobile client.

Let the admin UI do the reconciliation work. The Payments resource already groups and filters by status, so build your ops habit around it rather than writing your own reporting: filter by gate to spot a broken credential, group by status to find pending rows that never resolved. The Payment View page carries the media/receipt attachments — that's why MediaLibrary is mandatory.

Keep your own observer on the payment model for side effects (fulfilment, provisioning, receipts). success_url/cancel_url are browser redirects and can be abandoned mid-flight; the payment record's status is the durable signal.

Gotchas and Tips

The v1.0.21 breaking change will bite on upgrade. If you're coming from ≤1.0.20, every hardcoded payment link and every GET cancel link breaks at once. Audit for /pay/ string literals and route('...cancel') usage. Also review filament-payments.links (expiry window) — set it long enough for emailed links to survive, and filament-payments.enforce_owner if you need staff or guests to open a payment page on someone else's behalf. Owner enforcement on by default is safe but it will block your support team opening a customer's link.

Gate secrets live in the database. Gate Options are rows edited through the panel, not .env keys. That means: your DB backups are now credential backups, and any panel user who can edit gates can exfiltrate or swap live keys. Restrict the Payment Gates resource with policies and consider encrypting those columns at the app level.

Amounts are floats. v1.0.13 changed the wallet column from integer to float, and amounts flow through the DTO as plain numbers. For anything beyond toy values, cast explicitly and be aware that gateway APIs differ on minor units (Stripe wants cents, others want decimals). Verify per-driver behaviour before going live — this is the classic source of 100× or 0.01× charges.

Custom drivers are the weakest documented path. The release notes openly reference a community report that a custom payment built "following the example didn't work" (#6). Budget real time to read the driver contract in src/Services and the existing StripeV3/Paypal drivers rather than expecting a copy-paste template. Copy an existing driver end-to-end and change one thing at a time.

Publish before you patch.

php artisan vendor:publish --tag="filament-payments-config"
php artisan vendor:publish --tag="filament-payments-views"     # payment page markup
php artisan vendor:publish --tag="filament-payments-lang"      # translations, incl. error strings
php artisan vendor:publish --tag="filament-payments-migrations"

Publish the views if you need to brand the payment page — otherwise your next composer update silently reverts your customisations, or worse, your overrides drift out of sync with the signed-link logic added in 1.0.21. The package also requires filament-translations-component (added in 1.0.14); if the payment page renders raw keys, that dependency or your published lang files are the cause.

Debugging a stuck payment, in order: (1) is the gate active with complete options and the right mode; (2) does the payment row exist with the expected amount/currency; (3) did the browser come back to success_url but the row still says pending (→ your callback/webhook is unreachable, check APP_URL and that the route isn't behind auth); (4) is the link expired per filament-payments.links. A 403 on the pay page is almost always signature/expiry/owner — not a driver problem.

Maturity caveats. 55 stars, zero dependents, mostly single-maintainer with community PRs, and the roadmap still lists Paddle, Lemon Squeezy, Binance, PayTabs, Moyaser, Payfort and Fawery as unimplemented. Treat the shipped gateways as the supported surface, pin the version in composer.json, and write your own integration test that exercises one full sandbox payment per gateway you enable — there is no test suite here to catch a regression for you.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route