PayTM is the largest mobile commerce platform and digital wallet company. It has large user base of around 300 million and average transactions of 5 million per day. All digital payments can be made through paytm like mobile recharges, electricity bills, credit cards etc. Almost everything under the earth is covered in paytm πŸ™‚ Paytm is fast, reliable and more accessible payment solution.

PayTM also provides payment gateway solution that can be integrated into web and mobile apps with UPI, Debit/ Credit Card, NetBanking and PayTM Wallet payment options.

android-paytm-gateway-tutorial-ecommerce-app

Today, in this article we are going to see how to integrate the PayTM gateway in a simple (bit complex actually πŸ˜› ) e-commerce app. As writing an e-commerce app needs a bit of coding and architecture, everything can’t be covered in a single article. So, I have prepared a sample project (both web and mobile) and provided the code on Github. I also tried making it simple as much as possible so that every beginner can understand it.

1. Mart9 e-Commerce App

The example app I have created is very minimal with very limited screens. It has database (Realm), network layer (Retrofit) and payment options.

  • Login, Register screens to login or create a new user.
  • Home – To list down the available products along with name, thumbnail and price.
  • Cart – A BottomSheet view to maintain the cart items.
  • Payment screen – To make the necessary calls to backend server before redirecting user to payment gateway.
  • Transactions – To show the list of transactions made by a user.

Here are the screenshots from the app.

2. Overview of PayTM Payment Lifecycle

Completing the checkout involves number of calls between app, backend and PayTM server. Below is lifecycle of a transaction from initiation to payment completion.

1. Preparing Order: Once customer selects the items, the cart items will be sent to backend server. This will insert a new order row or update existing row in db and generate unique Order ID. This order ID has to be unique each time user redirected to PayTM payment screen, otherwise PayTM throws duplicate order id error.

2. Order Id & Checksum: The next step is to generate checksum considering the order ID. The checksum has to be generated considering the mandatory fields and Merchant ID.

3. Redirecting to PayTM: Once the checksum is generated, user will be redirected to PayTM payment screen showing multiple payment options like Debit / Credit card, Wallet, Net Banking etc., In this step, if the generated checksum is wrong, user will redirected back to app with error status.

4. Bank Transaction: User completes the payment by choosing the options provided. Here PayTM takes care of communicating with bank and completing the payment.

5. Verifying transaction: Once the transaction is completed, it has to be verified on backend also to avoid fraudulent transactions. Here the backend server makes CURL request to PayTM server to verify the transaction status.

6. Order status: The transaction status is received from backend and appropriate order status is shown to user.

The below diagram illustrates the communication flow between each party. Here you can see the transaction lifecycle in detailed manner.

paytm android integration php laravel flow

3. Integrating PayTM gateway

In real life scenario, building an e-commerce app needs proper architecture and taking security precautions. This article aims to explain the flow as simple as possible. So I have built what is necessary to complete the payment gateway integration. Integrating PayTM (or any other gateway) involves the below steps.

3.1. Registering with PayTM and obtain necessary API keys.
3.2 Building backend app with necessary database schema and REST API. Integrating the PayTM server SDK.
3.3 Integrating PayTM mobile SDK in android / iOS app.

3.1 PayTM SandBox – Test API Details

To get started with PayTM, you can register a new account and get a test account to tryout the payment gateway. Follow the below steps to get your sandbox credentials.

1. Goto PayTM developer website and proceed with necessary steps to create a new account / login into existing account.

2. Once registered, navigate to API Keys in left navigation panel.

3. You can notice API details for Test and Production. Once the app is tested in sandbox, you can approach PayTM team to make your app live.

paytm-developer-test-api-keys

3.2 Building the backend – REST API

I have chosen Laravel (PHP) framework to build the backend module. In this, I have written necessary REST API required for this app. The admin panel is not built in this project.

This backend app / REST is already live and publicly available to tryout.

Base Url: https://demo.androidhive.info/paytm/public/api/

Postman dump: https://www.getpostman.com/collections/8b2e7763a8b7e0673918

