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.
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
Install via Composer:
composer require tigusigalpa/telegram-wallet-php- PHP 8.1 or higher
ext-jsonextension- Laravel 9.x, 10.x, 11.x, 12.x, or 13.x (optional, for Laravel integration)
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;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]);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';
}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])
);Publish the configuration file:
php artisan vendor:publish --tag=walletpay-configEdit 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/walletpayHere'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 |
| 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 |
For pricing: USD, EUR
For receiving: TON, USDT, BTC, NOT
| Status | Meaning |
|---|---|
ACTIVE |
Waiting for payment |
PAID |
Payment received! 🎉 |
EXPIRED |
Time ran out |
CANCELLED |
User or system cancelled |
ORDER_PAID— Money's in! Time to deliver.ORDER_FAILED— Something went wrong (expired, cancelled, etc.)
Webhooks tell you when payments happen. Here's how to set them up properly.
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
If you have a firewall, allow these IPs:
188.42.38.156172.255.249.124
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]);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).
The package auto-registers — no setup needed! But if you need manual registration:
// config/app.php
'providers' => [
Tigusigalpa\TelegramWallet\Laravel\WalletPayServiceProvider::class,
],// config/app.php
'aliases' => [
'WalletPay' => Tigusigalpa\TelegramWallet\Laravel\Facades\WalletPay::class,
],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,
];// 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');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);
}
}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 |
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);
}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
Telegram requires specific button text:
👛 Wallet Pay👛 Pay via Wallet
Yes, the purse emoji is mandatory. 👛
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();Want to receive payments in a specific crypto? Set autoConversionCurrency, but note:
- 1% fee applies
- Minimum: $1.30 (or $3 for BTC)
Funds are held for 48 hours after payment before you can withdraw them. This is a Wallet Pay policy.
Only the Telegram user specified in customerTelegramUserId can pay that order. This prevents payment link sharing.
composer install
vendor/bin/phpunit
# Just unit tests
vendor/bin/phpunit --testsuite Unit
# Just feature tests
vendor/bin/phpunit --testsuite FeatureThis 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.
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.
MIT — do whatever you want with it.
Open an issue on GitHub — I'll do my best to help.
Built by Igor Sazonov
