Introduction
Integrating payments directly within a Telegram Mini App can transform user experience by making in-app transactions straightforward and secure. Imagine users who can pay for products or services without ever leaving the app—they simply click, pay, and continue using your app seamlessly. With Telegram’s Stars payment system, this integration has become even easier for developers and more convenient for users.
In this guide, we’ll dive deep into the steps of adding Telegram payment functionality to a Mini App built with Django and React, covering everything from setting up your models to handling webhook updates from Telegram. Let’s jump in and create an app that offers a streamlined and modern payment experience!
Prerequisites for Payment Integration
Before diving into the code, make sure you have:
- A Django backend set up for managing API requests and business logic.
- A React frontend that will serve as the user interface within the Telegram Mini App.
- A Telegram bot, created via BotFather, with payment functionality enabled.
Once you have these components in place, you’ll be ready to start building the payment system!
Setting Up Models in Django
To manage transactions, we need a Payment
model in Django. This model will track each user’s payments with fields for the user’s ID, payment amount, a unique order ID, and the payment status.
from django.db import models
class Payment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
order_id = models.CharField(max_length=100, unique=True)
status = models.CharField(max_length=10, default="pending")
created_at = models.DateTimeField(auto_now_add=True)
The Payment
model serves as a record for each transaction, allowing us to track when a user initiates payment and whether it’s completed successfully.
Creating the Payment Invoice View in Django
The create_invoice
function is a key part of this integration. This function will generate an invoice and send a request to Telegram’s API, which will then return a link that the user can click to complete their payment.
def create_invoice(user, amount) -> dict:
order_id = user.tg_id # Unique order ID
# Save payment in the database
payment = Payment.objects.create(
user=user,
amount=amount,
order_id=order_id
)
# Set payload with payment details
payload = f"{order_id}&&&{payment.id}"
data = {
"title": "Purchase",
"description": "Exclusive Access",
"payload": payload,
"currency": "XTR",
"prices": [{"label": "Telegram Stars", "amount": int(amount * 100)}]
}
headers = {'Content-Type': 'application/json'}
url = f"https://api.telegram.org/bot{settings.BOT_TOKEN}/createInvoiceLink"
response = requests.post(url, json=data, headers=headers)
if response.ok and response.json().get('ok'):
return {"url": response.json()['result']}
raise Exception("Invoice creation failed.")
Here’s what each piece does:
- Order ID: Unique identifier for the order based on the user’s Telegram ID.
- Payload: Information about the order, including the ID and amount.
- API Request: Sends a request to Telegram’s API, generating a unique payment link.
If everything goes well, we get an invoice URL that we’ll later send to the frontend.
Configuring API Request Headers and Endpoint for Telegram
To interact with Telegram’s API, set the necessary headers and construct the appropriate endpoint URL. This ensures that Telegram recognizes our request, and it returns the required payment link.
Returning Invoice URL from Django to React Frontend
Now, let’s create a view in Django that will interact with React when the user initiates a payment. This view generates the invoice link and sends it back to the frontend.
@api_view(["POST"])
def buy_subscription(request):
user = request.user
amount = request.data.get("amount")
if not amount:
return Response({"detail": "Invalid amount."}, status=400)
try:
url = create_invoice(user, amount)
return Response(url)
except Exception as e:
return Response({"error": str(e)}, status=500)
This view does a few things:
- Retrieves the payment amount from the request.
- Calls
create_invoice
to generate the payment link. - Sends the link back to React, which the user will use to complete the transaction.
Setting Up Payment Functionality in React
With the backend in place, let’s move to the frontend. Using axios
, we’ll set up a function in React to send a payment initiation request to Django and retrieve the invoice URL.
Creating the payByTelegramStar
Function in React
This function will make a request to our Django API endpoint to create an invoice. It will also handle what happens when the user clicks to pay.
const payByTelegramStar = () => {
const telegram = window.Telegram.WebApp;
const token = localStorage.getItem("token");
setIsLoading(true);
axios.post(
"/pay/by/telegram-star",
{ amount: 100 },
{ headers: { Authorization: `Token ${token}` } }
)
.then(response => {
telegram.openInvoice(response.data.url, (status) => {
if (status === "cancelled" || status === "failed") {
telegram.showAlert("Payment was cancelled or failed.");
} else {
pollSubscriptionStatus(token);
}
setIsLoading(false);
});
})
.catch(() => {
telegram.showAlert("Failed to initiate payment.");
setIsLoading(false);
});
};
In this function:
- We initialize Telegram’s WebApp API.
- We send a request to our Django endpoint, passing in the payment amount and the user’s token.
- When the request is successful, we use
telegram.openInvoice
to open the payment link, and a callback checks whether the payment was successful or canceled.
Opening the Invoice in the Telegram Mini App
The telegram.openInvoice
method will open the invoice link within the Mini App, allowing the user to complete payment directly. We handle different responses with a callback, including success, failure, and cancellation, giving the user real-time feedback on their payment attempt.
Webhook Configuration in Django for Telegram Updates
After payment, Telegram sends updates to a designated webhook. This webhook, configured in Django, will listen for updates on the status of each transaction. We’ll use this webhook to confirm payments and update the database.
Handling pre_checkout_query
to Confirm Payment
When a payment is initiated, Telegram sends a pre_checkout_query
. This needs to be confirmed with Telegram to let it know that our system is ready to handle the transaction.
@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def telegram_webhook(request):
update = request.data
if 'pre_checkout_query' in update:
id = update['pre_checkout_query']['id']
url = f"https://api.telegram.org/bot{settings.BOT_TOKEN}/answerPreCheckoutQuery"
requests.post(url, data={"pre_checkout_query_id": id, "ok": True})
return Response({"status": "success"})
In this function:
- We check for the
pre_checkout_query
key in the webhook update. - If it exists, we extract the query ID and confirm the transaction with Telegram.
Processing successful_payment
Webhook Update
When a payment is successfully completed, Telegram sends a successful_payment
update to our webhook. We can then mark the corresponding Payment
record as “paid” in our database. Let's edit our function
@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def telegram_webhook(request):
update = request.data
if 'pre_checkout_query' in update:
id = update['pre_checkout_query']['id']
url = f"https://api.telegram.org/bot{settings.BOT_TOKEN}/answerPreCheckoutQuery"
requests.post(url, data={"pre_checkout_query_id": id, "ok": True})
return Response({"status": "success"})
# Successfull payment
elif 'successful_payment' in update.get("message", {}):
order_id = update.get("message", {}).get("successful_payment", {}).get("invoice_payload") # Get invoice payload
# Update your payment record based on order ID
try:
user_tg_id = order_id.split("&&&")[0]
payment_id = int(order_id.split("&&&")[1])
payment = Payment.objects.get(order_id=user_tg_id, id=payment_id)
if payment.status == "paid":
print(f'[payment already marked as paid]')
return Response({"status": "fail"})
else:
payment.status = "paid" # mark as paid
payment.save()
### Other operations here when payment is successful
except Exception as e:
print("[error, data not valid]", str(e))
return Response({"status": "fail"})
Setting the Webhook in Telegram
To receive payment updates, we must register the webhook URL with Telegram using the following format:
https://api.telegram.org/bot{YOUR_TELEGRAM_BOT_TOKEN}/setWebhook?url=https://example.com/webhooks/telegram
This URL registers our webhook, and Telegram will start sending transaction updates to our app.
Polling the Subscription Status in React
To confirm payment on the frontend, we can create a polling function in React that periodically checks the payment status by sending a request to our backend.
const pollSubscriptionStatus = (token) => {
const intervalId = setInterval(() => {
axios.get("/payment/check", { headers: { Authorization: `Token ${token}` } })
.then(response => {
if (response.data.success) {
clearInterval(intervalId);
}
})
.catch(error => console.error(error));
}, 3000);
};
This polling function will check the backend every 3 seconds to see if the payment has been marked as successful, providing a smooth user experience by confirming the transaction without the user having to refresh.
Conclusion
Integrating Telegram payments within a Django and React Mini App opens the door for a smooth, convenient transaction process that keeps users engaged within the app. From creating an invoice to handling real-time payment updates with webhooks and polling, each step contributes to a streamlined and secure payment flow.
By following this guide, you’ll have all the tools you need to build a Telegram payment integration that works seamlessly for users. Give it a try and see how much easier in-app payments can be!
Top comments (2)
Thanks, it was very helpful.
But you forgot the catching and processing successful payment.
I do it like:
data = {
"title": "Purchase",
"description": desc,
"payload": str(purchase.id), # we'll check success with this value. this could be dict also.
"currency": "XTR",
"prices": [{"label": "Telegram Stars", "amount": star_amount}],
}
web hook. I set my webhook like /api/telegram-webhook/a-long-secret/
`def post(self, request, *args, **kwargs):
update = request.data
@transaction.atomic()
def successful_payment(update):
invoice_payload = update["invoice_payload"]
try:
purchase = Purchase.objects.get(id=invoice_payload)
except Purchase.DoesNotExist:
logger.error(
f"Successfull Payment | Purchase not found for id={invoice_payload}"
)
raise ValidationError("Purchase not found")
Oh, sorry about that. You are right. I will update :)