Development
Gateway reference
Every manifest key, method, helper and rule a gateway package can use.
Everything a gateway package can use. For a step-by-step introduction, start with Gateway development. Every helper below is a protected method on App\Support\Gateway\Gateway, called as $this->… from inside your ProcessPayment class.
Metadata
public static function metadata(): GatewayMetadata
{
return GatewayMetadata::make()
->name('My Gateway') // required
->logo('assets/logo.png') // relative to the package
->currency('USD') // required: exactly one supported currency code
->api() // required: api(), manual('sender-id') or custom()
->version('1.0.0'); // required: semantic version
}An unknown currency code makes the package invalid. A package with an invalid metadata(), or the wrong namespace, isn't listed in Create New; check the log for the reason.
Configuration forms
config() returns a Form of Fields and, optionally, Tabs:
use App\Support\Gateway\Forms\Field;
use App\Support\Gateway\Forms\Form;
use App\Support\Gateway\Forms\Tab;Return null (the default) for no Configuration section.
Field types
| Call | Renders as |
|---|---|
Field::make('key') | Text input (the default) |
->textarea() | Multi-line text |
->password() | Password input, encrypted when saved |
->number() | Number input |
->select([value => label]) | Dropdown |
->radio([value => label]) | Radio buttons |
->toggle() | On/off switch |
->image() | Image upload |
->file([extensions]) | File upload |
->repeater() | A list of text values the admin can add to and remove from |
Modifiers
| Modifier | Effect |
|---|---|
->label(), ->placeholder(), ->helperText() | Text around the field |
->default($value) | Initial value |
->required() | Must be filled in |
->disabled() | Read-only |
->columnSpanFull() | Full width |
->revealable() | Show/hide button on a password field |
->suffixCopy() | Copy-to-clipboard button |
->value($computed) | A read-only computed value, such as a callback URL. Never saved. |
->encrypted() / ->encrypted(false) | Encrypt a non-password field, or store a password field in plain text |
Field keys are unique within the form. Read saved values with $this->configValue('key', $default).
Lists of values
->repeater() lets the admin enter several values, such as several wallet numbers. configValue() returns a plain list of strings:
Field::make('wallet_numbers')
->repeater()
->label('Wallet Numbers')
->placeholder('01XXXXXXXXX')
->required(),$numbers = $this->configValue('wallet_numbers') ?? [];Repeater values are never encrypted, so don't use them for secrets.
Tabs
use Filament\Support\Icons\Heroicon;
return Form::make([
Tab::make('Account')->icon(Heroicon::OutlinedKey)->fields([
Field::make('api_key')->required(),
]),
Tab::make('Verification')->fields([
Field::make('verification_method')->select([...]),
]),
]);All tabs form one tab bar. A tab's icon is optional: a Heroicon case, or a name such as 'heroicon-o-key'. An unknown icon name is ignored and logged.
Showing fields conditionally
Field::make('payment_window_minutes')
->number()
->visibleWhen('verification_method', ['sender_number', 'unique_amount']);
Field::make('live_secret')->password()->hiddenWhen('sandbox', true);
Field::make('webhook_secret')->password()->visibleWhen(
fn (callable $get): bool => $get('mode') === 'api' && ! $get('sandbox')
);- One value, or a list meaning "any of these". No value means
true. Comparisons are strict:trueisn't'true'. - A callback reads other fields with
$get('key')and returns a boolean. It must not change anything. - Several conditions on one field must all pass.
- Hidden fields aren't validated or saved, and their previously saved values (including secrets) are kept. Visibility is presentation only: still check the configuration in your runtime code.
Runtime helpers
The transaction and settings
| Helper | Returns |
|---|---|
payment() / transaction() | The current transaction. null in ipn(), which has no transaction in its address. |
totalAmount($payment) | What the customer must pay: converted, with discounts and fees, rounded to 2 decimals. |
financial() | Paybob's money calculator: totalAmount(), netAmount(), maxRefundable(), format(). |
configValue($key, $default) | A configuration value, decrypted. |
setting($key, $default) | A raw General settings value by key, such as general.site_name. Prefer merchant(). |
merchant() | The Brand's display-ready profile. See below. |
merchantName(), merchantLogoUrl() | Shorthands for the Brand's name and logo. |
faqs() | The Brand's active FAQs. |
gateway() | The installed gateway record. |
gatewayId(), gatewaySlug() | Its g_id, and your package's slug. |
checkoutInitializedAt() | When the customer first opened your checkout for this transaction (a Carbon date). Never changes on reload. |
Addresses and responses
| Helper | Returns |
|---|---|
ipnUrl() | Your IPN address: /{brand}/gateways/{g_id}. Register this with your provider. |
gatewayCheckoutUrl($payment) | Your checkout address, to post forms back to. |
statusUrl($payment) | Your status address, the usual provider return URL. |
checkoutUrl() | Paybob's checkout page, where the customer picks a gateway. Use it for "choose another method". |
redirectToStatusURL($payment) | A redirect to your status page. |
redirectToCheckout($payment) | A redirect to your checkout page. |
view($name, $data) | Renders views/{name}.blade.php. |
asset($path) | The public URL of a file in assets/. |
Inside views, gateway_assets('your-slug', 'images/qr.png') does the same as asset().
Merchant profile
$this->merchant() returns the Brand's General settings, every value ready to display:
$m = $this->merchant();
$m->name(); // never null
$m->logoUrl(); // never null: falls back to a default logo
$m->faviconUrl(); // never null
$m->defaultCurrency(); // 'USD'
$m->currencySymbol();
$m->addressLines(); // non-empty address lines, ready to print
$m->countryName();
$m->supportEmail(); // null when not set
$m->supportPhone();
$m->supportWebsite(); // includes https://
$m->socialLinks(); // ['facebook' => 'https://facebook.com/…', …], filled-in profiles only
$m->googleTagManagerId();
$m->metaDescription();Everything except the name, logo, favicon and symbol is null (or empty) when not set, so check before showing it.
Payment log
Store your own data on the transaction, such as a provider reference, under your gateway's entry:
$this->updatePaymentLog(['provider_reference' => $reference]);
$reference = $this->paymentLog('provider_reference');
$all = $this->paymentLog();- Values must be JSON-serialisable. This is plain storage: never put secrets in it.
updatePaymentLog()merges top-level keys and leaves other gateways' entries alone.- Some keys are reserved and can't be overwritten:
initialize_timestamp,customer_number,sender_registration,unique_amount,sms_after_id.
Completing payments
markAsCompleted()
$result = $this->markAsCompleted(
payment: $payment->t_id, // required
transactionId: $providerId, // optional: the provider's transaction ID
transactionSlip: $slipUrl, // optional: a payment slip image
senderNumber: $customerNumber, // optional
paymentMethodInfo: [...], // optional: defaults to your manual() identity
);
if ($result['completed']) {
// success
} else {
// $result['reason']: 'not_eligible', 'duplicate_transaction_id',
// 'no_exchange_rate', 'amount_out_of_range', …
}Completes the payment through the same path as every other completion in Paybob: it saves the converted amount and fees, marks the transaction Completed, settles its invoice, fires the TransactionStatusUpdated event and sends the merchant's webhook immediately. It works straight from an unpaid transaction: you don't need markAsPending() first.
It's safe to call twice: an already-completed transaction returns completed: false with not_eligible and nothing changes. Always check completed before showing success. Pass arguments by name.
markAsPending()
$result = $this->markAsPending(
$payment->t_id,
transactionId: $submittedId, // optional
senderNumber: $customerNumber, // optional
transactionSlip: $slipUrl, // optional
paymentMethodInfo: [...], // optional
);
// ['pending' => bool, 'reason' => …]Moves the transaction to Pending for the merchant to review, for example when a customer submits a transaction ID that doesn't match any SMS yet. A claimed transaction ID is checked for duplicates and saved together with the status.
Call it only when your checkout page is done with the transaction: a pending transaction never shows your checkout page again. Don't call it just to start an API payment.
Exceptions
To stop with a message for the customer, throw one of Paybob's gateway exceptions, such as App\Support\Gateway\Exceptions\GatewayPaymentException or GatewayVerificationException, with an internal message (logged) and a public message (shown):
throw new GatewayVerificationException(
"Provider returned status {$status} for {$reference}", // logged
'This payment could not be verified.', // shown to the customer
);The customer sees a "Gateway Error" page with the public message only.
SMS verification
These helpers check payments against the SMS the merchant received (see SMS verification). They only accept an SMS that:
- is Approved and not used yet;
- came from your sender ID's SMS filter, in the filter's currency;
- reached Paybob after the customer first opened your checkout.
On success they mark the SMS Used, so it can't pay twice. Pass consume: false to check without using it.
verifyTransactionID()
$result = $this->verifyTransactionID(
transactionId: request()->input('transaction_id'),
expectedAmount: $this->totalAmount($payment),
);| On the result | Gives |
|---|---|
verified() | true when a matching SMS was found |
reason() | Why not: not_found, amount_mismatch, currency_mismatch, sender_mismatch, type_mismatch, already_used, duplicate_transaction_id |
transactionId() | The transaction ID |
senderNumber(), amount(), balance(), reference(), smsDatetime() | Values read from the SMS |
smsData() | The full SMS record |
A transaction ID already recorded on another payment for this gateway returns duplicate_transaction_id. Payment tolerance applies to the amount when the merchant has it on.
Automatic verification
The customer doesn't type anything: your checkout page polls, and Paybob completes the payment when the SMS arrives. Both methods use a payment window: build it from checkoutInitializedAt() and a payment_window_minutes setting.
Unique amount. When the merchant sets your verification_method field to unique_amount, Paybob reserves a unique amount for the checkout: the normal total plus 0.01 to 2.00, different from every other open checkout on the gateway. totalAmount($payment) already returns it. Tell the customer to send exactly that amount, then poll:
$result = $this->verifyByAmount(expectedAmount: $this->totalAmount($payment));
if ($result->verified()) {
$this->markAsCompleted(
payment: $payment->t_id,
transactionId: $result->transactionId(),
senderNumber: $result->senderNumber(),
);
}
// reason(): 'not_found' (keep waiting), 'ambiguous', 'expired', 'amount_mismatch', …Only SMS received during the reservation's window count, and payment tolerance never applies.
Sender number. With verification_method set to sender_number, ask the customer for the number they'll pay from and register it, then poll:
$error = $this->registerCustomerNumber($number, $windowMinutes);
// null on success, or 'invalid_sender_number', 'registration_expired',
// 'sender_number_locked', 'sender_number_in_use'
$result = $this->verifyBySenderNumber(
senderNumber: $number,
expectedAmount: $this->totalAmount($payment),
);A sender-number checkout also reserves a unique amount, so the SMS must match both the number and that exact amount.
A provider chosen in configuration
Some gateways, such as an interoperable QR code, can be paid into different accounts. Declare ->custom() instead of ->manual(), add a select field named sms_sender_id whose values are SMS filter sender IDs, and pass the chosen provider to every SMS helper:
$provider = ['sender_id' => $this->configValue('sms_sender_id')];
$this->verifyTransactionID($id, $provider);
$this->verifyByAmount($this->totalAmount($payment), paymentMethodInfo: $provider);
$this->markAsCompleted(payment: $payment->t_id, transactionId: $id, paymentMethodInfo: $provider);gateways/bangla-qr/ is the working example.
Languages
| Helper | Does |
|---|---|
lang($key, $replace, $default) | Text from lang/{locale}.php, with :placeholder replacement. |
trans($value) | A string as is, or a ['en' => …, 'bn' => …] array resolved to the current language. |
locale() | The customer's language: en, bn, hi or ur. |
supportedLocales() | Every supported language, for a language menu. |
setLocale($code) | Changes the customer's language for every Paybob page. |
Language falls back from the customer's choice to the Brand's default language, then English, then the key itself. Adding ?lang=bn to your checkout or status address switches language automatically.
Debug logging
To help with provider integration, you can offer a Debug setting. When it's on, log the provider's responses (never the requests that carry credentials) with an event name and the g_id, t_id and provider reference:
if ($this->configValue('debug') === 'enable') {
Log::info('gateway.my-gateway.payment_status_response', [
'g_id' => $this->gatewayId(),
't_id' => $payment->t_id,
'reference' => $reference,
'response' => $response->json(),
]);
}When Debug is off, write nothing. Debug never changes what the customer sees.
Package rules
| Rule | Detail |
|---|---|
| Slug | The folder name: ^[a-z0-9]+(?:-[a-z0-9]+)*$ |
| Namespace | Gateways\{StudlySlug}, such as Gateways\BkashPersonal |
| Required files | ProcessPayment.php and an assets/ folder |
| ZIP | One top-level folder named after the slug, up to 50 MB |
| Updates | A higher version() replaces the files; installed gateways keep their settings. The same or a lower version is refused. |
| Several copies | Admins can create several gateways from one package, each with its own g_id and settings. |
| Dependencies | No Composer: ship them in the package or use what Paybob includes |