Skip to content

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 migrate

Configure per-bot via Settings:

Setting keyTypePurpose
billing.gateways.zibal.statusCHECKBOXMaster on/off switch
billing.gateways.zibal.merchantSENSITIVEZibal 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: CardGateway: Zibal
Payment happensInside the Telegram chatOn an external web page
VerificationManual (admin accept/reject)Automatic (verify() callback)
Button typecallback_dataurl
Uses CallbackQuery/StateAnswerYesNo — HTTP routes/controller instead
Sensitive credentialsNoneMerchant 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.