tigusigalpa/telegram-wallet-php

Complete PHP SDK for Telegram Wallet Pay API v1.2.0 with first-class Laravel support

Maintainers

Package info

github.com/tigusigalpa/telegram-wallet-php

pkg:composer/tigusigalpa/telegram-wallet-php

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-15 07:03 UTC

This package is auto-updated.

Last update: 2026-04-15 07:46:16 UTC


README

Telegram wallet PHP/Laravel

PHP Version License Latest Version

Accept crypto payments in your PHP app or Laravel project. This SDK wraps the Telegram Wallet Pay API, letting your users pay with TON, USDT, BTC, and NOT — right inside Telegram.

📖 Full documentation available on Wiki

Why This Package?

Building a Telegram bot with payments? We've got you covered:

  • Laravel-first — Service Provider, Facade, Middleware out of the box
  • Works anywhere — Use standalone in any PHP 8.1+ project
  • Secure webhooks — HMAC-SHA256 signature verification built-in
  • Type-safe — Modern PHP with enums, DTOs, and typed exceptions
  • Tested — Comprehensive test suite you can trust

Installation

Install via Composer:

composer require tigusigalpa/telegram-wallet-php

Requirements

  • PHP 8.1 or higher
  • ext-json extension
  • Laravel 9.x, 10.x, 11.x, 12.x, or 13.x (optional, for Laravel integration)

Quick Start

Here's how simple it is to create a payment:

<?php

use Tigusigalpa\TelegramWallet\WalletPayClient;
use Tigusigalpa\TelegramWallet\DTO\CreateOrderRequest;
use Tigusigalpa\TelegramWallet\DTO\MoneyAmount;

// Initialize with your API key from Wallet Pay
$client = new WalletPayClient(apiKey: 'YOUR_STORE_API_KEY');

// Create a payment order
$order = $client->createOrder(new CreateOrderRequest(
    amount: new MoneyAmount('USD', '9.99'),
    description: 'Premium subscription for 1 month',
    externalId: 'ORDER-' . uniqid(),      // Your unique order ID
    timeoutSeconds: 3600,                  // 1 hour to pay
    customerTelegramUserId: 123456789,     // Who can pay this order
    autoConversionCurrency: 'USDT',        // Receive payment in USDT
    returnUrl: 'https://t.me/YourBot/YourApp',
    customData: json_encode(['user_id' => 42])
));

// Send this link to your user — they'll pay right in Telegram!
echo $order->directPayLink;

With Laravel

Using Laravel? It's even cleaner with the Facade:

use Tigusigalpa\TelegramWallet\Laravel\Facades\WalletPay;
use Tigusigalpa\TelegramWallet\DTO\CreateOrderRequest;
use Tigusigalpa\TelegramWallet\DTO\MoneyAmount;

$order = WalletPay::createOrder(new CreateOrderRequest(
    amount: new MoneyAmount('USD', '9.99'),
    description: 'Premium subscription',
    externalId: 'SUB-' . $user->id . '-' . time(),
    timeoutSeconds: 3600,
    customerTelegramUserId: $user->telegram_id,
    autoConversionCurrency: 'USDT',
    returnUrl: 'https://t.me/YourBot/YourApp',
    customData: json_encode(['user_id' => $user->id])
));

return response()->json(['pay_url' => $order->directPayLink]);

Handling Webhooks

When a payment succeeds (or fails), Wallet Pay will notify your server:

use Tigusigalpa\TelegramWallet\Webhook\WebhookVerifier;
use Tigusigalpa\TelegramWallet\Enums\WebhookEventType;

$verifier = new WebhookVerifier('YOUR_STORE_API_KEY');

try {
    // Verify signature and parse events in one call
    $events = $verifier->verifyAndParse(
        $_SERVER['REQUEST_METHOD'],
        parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
        $_SERVER['HTTP_WALLETPAY_TIMESTAMP'],
        file_get_contents('php://input'),
        $_SERVER['HTTP_WALLETPAY_SIGNATURE']
    );
    
    foreach ($events as $event) {
        if ($event->type === WebhookEventType::ORDER_PAID) {
            // 🎉 Payment successful!
            $customData = json_decode($event->payload->customData, true);
            
            // Now you can:
            // - Update your database
            // - Grant premium access
            // - Send a thank-you message
        }
    }
    
    http_response_code(200);
    echo 'OK';
} catch (InvalidWebhookSignatureException $e) {
    http_response_code(401);
    echo 'Invalid signature';
}

Configuration

The defaults work great for most cases. Here's how to customize if needed:

use Tigusigalpa\TelegramWallet\WalletPayClient;
use GuzzleHttp\Client;

// Simple — just your API key
$client = new WalletPayClient(apiKey: getenv('WALLETPAY_API_KEY'));

