@starspay/sdk
v0.3.0
Published
Telegram Stars + Telegram Payments SDK - subscriptions, paywalls, and analytics for Telegram Mini Apps
Downloads
678
Maintainers
Readme
@starspay/sdk
Telegram payments for Mini Apps and bots — Telegram Stars, Telegram Payments (cards), and TronDealer (USDT/USDC) — with subscriptions, paywalls, and webhooks in a few lines of code.
@starspay/sdk is the official client + server SDK for StarsPay — a managed backend that abstracts Telegram payment providers behind one typed, framework-friendly subscription platform. Think of it as RevenueCat for Telegram Mini Apps.
- Four payment providers — Telegram Stars (XTR), Telegram Payments API (Stripe-via-TG, fiat cards via the gateway you configure in
@BotFather), TronDealer (USDT/USDC on BSC/Polygon/Ethereum via EVM, or TRC-20 USDT on TRON — deposit-address model), and TON (keyless TON Connect in a Telegram Mini App — native Gram, non-custodial; USDT-on-TON coming soon). Pick per product/price. - Subscription state machine — six states (
pending,active,canceled,past_due,expired,revoked) with enforced transitions and a 3-day grace period. Fiat-renewal scheduler available for non-Stars subscriptions. - Auto-handled
pre_checkout_query— middleware answers Telegram inside the 10-second deadline so you never miss a payment - Browser-safe client —
sp_pub_*publishable-key enforcement; server keys throw on construction - React components —
<PaywallGate>,<SubscriptionButton>,<PurchaseButton>,<ProductCheckout>,<PaymentMethodSelector>,<TronDealerPayment>,useSubscription() - Product deep links — generate
t.me/yourbot?start=buy_<priceId>URLs that open straight into checkout - Init-data validation — constant-time HMAC verification for Mini App
initData - Refund + cancel — wraps
editUserStarSubscriptionandrefundStarPayment(Stars); Telegram Payments refunds are handled by the upstream PSP - Test mode — bypass entitlement checks in development without touching production data
- Zero runtime deps for shared/server code; React is an optional peer
Get an API key at app.starspay.dev. Full guides at docs.starspay.dev.
Install
npm install @starspay/sdkRequires Node.js 18+ for the server entry. React >=18 is an optional peer dependency for the React entry.
Entry points
The package ships four entry points; import only what you need.
| Import | Use it in | Ships |
| ------------------------------- | ---------------------------------- | ----------------------------------------------- |
| @starspay/sdk | Anywhere (shared) | Types, constants, product-link helpers |
| @starspay/sdk/server | Node.js bots / webhook handlers | createStarsPay, validateInitData, API clients, StarsProvider + TelegramPaymentsProvider |
| @starspay/sdk/client | Browsers / Mini Apps (vanilla JS) | StarsPayClient, isEntitled |
| @starspay/sdk/react | React Mini Apps | Provider, hooks, gate + button components |
Both ESM and CommonJS builds are emitted with full .d.ts types.
Server quickstart
Express
import express from 'express';
import { createStarsPay } from '@starspay/sdk/server';
const starspay = createStarsPay({
apiKey: process.env.STARSPAY_API_KEY!, // sp_live_... or sp_test_...
botToken: process.env.BOT_TOKEN!,
webhookSecret: process.env.WEBHOOK_SECRET!, // required in production
onEvent: async (event, data) => {
// event: 'payment.one_time' | 'subscription.created' | 'subscription.renewed' | 'payment.refunded'
console.log(event, data.telegramUserId, data.amount);
},
});
const app = express();
app.post(
'/webhook',
express.json(),
starspay.middleware(), // intercepts pre_checkout + successful_payment + refund
(req, res) => {
// your bot logic for non-payment updates
res.sendStatus(200);
},
);The middleware returns 200 after a successful payment is recorded and 403 when the X-Telegram-Bot-Api-Secret-Token header doesn't match. Configure that header by passing secret_token to Telegram's setWebhook call.
Cloudflare Workers / Fastify / anywhere else
If you don't have an Express-style middleware chain, drive the SDK directly:
const starspay = createStarsPay({ apiKey, botToken, webhookSecret });
export default {
async fetch(req: Request) {
const secretHeader = req.headers.get('x-telegram-bot-api-secret-token') ?? undefined;
const update = await req.json();
await starspay.handleUpdate(update, secretHeader); // throws on invalid secret
return new Response('OK');
},
};Create a subscription invoice
const invoiceUrl = await starspay.createInvoice({
title: 'Premium', // ≤32 bytes UTF-8
description: 'Unlock all features', // ≤255 bytes UTF-8
payload: `sub:premium:${userId}:${Date.now()}`,
amount: 100, // Stars (1–10,000)
subscription: true, // 30-day recurring
});
await bot.sendMessage(chatId, `Subscribe: ${invoiceUrl}`);Check entitlement, cancel, refund
const isActive = await starspay.isActive(telegramUserId);
await starspay.cancelSubscription(telegramUserId, telegramPaymentChargeId); // disable auto-renew
await starspay.refund(telegramUserId, telegramPaymentChargeId); // full Stars refund
await starspay.refundPayment({ provider: 'stars', chargeId, telegramUserId }); // provider-aware refundValidate Mini App initData
import { validateInitData } from '@starspay/sdk/server';
const data = validateInitData(req.body.initData, process.env.BOT_TOKEN!);
const userId = data.user?.id; // verifiedHandle product link /start payloads
// Inside your bot's /start handler
const handled = await starspay.handleBotStart(chatId, ctx.startPayload);
if (!handled) {
// not a StarsPay link — fall back to your normal welcome flow
}Payment providers
StarsPay supports the following buyer-side providers. The provider is chosen per-price in your dashboard (provider column on the prices table); the SDK + backend dispatch automatically.
| Provider | Currency | Invoice mechanism | Webhook receiver | Refunds |
|----------|----------|-------------------|-----------------|---------|
| stars | XTR (Telegram Stars) | createInvoiceLink (currency=XTR) | bot-webhook | Yes — refundStarPayment |
| telegram_payments | EUR / USD / ... (fiat cards) | createInvoiceLink + provider_token from BotFather | bot-webhook | No (handled by upstream PSP) |
| trondealer | USDT / USDC (EVM: BSC, Polygon, Ethereum) · USDT TRC-20 (TRON) | Deposit address (no URL) — buyer sends funds, settlement via webhook | crypto-webhook?provider=trondealer | No (on-chain) |
| ton | TON (native Gram; USDT-on-TON coming soon) | TON Connect in a Telegram Mini App; buyer connects a TON wallet, the Mini App builds + signs the transfer client-side | ton-watcher cron (on-chain) | No (on-chain) |
Configure providers on the server
When using createStarsPay, pass the keys your merchants saved in the dashboard. Only the providers you configure are dispatchable.
import { createStarsPay } from '@starspay/sdk/server';
const starspay = createStarsPay({
apiKey: process.env.STARSPAY_API_KEY!,
botToken: process.env.BOT_TOKEN!,
webhookSecret: process.env.WEBHOOK_SECRET!,
payments: {
providers: {
// The key is the merchant-defined identifier; token comes from @BotFather.
// Optional `testToken` is used when testMode is enabled.
stripe: { token: process.env.TG_PAYMENTS_TOKEN!, testToken: process.env.TG_PAYMENTS_TEST_TOKEN },
},
},
});Create a Telegram Payments invoice
const invoice = await starspay.createProviderInvoice({
provider: 'telegram_payments',
title: 'Premium',
description: 'Premium plan',
amount: 999, // smallest unit (cents)
currency: 'USD',
payload: `sub:premium:${userId}:${Date.now()}`,
needEmail: true, // optional — collect email at checkout
});
console.log(invoice.payUrl);
console.log(invoice.providerInvoiceId);TronDealer (USDT / USDC)
TronDealer is a deposit-address provider: there's no hosted checkout. The buyer sends stablecoins to a per-order address and settlement arrives via a webhook. TronDealer is one-time only. The merchant picks a concrete network per price ("bsc", "polygon", "ethereum", or "tron") and the buyer pays on exactly that chain — no on-checkout picker. ("evm" is a legacy value meaning "any V2 EVM chain".)
- EVM (
"bsc"/"polygon"/"ethereum") — USDT or USDC. Deposit address is0x…. The checkout Mini App shows an Open in wallet button (an EIP-681 deep link that opens the buyer's wallet prefilled with the transfer), a QR code, and a copyable address — no wallet-connect step. (Polygon uses native USDC — bridged USDC.e is not credited.) - TRON (
"tron") — TRC-20 USDT only. Deposit address isT…(base58, case-sensitive). The Mini App shows the address + QR; the buyer sends from any wallet or exchange. Buyer's wallet handles the TRON energy/TRX fee.
Configure it per-merchant in the dashboard (Settings → Payment Providers → TronDealer) with an api_key and a webhook_secret, both stored encrypted. Once enabled, creating a TronDealer invoice (POST {webhook-ingest}/v1/invoices/create with priceId + telegramUserId) returns a deposit object instead of a URL:
// EVM price (network: "evm")
{
"url": null,
"provider": "trondealer",
"provider_invoice_id": "0xabc…def",
"deposit": { "address": "0xabc…def", "amount": "10.00", "asset": "USDT", "chains": ["bsc", "polygon"] }
}
// TRON price (network: "tron")
{
"url": null,
"provider": "trondealer",
"provider_invoice_id": "tabc…xyz",
"deposit": { "address": "T…base58address", "amount": "10.00", "asset": "USDT", "chains": ["tron"] }
}In a React Mini App, render the deposit screen with <TronDealerPayment> — it assigns the address, shows a chain toggle + EIP-681 deeplink + QR, and polls getDepositStatus() until the payment confirms:
import { TronDealerPayment } from '@starspay/sdk/react';
<TronDealerPayment priceId="price_pro_usdt" onSuccess={() => unlock()} />For buyers arriving via a product link, StarsPay opens its hosted payment Mini App with an opaque checkout token and polls GET {webhook-ingest}/v1/checkout?token=<checkout_token> for the deposit + status.
Settlement: TronDealer posts a transaction.confirmed webhook to {crypto-webhook}?app=<appId>&provider=trondealer, signed with HMAC-SHA256 over the raw body in the X-Signature-256 header (sha256=<hex>) using your webhook_secret. StarsPay verifies the signature, matches the deposit address, reconciles asset + amount, records the payment, flips the invoice to paid, and DMs the buyer. Invoice statuses: pending | paid | expired | canceled.
Test mode (canary only): Toggle Settings → TronDealer → Test mode in the dashboard to validate the full buyer flow without real funds. Invoice creation skips the live /wallets/assign call and returns a simulated deposit address; the hosted Mini App shows a "Simulate payment" button that synthesises a real transaction.confirmed event and runs the genuine settlement path. Test mode is blocked in production by the STARSPAY_ALLOW_TEST_PAYMENTS server flag and requires both the invoice and app config to carry test_mode: true. See the TronDealer docs for details.
TON (TON Connect)
The ton provider settles non-custodially in a Telegram Mini App: the buyer connects a TON wallet via TON Connect, the Mini App builds the transfer client-side (a comment carrying the order id), and the wallet signs it. Funds arrive directly in the merchant's own TON wallet — StarsPay never holds them, and there is no API key or settlement webhook. TON prices are one-time only and denominated in TON (native Gram); USDT-on-TON (jetton) is a planned follow-up.
Configure it per-merchant in the dashboard (Settings → Payment Providers → TON) with a single TON wallet address (wallet_address, stored encrypted) — that's the only field. Create a one-time price denominated in TON and assign it the ton provider. Like TronDealer, TON is dispatched by the StarsPay backend — POST {webhook-ingest}/v1/invoices/create with priceId + telegramUserId returns a checkout_token, and the bot opens the TON Mini App with it.
// Buyer flow (bot → TON Mini App → TON Connect → sign → on-chain watcher confirms):
// 1. POST {webhook-ingest}/v1/invoices/create { priceId, telegramUserId }
// → { url: null, provider: 'ton', checkout_token: '…' } (pending row)
// 2. Mini App: GET {webhook-ingest}/v1/checkout?token=<checkout_token>
// → { …, ton: { to, amount_nano, comment } }
// Buyer connects a TON wallet (TON Connect); the Mini App builds the comment
// cell client-side and calls tonConnectUI.sendTransaction({ messages: [{ to, amount, payload }] }).
// 3. Mini App polls GET {webhook-ingest}/v1/checkout until status === 'paid'.Confirmation is on-chain: the ton-watcher cron polls the public toncenter indexer, matches the on-chain comment to the pending invoice, then records the payment, flips the invoice to paid, and DMs the buyer. There is no webhook to register. An optional ops-level TONCENTER_API_KEY only raises toncenter rate limits — keyless operation works fine. Invoice statuses: pending | paid | expired | canceled. See the TON Payments docs.
Provider classes
If you need lower-level control (custom invoice flows, refund logic), the provider classes are exported directly:
import {
StarsProvider,
TelegramPaymentsProvider,
TelegramApiClient,
} from '@starspay/sdk/server';
const telegram = new TelegramApiClient(process.env.BOT_TOKEN!);
const stars = new StarsProvider(telegram);
const invoice = await stars.createInvoice({
title: 'Premium',
description: 'Monthly premium',
payload: 'sub:premium:123',
amount: 100,
subscription: true,
});Browser quickstart
import { StarsPayClient } from '@starspay/sdk/client';
const client = new StarsPayClient({
apiKey: import.meta.env.VITE_STARSPAY_PUB_KEY, // must start with sp_pub_
});
const isActive = await client.isActive(telegramUserId);
const url = await client.createInvoiceLink({
priceId: 'price_monthly_premium',
telegramUserId,
});
const status = await client.openPayment(url); // 'paid' | 'cancelled' | 'failed' | 'pending'The client enforces publishable keys (sp_pub_*) and throws on sp_live_* / sp_test_* / sk_* to prevent accidentally shipping a server key to the browser. Subscription queries are memoized for 30 seconds (configurable via cacheTtl).
For deposit-style providers (e.g. TronDealer), call client.createDepositInvoice({ priceId, telegramUserId }) — it returns a DepositInvoice (bare deposit address + amount + chains) instead of a hosted-checkout URL; poll client.getDepositStatus() until it settles.
Plan-limit error handling
import { TxLimitExceededError } from '@starspay/sdk';
try {
await client.createInvoiceLink({ priceId, telegramUserId });
} catch (err) {
if (err instanceof TxLimitExceededError) {
// err.tier, err.txCount, err.txLimit — show an upgrade prompt
}
}React quickstart
import {
StarsPayProvider,
PaywallGate,
SubscriptionButton,
} from '@starspay/sdk/react';
function App() {
const userId = window.Telegram.WebApp.initDataUnsafe.user?.id;
return (
<StarsPayProvider apiKey={import.meta.env.VITE_STARSPAY_PUB_KEY} telegramUserId={userId}>
<PaywallGate
loading={<Spinner />}
fallback={
<SubscriptionButton
priceId="price_monthly_premium"
onSuccess={() => location.reload()}
>
Subscribe — 100 Stars/month
</SubscriptionButton>
}
errorFallback={<p>Couldn't verify subscription. Try again.</p>}
>
<PremiumApp />
</PaywallGate>
</StarsPayProvider>
);
}PaywallGate is fail-closed: errors render errorFallback (or fallback when omitted), never children.
Security note:
PaywallGateis a UX convenience, not a security boundary. Always re-check entitlement server-side withstarspay.isActive()or the middleware before serving premium data.
Hooks
import { useSubscription, useProductLink } from '@starspay/sdk/react';
function Status() {
const { isActive, subscription, isLoading, error, refresh } = useSubscription();
// ...
}Other components
<PurchaseButton priceId={...} />— one-time purchases<ProductCheckout priceId={...} />— full checkout card with price + product details<TronDealerPayment priceId={...} />— USDT/USDC deposit screen for thetrondealerprovider<PaymentMethodSelector availableProviders={['stars', 'telegram_payments', 'trondealer', 'ton']} value={...} onChange={...} />— selector grid for users who can pick a method
Subscription state machine
pending ──► active ──┬─► canceled ──► expired ──► (terminal)
├─► past_due ──► expired
└─► revoked ──► (terminal)States that grant access (ENTITLED_STATUSES): active, canceled, past_due. canceled users keep access until current_period_end; past_due users keep access during the grace window (default 3 days, configurable per app).
Transitions are validated via the VALID_TRANSITIONS map exported from @starspay/sdk. Invalid transitions throw and are never written to the backend.
Product deep links
Generate shareable Telegram links that drop the user straight into checkout — no Mini App code required.
import { generateProductLink } from '@starspay/sdk';
const link = generateProductLink({
botUsername: 'mybot',
priceId: 'price_monthly_premium',
type: 'bot_start', // or 'mini_app' with appShortName
});
// → https://t.me/mybot?start=buy_price_monthly_premiumPair with starspay.handleBotStart(chatId, startParam) on the server to send the invoice automatically.
API key types
| Prefix | Where it goes | Enforcement |
| ------------ | -------------------------- | -------------------------------------------------- |
| sp_live_* | Server (production) | Required for live payments |
| sp_test_* | Server (development) | Use against the staging API URL |
| sp_pub_* | Browser / Mini App | Browser client throws if any other prefix is used |
Get keys at app.starspay.dev → Settings.
Test mode
const starspay = createStarsPay({ apiKey, botToken, testMode: true });
await starspay.isActive(123); // → true (no API call)testMode requires NODE_ENV to be development or test, and refuses to run against api.starspay.dev. The browser client also accepts testMode: true and logs a warning when combined with a publishable key.
Webhook secret (production)
createStarsPay throws if webhookSecret is missing outside of test mode. To use it:
- Pass
webhookSecret: '...'when constructingcreateStarsPay. - Pass the same value as
secret_tokento Telegram'ssetWebhook.
The middleware verifies the X-Telegram-Bot-Api-Secret-Token header in constant time and returns 403 on mismatch. When you call handleUpdate directly, pass the header value as the second argument.
Constants
Exported from @starspay/sdk:
| Constant | Value | Meaning |
| --------------------------------- | ---------- | ---------------------------------------- |
| STARS_CURRENCY | 'XTR' | Telegram Stars currency code |
| SUBSCRIPTION_PERIOD_SECONDS | 2592000 | 30 days — only period Telegram supports |
| DEFAULT_GRACE_PERIOD_SECONDS | 259200 | 3 days |
| MIN_INVOICE_AMOUNT | 1 | Minimum invoice in Stars |
| MAX_SUBSCRIPTION_AMOUNT | 10000 | Maximum subscription per period in Stars |
| STARS_TO_USD_RATE | 0.013 | Approximate conversion rate |
Links
- Documentation: docs.starspay.dev
- Dashboard: app.starspay.dev
- Issues: github.com/alexiglesias31/starspaysdk/issues
- Changelog: CHANGELOG.md
License
MIT © Alejandro Iglesias
