Development
Events
The events Paybob fires, their payloads, and how to listen to them.
Paybob fires a Laravel event whenever something important happens. Addons listen to them to send messages, sync other systems or keep their own records. Every event is fired after the change is saved, so a listener never sees a half-finished state.
Listening
In an addon, register listeners in your provider's register():
use App\Events\TransactionStatusUpdated;
public function register(): void
{
$this->listen(TransactionStatusUpdated::class, NotifyCustomer::class);
}$this->listen() only calls your listener when your addon is enabled in the event's Brand, and runs it inside that Brand. A listener that throws is logged, and the action that fired the event carries on.
Every event
| Event | Fires when |
|---|---|
App\Events\TransactionStatusUpdated | A transaction's status changes. |
App\Events\TransactionRefunded | A transaction is refunded, fully or partly. |
App\Events\InvoiceStatusUpdated | An invoice's status changes. |
App\Events\InvoiceEmailSent | An invoice email is sent successfully. |
App\Events\BrandContextChanged | The current Brand changes during a request or task. |
All properties are plain values: strings, numbers and nulls. Money amounts are decimal strings, such as "1250.00000000": use bcmath for any calculation, never floats.
The Brand on every event
Every event except BrandContextChanged carries its Brand:
| Member | Gives |
|---|---|
brandId, brandSlug, brandName | The Brand it happened in. |
brand() | The Brand model. |
merchant() | The Brand's General settings, display-ready: name(), logoUrl(), supportEmail(), addressLines(), socialLinks() and so on. |
TransactionStatusUpdated
Fires when a transaction's status changes:
- a gateway completes a payment;
- SMS verification completes a pending payment;
- someone clicks Approve or Bulk Approve (once per transaction actually approved);
- someone changes the status with Edit (only when it really changes).
Refunds fire TransactionRefunded instead, and Send IPN fires nothing.
| Property | Type | Description |
|---|---|---|
transactionId | int | The internal record ID, to load the full transaction if you need it. |
tId | string | Paybob's payment ID. |
trxId | ?string | The provider's transaction ID, if recorded. |
oldStatus | string | The status before: pending, completed, failed, canceled, voided, refunded or partially_refunded. |
newStatus | string | The status after, from the same list. |
amount | string | The transaction's base amount. |
currency | ?string | Such as USD. |
customerName, customerEmail, customerPhone | ?string | The customer's details on this payment. |
public function handle(TransactionStatusUpdated $event): void
{
if ($event->newStatus !== 'completed') {
return;
}
// $event->tId, $event->amount, $event->currency, $event->customerEmail …
}TransactionRefunded
Fires once for each refund, whether from the panel or the API.
| Property | Type | Description |
|---|---|---|
transactionId | int | The internal record ID. |
tId | string | Paybob's payment ID. |
trxId | ?string | The provider's transaction ID, if any. |
amount | string | This refund's amount. |
totalRefunded | string | The total refunded on this transaction so far, including this one. |
reason | string | The reason given for this refund. |
status | string | The resulting status: refunded or partially_refunded. |
currency | ?string | |
customerName, customerEmail, customerPhone | ?string |
Paybob keeps a refund total rather than a record of each refund, so this event is the only moment you see each individual refund. Store it yourself if you need the history.
InvoiceStatusUpdated
Fires when an invoice's status changes: Mark Paid, Mark Unpaid, Refund, Cancel (single or bulk), a status change on the edit page, or an invoice being paid automatically when its payment completes.
| Property | Type | Description |
|---|---|---|
invoiceId | int | The internal record ID. |
iId | string | The invoice ID, as in its public link. |
oldStatus, newStatus | string | paid, unpaid, refunded or canceled. |
grandTotal | string | The invoice's grand total. |
currency | ?string | |
customerName, customerEmail | ?string | |
actorId | ?int | The user who made the change, or null when it happened automatically. |
InvoiceEmailSent
Fires after an invoice email is accepted for sending. A failed send doesn't fire it.
| Property | Type | Description |
|---|---|---|
invoiceId | int | The internal record ID. |
iId | string | The invoice ID. |
customerName, customerEmail | ?string | The invoice's customer. |
receiverEmail | string | Where the email was actually sent. It can differ from the customer's email. |
subject | string | The subject as sent. |
renderedBody | string | The body as sent, with placeholders filled in. |
invoiceUrl | string | The invoice's public link. |
sentAt | string | When it was sent. |
actorId, actorEmail | ?int, ?string | Who sent it. |
BrandContextChanged
Fires when the current Brand changes: once per request when the Brand is known (from the panel's address, a custom domain or an API key), and when background work enters and leaves a Brand.
| Property | Type | Description |
|---|---|---|
brandId | ?int | The Brand now current, or null when there's none. |
brandSlug | ?string | Its slug. |
previousBrandId | ?int | The Brand before. |
Use it to switch per-Brand configuration, such as a mail transport or an API client. Register it with a plain Event::listen(), not $this->listen(), so it also runs in Brands where your addon is off and you can restore the default:
use App\Events\BrandContextChanged;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(BrandContextChanged::class, function (): void {
addon_enabled('my-addon')
? MyClient::configure(addon_settings('my-addon')->all())
: MyClient::reset();
});
}Your own events
Addons can define and fire their own events like any Laravel application, and other addons can listen to them:
namespace Addons\SmsNotifications\Events;
use Illuminate\Foundation\Events\Dispatchable;
class SmsSent
{
use Dispatchable;
public function __construct(
public readonly string $to,
public readonly string $message,
) {}
}SmsSent::dispatch($phone, $text);Give your event brandId, brandSlug and brandName properties and use App\Events\Concerns\CarriesBrand, and $this->listen() handles it per Brand like a core event. Wrap your own dispatch in try/catch and report(), so a broken listener never breaks your feature.
Webhooks are separate
Events are for code running inside Paybob. To notify an outside website or app, use the payment's webhook instead. It's described in the API reference.