// Custom — bring your own HTTP client
$client = new WalletPayClient(
    apiKey: getenv('WALLETPAY_API_KEY'),
    timeout: 60,
    httpClient: new Client(['verify' => true])
);

Laravel Configuration

Publish the configuration file:

php artisan vendor:publish --tag=walletpay-config

Edit config/walletpay.php:

<?php

return [
    'api_key' => env('WALLETPAY_API_KEY'),
    'base_url' => env('WALLETPAY_BASE_URL', 'https://pay.wallet.tg'),
    'timeout' => env('WALLETPAY_TIMEOUT', 30),
    'webhook_path' => env('WALLETPAY_WEBHOOK_PATH', '/webhook/walletpay'),
];

Add to your .env:

WALLETPAY_API_KEY=your_store_api_key_here
WALLETPAY_WEBHOOK_PATH=/webhook/walletpay

API Reference

Here's everything you can do with the client:

Method What it does
createOrder($request) Create a new payment order
getOrderPreview($orderId) Check order status
getOrderList($offset, $count) List orders (paginated, max 10,000)
getOrderAmount() Get total order count

Order Parameters

Parameter Required What it's for
amount Yes How much to charge (e.g., new MoneyAmount('USD', '9.99'))
description Yes What the user sees (5-100 chars)
externalId Yes Your order ID — use this to match payments
timeoutSeconds Yes How long the order stays valid (30s to 10 days)
customerTelegramUserId Yes Only this Telegram user can pay
autoConversionCurrency No Convert payment to TON/USDT/BTC/NOT (+1% fee)
returnUrl No Where to send user after payment
failReturnUrl No Where to send user if payment fails
customData No Your metadata — comes back in webhooks

Currencies

For pricing: USD, EUR
For receiving: TON, USDT, BTC, NOT

Order Lifecycle

Status Meaning
ACTIVE Waiting for payment
PAID Payment received! 🎉
EXPIRED Time ran out
CANCELLED User or system cancelled

Webhook Events

  • ORDER_PAID — Money's in! Time to deliver.
  • ORDER_FAILED — Something went wrong (expired, cancelled, etc.)

Setting Up Webhooks

Webhooks tell you when payments happen. Here's how to set them up properly.

Step 1: Configure Your URL

In your Wallet Pay store settings, set the webhook URL:

https://yourdomain.com/webhook/walletpay