Header Field Value Description
Authorization Bearer A492Kdleo3d83ba21699… Use the token received in /login or /register call
Content-Type application/json
Endpoint Method Description
/appConfig GET PayTM app config like Merchant ID and app environment (dev / production)
/register POST Registering a new user. This returns auth token needed to make further calls.
/login POST Login of an existing user. This returns auth token needed to make further calls.
/products GET Fetching all products along with name, thumbnail and price.
/prepareOrder POST Preparing a new order. This takes list of cart items and gives the unique Order ID that needs to be sent to PayTM
/getChecksum POST Generates the checksum needed while redirecting to PayTM payment screen.
/transactionStatus POST Verifies the transaction status once the payment is done. This involves our backend server making call to PayTM server and verifies the transaction
/transactions GET List of transactions made by a user
/orders/{id} GET Complete details of a single order including the total amount and list of items ordered

3.3 PayTM Android SDK Integration

Unlike my earlier tutorials, I am focusing only on important things in this article. The code of complete app can be found on Github page. Here, I am particularly interested in PayTMActivity.java as it has the core components of PayTM module.

The user will redirected to PayTM activity once the cart items are selected and clicked on Pay. This activity communicates with backend server multiple times to fulfil the order. I have written detailed comments under each method used.

Step 1: The list of cart items sent to server to create a new order. Here we call /prepareOrder endpoint to generate the unique order ID.

Step 2: Once the order ID is received, next we call /getChecksum by passing the Order ID along with other params to generate the checksum hash.

Step 3: Once the checksum is received, we call pgService.startPaymentTransaction() to redirect user to PayTM payment screen.

Step 4: Once the payment is completed, /transactionStatus is called to verify the transaction on server. Here we pass the same Order ID to verify the status.

Step 5: Once the transaction status is received, user will be shown success or failed screen by calling showOrderStatus() method.

package info.androidhive.paytmgateway.ui.paytm;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.paytm.pgsdk.PaytmOrder;
import com.paytm.pgsdk.PaytmPGService;
import com.paytm.pgsdk.PaytmPaymentTransactionCallback;
import com.wang.avi.AVLoadingIndicatorView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import info.androidhive.paytmgateway.BuildConfig;
import info.androidhive.paytmgateway.R;
import info.androidhive.paytmgateway.app.Constants;
import info.androidhive.paytmgateway.db.AppDatabase;
import info.androidhive.paytmgateway.db.model.CartItem;
import info.androidhive.paytmgateway.db.model.User;
import info.androidhive.paytmgateway.networking.model.AppConfig;
import info.androidhive.paytmgateway.networking.model.ChecksumResponse;
import info.androidhive.paytmgateway.networking.model.Order;
import info.androidhive.paytmgateway.networking.model.OrderItem;
import info.androidhive.paytmgateway.networking.model.PrepareOrderRequest;
import info.androidhive.paytmgateway.networking.model.PrepareOrderResponse;
import info.androidhive.paytmgateway.ui.base.BaseActivity;
import info.androidhive.paytmgateway.ui.transactions.TransactionsActivity;
import io.realm.Realm;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import timber.log.Timber;

public class PayTMActivity extends BaseActivity {
    @BindView(R.id.lbl_status)
    TextView lblStatus;

    @BindView(R.id.loader)
    AVLoadingIndicatorView loader;

    @BindView(R.id.icon_status)
    ImageView iconStatus;

    @BindView(R.id.status_message)
    TextView statusMessage;

    @BindView(R.id.title_status)
    TextView responseTitle;

    @BindView(R.id.btn_check_orders)
    TextView btnCheckOrders;

    @BindView(R.id.layout_order_placed)
    LinearLayout layoutOrderPlaced;

    private Realm realm;
    private AppConfig appConfig;
    private User user;

