Appearance
Gateway: Zibal
Current version: v0.0.6
telegram-bot-essentials/gateway-zibal (namespace TelegramBotEssentials\GatewayZibal) integrates Zibal — an Iranian online payment gateway — with Billing. Unlike Gateway: Card, this is a fully automated, redirect-based gateway: the buyer is sent to a real payment page and the invoice is marked paid automatically via a server-to-server callback.
Installation
bash
composer require telegram-bot-essentials/gateway-zibal
php artisan migrateConfigure per-bot via Settings:
| Setting key | Type | Purpose |
|---|---|---|
billing.gateways.zibal.status | CHECKBOX | Master on/off switch |
billing.gateways.zibal.merchant | SENSITIVE | Zibal merchant key (encrypted at rest) |
Both must be set for the gateway to appear on the invoice screen.
Flow
Because Zibal payment happens on Zibal's own web page (outside Telegram), this gateway is driven by HTTP routes, not CallbackQueries/StateAnswers — it registers none of either.
Invoice screen (Billing)
│ inline button → external URL: route('invoice.zibal.pay', $invoice->public_token)
▼
GatewayZibalController::pay($token)
│ converts invoice price to Rial: priceIn($invoice->price)->toIRR()
│ zibal()->paymentRequest($amountRial, callbackUrl)->execute()
│ creates a ToZibalAttempt with the returned trackId
│ billing()->attemptPayment($invoice, $attempt)
▼
redirect → https://gateway.zibal.ir/start/{trackId} (buyer pays on Zibal's page)
▼
Zibal redirects back to:
GatewayZibalController::callback($token)
│ zibal()->verify($trackId)->execute()
│ on success: $zibalAttempt->attemptSucceed() → Invoice::markAsPaid()
│ renders a result Blade view with a "back to bot" deep link (?start=invoice_{id})Because this runs outside any Telegram webhook request, the controller manually re-establishes tenancy and the webhook context before touching wHook() or billing/invoice state:
php
tenancy()->initialize($invoice->bot);
wHook()->setBot($invoice->bot);
wHook()->setApi(new Api($invoice->bot->bot_token));
wHook()->setUser($invoice->botUser);
wHook()->setUpdate(Update::make(request()->all()));This is the pattern to copy for any gateway or webhook that arrives via a plain HTTP route instead of the Telegram webhook.
The Zibal service
Helper: zibal(). Thin wrapper exposing Zibal's two API calls as fluent method-object pairs (src/Services/Methods/PaymentRequest.php, Verify.php):
php
zibal()->paymentRequest(int $amountRial, string $callbackUrl)->execute();
// => ['result' => 100, 'trackId' => ..., 'message' => ...]
zibal()->verify(string $trackId)->execute();
// => ['status' => 1, 'amount' => ..., 'message' => ...]result === 100 signals a successful payment request; status === 1 signals a successful verification. Both are checked explicitly in the controller — don't assume a non-error HTTP response means success.
ToZibalAttempt
Extends Billing's abstract PaymentAttempt. Columns: track_id, amount, received_amount. Both lifecycle hooks are currently no-ops (attemptSucceedHook/attemptFailedHook) — Invoice::markAsPaid()/markAsFailed() (triggered by attemptSucceed()/attemptFailed()) already does the work that matters.
Registering with Billing
php
gateways()->addGateway(new Gateway(
key: 'zibal',
label: 'Zibal',
inlineButtonGenerator: function (Invoice $invoice) {
if (!settings()->get('billing.gateways.zibal.status') || !settings()->get('billing.gateways.zibal.merchant')) {
return null;
}
return Keyboard::inlineButton([
'text' => __('tbe-billing::invoice.summary.keys.to_zibal', [
'price' => number_format(priceIn($invoice->price)->toIRT()),
]),
'url' => route('invoice.zibal.pay', ['token' => $invoice->public_token]),
]);
},
));Note this button uses 'url' (an external link), not 'callback_data' — appropriate for a gateway whose flow leaves Telegram entirely, in contrast to Gateway: Card's callback_data button.
Comparing the two gateways
| Gateway: Card | Gateway: Zibal | |
|---|---|---|
| Payment happens | Inside the Telegram chat | On an external web page |
| Verification | Manual (admin accept/reject) | Automatic (verify() callback) |
| Button type | callback_data | url |
| Uses CallbackQuery/StateAnswer | Yes | No — HTTP routes/controller instead |
| Sensitive credentials | None | Merchant key (SENSITIVE setting) |
If you're integrating another redirect-based provider (an alternative to Zibal), this package — not Gateway: Card — is the template to follow.