Important:

  • Must be HTTPS with a real SSL certificate (Let's Encrypt works great)
  • Self-signed certs won't work
  • Always return HTTP 200 to confirm receipt

Step 2: Allowlist Wallet Pay IPs

If you have a firewall, allow these IPs:

  • 188.42.38.156
  • 172.255.249.124

Step 3: Handle Duplicate Webhooks

Wallet Pay might send the same webhook multiple times (network issues happen). Use eventId to avoid processing duplicates:

<?php

// Example: Store processed event IDs in database
if (ProcessedWebhookEvent::where('event_id', $event->eventId)->exists()) {
    return; // Already processed
}

// Process the event
processPayment($event);

// Mark as processed
ProcessedWebhookEvent::create(['event_id' => $event->eventId]);

How Signature Verification Works

You don't need to implement this yourself (our WebhookVerifier handles it), but here's what happens under the hood:

stringToSign = HTTP_METHOD + "." + URI_PATH + "." + TIMESTAMP + "." + Base64(BODY)
signature    = Base64(HmacSHA256(stringToSign, API_KEY))

Heads up: The URI path must match exactly what you configured — including the trailing slash (or lack thereof).

Laravel Deep Dive

Service Provider

The package auto-registers — no setup needed! But if you need manual registration:

// config/app.php
'providers' => [
    Tigusigalpa\TelegramWallet\Laravel\WalletPayServiceProvider::class,
],

Facade

// config/app.php
'aliases' => [
    'WalletPay' => Tigusigalpa\TelegramWallet\Laravel\Facades\WalletPay::class,
],

Webhook Middleware

We include middleware that verifies webhook signatures for you. Register it in app/Http/Kernel.php:

protected $middlewareAliases = [
    'walletpay.webhook' => \Tigusigalpa\TelegramWallet\Laravel\Http\Middleware\VerifyWalletPayWebhook::class,
];

Simple Route Example

// routes/api.php
use Tigusigalpa\TelegramWallet\Webhook\WebhookVerifier;

Route::post('/webhook/walletpay', function (Request $request, WebhookVerifier $verifier) {
    $events = $verifier->parseWebhookEvents($request->getContent());
    
    foreach ($events as $event) {
        Log::info('Payment webhook', [
            'event_id' => $event->eventId,
            'type' => $event->type->value,
        ]);
    }
    
    return response('OK', 200);
})->middleware('walletpay.webhook');

Full Controller Example

Here's a production-ready controller with payment creation and webhook handling:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Tigusigalpa\TelegramWallet\Laravel\Facades\WalletPay;
use Tigusigalpa\TelegramWallet\DTO\CreateOrderRequest;
use Tigusigalpa\TelegramWallet\DTO\MoneyAmount;
use Tigusigalpa\TelegramWallet\Enums\WebhookEventType;
use Tigusigalpa\TelegramWallet\Webhook\WebhookVerifier;

class PaymentController extends Controller
{
    public function createPayment(Request $request)
    {
        $user = $request->user();
        
        $order = WalletPay::createOrder(new CreateOrderRequest(
            amount: new MoneyAmount('USD', '9.99'),
            description: 'Premium subscription',
            externalId: 'SUB-' . $user->id . '-' . time(),
            timeoutSeconds: 3600,
            customerTelegramUserId: $user->telegram_id,
            autoConversionCurrency: 'USDT',
            returnUrl: 'https://t.me/YourBot/YourApp',
            customData: json_encode(['user_id' => $user->id])
        ));
        
        // Save to your database
        Payment::create([
            'user_id' => $user->id,
            'wallet_pay_order_id' => $order->id,
            'amount' => $order->amount->amount,
            'status' => 'pending',
        ]);
        
        return response()->json(['pay_url' => $order->directPayLink]);
    }
    
    public function webhook(Request $request, WebhookVerifier $verifier)
    {
        $events = $verifier->parseWebhookEvents($request->getContent());
        
        foreach ($events as $event) {
            if ($event->type === WebhookEventType::ORDER_PAID) {
                $customData = json_decode($event->payload->customData, true);
                
                // Update payment status
                Payment::where('wallet_pay_order_id', $event->payload->id)
                    ->update(['status' => 'paid']);
                
                // Grant premium access
                User::find($customData['user_id'])
                    ->update(['is_premium' => true]);
            }
        }
        
        return response('OK', 200);
    }
}

When Things Go Wrong

Errors are typed, so you can handle them gracefully:

Exception What happened
InvalidRequestException Bad request (check your parameters)
InvalidApiKeyException Invalid API key
OrderNotFoundException Order doesn't exist
RateLimitException Slow down! Too many requests
ServerException Wallet Pay is having issues
InvalidWebhookSignatureException Webhook signature doesn't match

Example

use Tigusigalpa\TelegramWallet\Exceptions\OrderNotFoundException;
use Tigusigalpa\TelegramWallet\Exceptions\RateLimitException;
use Tigusigalpa\TelegramWallet\Exceptions\WalletPayException;

try {
    $order = $client->getOrderPreview('123456');
} catch (OrderNotFoundException $e) {
    // Order doesn't exist
    return response()->json(['error' => 'Order not found'], 404);
} catch (RateLimitException $e) {
    // Too many requests — back off and retry
    return response()->json(['error' => 'Try again later'], 429);
} catch (WalletPayException $e) {
    // Something else went wrong
    Log::error('Wallet Pay error', ['message' => $e->getMessage()]);
    return response()->json(['error' => 'Payment error'], 500);
}

Things to Know

Opening the Payment Link

The payment link (directPayLink) needs to be opened correctly:

In a Telegram Web App: Use Telegram.WebApp.openTelegramLink(url)
In a bot message: Use it as an Inline Button URL
Don't use: openLink() or MenuButtonWebApp — payment will fail

Payment Button Text

Telegram requires specific button text:

  • 👛 Wallet Pay
  • 👛 Pay via Wallet

Yes, the purse emoji is mandatory. 👛

Preventing Duplicate Orders

Use externalId as your idempotency key. If you retry with the same ID, you'll get the existing order back:

$externalId = 'ORDER-' . $userId . '-' . $productId . '-' . time();

Auto-Conversion Fees

Want to receive payments in a specific crypto? Set autoConversionCurrency, but note:

  • 1% fee applies
  • Minimum: $1.30 (or $3 for BTC)

When Can You Withdraw?

Funds are held for 48 hours after payment before you can withdraw them. This is a Wallet Pay policy.

One User Per Order

Only the Telegram user specified in customerTelegramUserId can pay that order. This prevents payment link sharing.

Testing

composer install
vendor/bin/phpunit

# Just unit tests
vendor/bin/phpunit --testsuite Unit

# Just feature tests
vendor/bin/phpunit --testsuite Feature

What's Next?

This package is built to grow. The architecture separates payment functionality from future trading features (spot trading, tokenized stocks, perpetual futures). When Wallet adds new APIs, we'll add support without breaking your existing code.

Contributing

Found a bug? Have an idea? PRs are welcome!

  1. Fork it
  2. Create your branch (git checkout -b fix/something)
  3. Make your changes
  4. Run tests (vendor/bin/phpunit)
  5. Open a PR

Please follow PSR-12 and include tests for new features.

License

MIT — do whatever you want with it.

Links

Need Help?

Open an issue on GitHub — I'll do my best to help.

Built by Igor Sazonov