    /**
     * Steps to process order:
     * 1. Make server call to prepare the order. Which will create a new order in the db
     * and returns the unique Order ID
     * <p>
     * 2. Once the order ID is received, send the PayTM params to server to calculate the
     * Checksum Hash
     * <p>
     * 3. Send the PayTM params along with checksum hash to PayTM gateway
     * <p>
     * 4. Once the payment is done, send the Order Id back to server to verify the
     * transaction status
     */

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pay_tm);
        ButterKnife.bind(this);
        setToolbar();
        enableToolbarUpNavigation();
        getSupportActionBar().setTitle(getString(R.string.title_preparing_order));
        changeStatusBarColor();
        init();
    }

    @Override
    public int getLayoutId() {
        return R.layout.activity_pay_tm;
    }

    private void init() {
        realm = Realm.getDefaultInstance();
        realm.where(CartItem.class).findAllAsync()
                .addChangeListener(cartItems -> {

                });

        user = AppDatabase.getUser();
        appConfig = realm.where(AppConfig.class).findFirst();

        prepareOrder();
    }

    private void setStatus(int message) {
        lblStatus.setText(message);
    }

    /**
     * STEP 1: Sending all the cart items to server and receiving the
     * unique order id that needs to be sent to PayTM
     */
    private void prepareOrder() {
        setStatus(R.string.msg_preparing_order);

        List<CartItem> cartItems = realm.where(CartItem.class).findAll();
        PrepareOrderRequest request = new PrepareOrderRequest();
        List<OrderItem> orderItems = new ArrayList<>();
        for (CartItem cartItem : cartItems) {
            OrderItem orderItem = new OrderItem();
            orderItem.productId = cartItem.product.id;
            orderItem.quantity = cartItem.quantity;
            orderItems.add(orderItem);
        }

        request.orderItems = orderItems;

        getApi().prepareOrder(request).enqueue(new Callback<PrepareOrderResponse>() {
            @Override
            public void onResponse(Call<PrepareOrderResponse> call, Response<PrepareOrderResponse> response) {
                if (!response.isSuccessful()) {
                    handleUnknownError();
                    showOrderStatus(false);
                    return;
                }

                getChecksum(response.body());
            }

            @Override
            public void onFailure(Call<PrepareOrderResponse> call, Throwable t) {
                handleError(t);
                showOrderStatus(false);
            }
        });
    }

    /**
     * STEP 2:
     * Sending the params to server to generate the Checksum
     * that needs to be sent to PayTM
     */
    void getChecksum(PrepareOrderResponse response) {
        setStatus(R.string.msg_fetching_checksum);

        if (appConfig == null) {
            Timber.e("App config is null! Can't place the order. This usually shouldn\'t happen");
            // navigating user to login screen
            launchLogin(PayTMActivity.this);
            finish();
            return;
        }

        Map<String, String> paramMap = preparePayTmParams(response);
        Timber.d("PayTm Params: %s", paramMap);

        getApi().getCheckSum(paramMap).enqueue(new Callback<ChecksumResponse>() {
            @Override
            public void onResponse(Call<ChecksumResponse> call, Response<ChecksumResponse> response) {
                if (!response.isSuccessful()) {
                    Timber.e("Network call failed");
                    handleUnknownError();
                    showOrderStatus(false);
                    return;
                }

                Timber.d("Checksum Received: " + response.body().checksum);

                // Add the checksum to existing params list and send them to PayTM
                paramMap.put("CHECKSUMHASH", response.body().checksum);
                placeOrder(paramMap);
            }

            @Override
            public void onFailure(Call<ChecksumResponse> call, Throwable t) {
                handleError(t);
                showOrderStatus(false);
            }
        });
    }

    public Map<String, String> preparePayTmParams(PrepareOrderResponse response) {
        Map<String, String> paramMap = new HashMap<String, String>();
        paramMap.put("CALLBACK_URL", String.format(BuildConfig.PAYTM_CALLBACK_URL, response.orderGatewayId));
        paramMap.put("CHANNEL_ID", appConfig.getChannel());
        paramMap.put("CUST_ID", "CUSTOMER_" + user.id);
        paramMap.put("INDUSTRY_TYPE_ID", appConfig.getIndustryType());
        paramMap.put("MID", appConfig.getMerchantId());
        paramMap.put("WEBSITE", appConfig.getWebsite());
        paramMap.put("ORDER_ID", response.orderGatewayId);
        paramMap.put("TXN_AMOUNT", response.amount);
        return paramMap;
    }


    /**
     * STEP 3: Redirecting to PayTM gateway with necessary params along with checksum
     * This will redirect to PayTM gateway and gives us the PayTM transaction response
     */
    public void placeOrder(Map<String, String> params) {
        setStatus(R.string.msg_redirecting_to_paytm);

        // choosing between PayTM staging and production
        PaytmPGService pgService = BuildConfig.IS_PATM_STAGIN ? PaytmPGService.getStagingService() : PaytmPGService.getProductionService();

        PaytmOrder Order = new PaytmOrder(params);

        pgService.initialize(Order, null);

        pgService.startPaymentTransaction(this, true, true,
                new PaytmPaymentTransactionCallback() {
                    @Override
                    public void someUIErrorOccurred(String inErrorMessage) {
                        Timber.e("someUIErrorOccurred: %s", inErrorMessage);
                        finish();
                        // Some UI Error Occurred in Payment Gateway Activity.
                        // // This may be due to initialization of views in
                        // Payment Gateway Activity or may be due to //
                        // initialization of webview. // Error Message details
                        // the error occurred.
                    }

                    @Override
                    public void onTransactionResponse(Bundle inResponse) {
                        Timber.d("PayTM Transaction Response: %s", inResponse.toString());
                        String orderId = inResponse.getString("ORDERID");
                        verifyTransactionStatus(orderId);
                    }

                    @Override
                    public void networkNotAvailable() { // If network is not
                        Timber.e("networkNotAvailable");
                        finish();
                        // available, then this
                        // method gets called.
                    }

                    @Override
                    public void clientAuthenticationFailed(String inErrorMessage) {
                        Timber.e("clientAuthenticationFailed: %s", inErrorMessage);
                        finish();
                        // This method gets called if client authentication
                        // failed. // Failure may be due to following reasons //
                        // 1. Server error or downtime. // 2. Server unable to
                        // generate checksum or checksum response is not in
                        // proper format. // 3. Server failed to authenticate
                        // that client. That is value of payt_STATUS is 2. //
                        // Error Message describes the reason for failure.
                    }

                    @Override
                    public void onErrorLoadingWebPage(int iniErrorCode,
                                                      String inErrorMessage, String inFailingUrl) {
                        Timber.e("onErrorLoadingWebPage: %d | %s | %s", iniErrorCode, inErrorMessage, inFailingUrl);
                        finish();
                    }

                    @Override
                    public void onBackPressedCancelTransaction() {
                        Toast.makeText(PayTMActivity.this, "Back pressed. Transaction cancelled", Toast.LENGTH_LONG).show();
                        finish();
                    }

                    @Override
                    public void onTransactionCancel(String inErrorMessage, Bundle inResponse) {
                        Timber.e("onTransactionCancel: %s | %s", inErrorMessage, inResponse);
                        finish();
                    }
                });
    }

    /**
     * STEP 4: Verifying the transaction status once PayTM transaction is over
     * This makes server(own) -> server(PayTM) call to verify the transaction status
     */
    private void verifyTransactionStatus(String orderId) {
        setStatus(R.string.msg_verifying_status);
        getApi().checkTransactionStatus(orderId).enqueue(new Callback<Order>() {
            @Override
            public void onResponse(Call<Order> call, Response<Order> response) {
                if (!response.isSuccessful()) {
                    Timber.e("Network call failed");
                    handleUnknownError();
                    showOrderStatus(false);
                    return;
                }

                showOrderStatus(response.body().status.equalsIgnoreCase(Constants.ORDER_STATUS_COMPLETED));
            }

            @Override
            public void onFailure(Call<Order> call, Throwable t) {
                handleError(t);
                showOrderStatus(false);
            }
        });
    }

    /*
     * Displaying Order Status on UI. This toggles UI between success and failed cases
     * */
    private void showOrderStatus(boolean isSuccess) {
        loader.setVisibility(View.GONE);
        lblStatus.setVisibility(View.GONE);
        if (isSuccess) {
            iconStatus.setImageResource(R.drawable.baseline_check_black_48);
            iconStatus.setColorFilter(ContextCompat.getColor(this, R.color.colorGreen));
            responseTitle.setText(R.string.thank_you);
            statusMessage.setText(R.string.msg_order_placed_successfully);

            // as the order placed successfully, clear the cart
            AppDatabase.clearCart();
        } else {
            iconStatus.setImageResource(R.drawable.baseline_close_black_48);
            iconStatus.setColorFilter(ContextCompat.getColor(this, R.color.btn_remove_item));
            responseTitle.setText(R.string.order_failed);
            statusMessage.setText(R.string.msg_order_placed_failed);
        }

        layoutOrderPlaced.setVisibility(View.VISIBLE);
    }

    @OnClick(R.id.btn_check_orders)
    void onOrdersClick() {
        startActivity(new Intent(PayTMActivity.this, TransactionsActivity.class));
        finish();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            finish();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (realm != null) {
            realm.removeAllChangeListeners();
            realm.close();
        }
    }
}

