DEV Community

Arslan Rejebov
Arslan Rejebov

Posted on • Edited on

How to Integrate Telegram Payments in a Django and React Mini App

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:

  1. A Django backend set up for managing API requests and business logic.
  2. A React frontend that will serve as the user interface within the Telegram Mini App.
  3. 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)
Enter fullscreen mode Exit fullscreen mode

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.")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
    });
};
Enter fullscreen mode Exit fullscreen mode

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"})
Enter fullscreen mode Exit fullscreen mode

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"})

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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!

Sources:

Top comments (2)

Collapse
 
0e5c07416a0e profile image
Ishak Okutan • Edited

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

    if "pre_checkout_query" in update:
        pre_checkout_query(update["pre_checkout_query"])
    if "message" in update:
        message = update["message"]
        if "successful_payment" in message:
            successful_payment(message["successful_payment"])

    return Response({"status": "success"})`
Enter fullscreen mode Exit fullscreen mode

@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")

// give products to the user paid for.

purchase.status = PurchaseStatus.COMPLETED
purchase.purchased_completed = timezone.now()
purchase.save()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
leen2233 profile image
Arslan Rejebov

Oh, sorry about that. You are right. I will update :)