tigusigalpa / telegram-wallet-php
Complete PHP SDK for Telegram Wallet Pay API v1.2.0 with first-class Laravel support
Package info
github.com/tigusigalpa/telegram-wallet-php
pkg:composer/tigusigalpa/telegram-wallet-php
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- laravel/framework: ^9.0|^10.0|^11.0
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
README
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.
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-jsonextension- 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.156172.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!
- Fork it
- Create your branch (
git checkout -b fix/something) - Make your changes
- Run tests (
vendor/bin/phpunit) - 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