4. Testing the App

Once you have completed the PayTM integration, you can test your app (or the app provided in this article), using the test mode credentials provided.

Mobile / username 7777777777
OTP 489871

5. References

Below are useful links you can keep handy while working on this project.
1. PayTM Android SDK documentation
2. PayTM REST API documentation
3. PayTM test mode credentials

I hope this article simplified the PayTM integration process. If you have any queries / suggestion, pls do post in the comment section below.

Happy Coding πŸ™‚

Hi there! I am Founder at androidhive and programming enthusiast. My skills includes Android, iOS, PHP, Ruby on Rails and lot more. If you have any idea that you would want me to develop? Let’s talk: ravi@androidhive.info
  • Fenny

    Woow!!! vry nice tutorial !!
    great work.

  • BraveShine.

    OMG MAN… Laravel & Android. This course is far far beyond Amazing. Absolutely Killed it, Litterly just need to do design pattern and dependency injection, and Boom. Real World App, For real. this is really far beyond of a tutorial. a great reference about how to mass with laravel and auth and how to get things into android with retrofit… got no word about what u did here. just amazing. :)))

    • Thanks Brave for your kind appreciation:)

      All the best!

      • BraveShine.

        Hello Again. can u please add pagination to this tutorial?
        Consider this, your customer getting almost 1000 product from retrofit, it’s not really smart to get all of them in same time. can u please add paginate in android app to make sure this won’t happen?

        • Hi Brave,

          The main intent here is to show the basic concepts of e-commerce app. You can learn the concepts and apply in the same project.

          • BraveShine.

            i know what u say and i undrestand, but as u know things are quite different when u use something like laravel. if i use paginate how can i do pagination base on data i’m getting from server? i know things u’re talking about but handling pagination from server and bringing into android is another thing. i’m sure not gonna take u much time to do a tutorial base on this. just changing user->get; to get()->paginate(5); then show us how u handle the pagination from server in android.

  • Anele

    This is insane Ravi very impressive work

  • I am glad this helped you in time:)

    I had the checksum mismatch problem for few days, but later using the latest PHP SDK it worked.

    • Rahul Kumar

      Thank you so much Ravi

  • Dirishala Vinod

    But By using this SDK we can’t get the amount available in Paytm user wallet bro

  • John Paul Lim Gabule

    awesome as always

  • Lynford Lagondi

    Good work Ravi Tamada….I learned a lot from androidhive Thank you so much for sharing your knowledge.

  • ondabpay

    Hi ravi, I learned a lot from androidhive like push notification, paytm integration but in paytm integration i found error in /prepareOrder from android mobile Response{protocol=http/1.1, code=500, message=Internal Server Error, url=http://XXXXXXXXXX.in/PayTM/public/api/v1/prepareOrder}

  • Firoz

    why do we need to use the backend REST api?? And where did you call backend API, in which line, in android code??

    • In PayTMActivity every method makes REST API call using retrofit.

  • Sagar Chavda

    @ravi8x:disqus how can i add line break (n) in arabic text inside resource file?

    • Line break works directly if you add n. Whats the problem?

  • AbelardoLG

    BIG THANKS, Ravi!!!!!

  • AbelardoLG

    I’m afraid PayTM allows only Indian phone numbers.

  • Rachit Mishra

    @@ravi8x:disqus how to use sabre sop api in android .please give me any idea .

  • Seems your IP address is not allowed. May be it’s blocked and you are accessing it from other countries than INDIA.

  • adi

    I have developed an ecommerce android app but using the firebase as the database. So is the process same or will there be any changes… I can send you the code if you would like to see the working… I just want to add the payment gateway to it..

  • Mahesh Pawar

    Hey Ravi,
    I am getting error “Paytm checksum mismatch.”
    Can you please tell me how to solve this error?

    • Are you using the latest PayTM php classes?

  • Marlos Trinidad

    muito bom, incrΓ­vel
    ! Obrigado.

  • Sachin Dodti

    I cant register my app its gives error : “message”: “Server Error”.

    Plzz Help

    • Have restarted the services. Pls check now.

  • Hanumant Nalwade

    Where is backend sql file ?

    • The database tables will be created by running migrations. No plain sql file will be available.

  • Bhupinder Tandon

    how to solve the error of can not find the symbol glideapp

  • Deepa bharti

    Hi sir, i am integrating Paytm but unable to redirect on paytm screen. please help i mm stuck