growth

WhatsApp Commerce UPI Payment Workflows in India

Connect WhatsApp orders and UPI payments with secure hosted links, dynamic QR, verified payment webhooks, idempotent fulfilment, and customer updates.

VC VyaparGateway Commerce Team Conversational Commerce 5 min read

Fact-checked and updated

WhatsApp Commerce UPI Payment Workflows in India guide
WhatsApp commerce India WhatsApp UPI payment payment workflow UPI payment link

A WhatsApp order often starts with an unstructured message: “Do you have the blue kurta in medium?” A production commerce flow has to turn that conversation into a priced order, a verifiable UPI payment, and a single fulfilment action—without asking staff to inspect screenshots.

The safest design uses WhatsApp for conversation and notifications, an HTTPS page for checkout, and the payment backend as the source of truth.

Design the customer journey

A clean flow for an Indian merchant:

1. Customer starts or consents to a WhatsApp conversation.
2. Bot or agent confirms product, quantity, delivery details, and total.
3. Commerce backend creates immutable order WA-1042 for ₹1,249.
4. Backend creates a short-lived payment session.
5. WhatsApp message contains an HTTPS “Pay ₹1,249” button.
6. Customer opens the page and taps a UPI app or scans the QR.
7. Payment backend verifies the bank/provider result.
8. Order changes from PAYMENT_PENDING to PAID exactly once.
9. Merchant sends an order-confirmation message.
10. Warehouse or service fulfilment starts from the paid event.

The message should show the merchant, order, amount, and expiry in plain language:

Your order WA-1042 is ready.
Cotton kurta, blue, size M
Total: ₹1,249.00
Payment link valid for 15 minutes.

[Pay securely]

Avoid “Pay immediately or your account will be blocked” language, unexpected payment requests, or vague links. Conversational commerce is a fraud target; clarity is part of security.

Why send HTTPS instead of a raw UPI URI?

A raw link such as upi://pay?... depends on client handling and can be difficult to audit, expire, revoke, or protect from forwarding. An HTTPS payment page lets you:

  • validate a signed/opaque session token;
  • show the merchant and order before app launch;
  • choose mobile intent or desktop QR;
  • expire the request server-side;
  • check status without exposing credentials;
  • provide support and retry guidance;
  • capture only necessary, consented analytics.

The UPI app launch should still require a customer tap.

Two-webhook architecture

You have two independent event producers:

WhatsApp Business Platform
   │ inbound message/status webhook

Conversation service ──► Order service ──► Payment API
                              ▲                  │
                              │                  │ signed payment webhook
                              └──────────────────┘


                               Fulfilment outbox


                          WhatsApp confirmation message

Do not mix their credentials:

BoundaryCredential/controlPurpose
Meta → your webhookMeta webhook verification and request signature validationAuthenticate WhatsApp events
Your server → WhatsApp APISystem-user/access tokenSend approved messages
Your server → payment APIMerchant API keyCreate and query payment orders
Payment provider → your webhookWebhook signature secretAuthenticate payment events
Customer → hosted payment pageExpiring opaque tokenAuthorise access to one public session

A token from one boundary must never substitute for another.

Order state machine

DRAFT
  └──► AWAITING_CUSTOMER_CONFIRMATION
           └──► PAYMENT_PENDING
                    ├──► PAID ─► FULFILMENT_PENDING ─► FULFILLED
                    ├──► EXPIRED
                    └──► CANCELLED

Do not move from EXPIRED to PAID automatically without a documented late-payment policy. A late bank credit may need human review, refund, or inventory revalidation.

Create the business order first, using integer paise:

type CommerceOrder = {
  id: string;
  customerId: string;
  amountPaise: bigint;
  currency: "INR";
  status: "PAYMENT_PENDING";
};

const order: CommerceOrder = {
  id: "WA_2026_1042",
  customerId: "cus_7de2f4",
  amountPaise: 124900n,
  currency: "INR",
  status: "PAYMENT_PENDING",
};

Create the payment from the server:

const paymentResponse = await fetch(
  "https://vyapargateway.com/api/v1/create_order",
  {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": process.env.VYAPARGATEWAY_API_KEY!,
    },
    body: JSON.stringify({
      client_txn_id: order.id,
      amount: Number(order.amountPaise) / 100,
      p_info: `WhatsApp order ${order.id}`,
      callback_url: `${process.env.APP_URL}/webhooks/payment`,
      redirect_url: `${process.env.APP_URL}/pay/complete`,
    }),
  },
);

Generate a random public token and store only a hash:

import { createHash, randomBytes } from "node:crypto";

const publicToken = randomBytes(32).toString("base64url");
const tokenHash = createHash("sha256")
  .update(publicToken)
  .digest("hex");

await paymentSessions.insert({
  tokenHash,
  orderId: order.id,
  expiresAt: new Date(Date.now() + 15 * 60 * 1000),
});

const paymentUrl =
  `${process.env.APP_URL}/pay/${encodeURIComponent(publicToken)}`;

The URL contains no phone number, amount, API key, webhook secret, or predictable order sequence. The page looks up the token hash and returns only the public QR/intent data for that session.

Handle forwarding

A customer might forward the link. Decide whether possession of the token is sufficient to pay the order. If order details are sensitive, ask for a low-friction second factor, such as the last four digits of the destination phone number, without exposing whether unrelated orders exist.

The payment should still credit the correct merchant and order even if another person completes it. Do not assume payer VPA equals customer identity unless your product and privacy notice explicitly support that association.

Send and receive WhatsApp events

Use the current WhatsApp Business Platform documentation and Graph API version configured for your app. Do not hard-code a blog’s old version number.

An illustrative template send request:

await fetch(
  `https://graph.facebook.com/v${process.env.META_GRAPH_VERSION}` +
    `/${process.env.WHATSAPP_PHONE_NUMBER_ID}/messages`,
  {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.WHATSAPP_ACCESS_TOKEN}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      messaging_product: "whatsapp",
      to: customer.whatsappNumber,
      type: "template",
      template: {
        name: "upi_order_payment",
        language: { code: "en_IN" },
        components: [
          {
            type: "body",
            parameters: [
              { type: "text", text: order.displayName },
              { type: "text", text: "₹1,249.00" },
            ],
          },
          {
            type: "button",
            sub_type: "url",
            index: "0",
            parameters: [
              { type: "text", text: publicToken },
            ],
          },
        ],
      },
    }),
  },
);

The template and URL-button configuration must match what Meta approved. Keep the base domain in the template and supply only the permitted dynamic suffix. Follow current opt-in, category, service-window, and quality rules.

Verify inbound Meta events

Meta webhook setup uses a verification challenge and verify token. Store the verify token as a secret and compare it exactly:

app.get("/webhooks/whatsapp", (req, res) => {
  const mode = req.query["hub.mode"];
  const token = req.query["hub.verify_token"];
  const challenge = req.query["hub.challenge"];

  if (
    mode === "subscribe" &&
    token === process.env.WHATSAPP_VERIFY_TOKEN
  ) {
    return res.status(200).send(challenge);
  }

  return res.status(403).send();
});

For POST events, capture raw bytes and apply the request-signature validation specified by Meta for your app. Parse the JSON only after authentication. A verify token used during subscription is not, by itself, proof that each POST is authentic.

Confirm after payment

The payment webhook drives confirmation:

await database.transaction(async (tx) => {
  const inserted = await tx.paymentEvents.insertOnce(event.id, event);
  if (!inserted) return;

  const order = await tx.orders.lockByClientTxnId(event.client_txn_id);
  assertPaymentMatchesOrder(event, order);

  await tx.orders.markPaid(order.id, event.transaction_id);
  await tx.outbox.add({
    type: "WHATSAPP_PAYMENT_CONFIRMATION",
    aggregateId: order.id,
  });
});

An outbox worker sends the WhatsApp message after the order commit. This avoids the dangerous case where the customer receives “Paid” but the order transaction later rolls back.

Operate the workflow safely

  • Record how and when the customer opted in to WhatsApp messages.
  • Use transactional data only for the stated order purpose.
  • Set retention periods for message payloads and payment metadata.
  • Do not put full addresses or sensitive order contents into observability labels.
  • Honour opt-out and suppression state before non-essential messages.
  • Restrict staff access to conversations and payment records.

Reliability

  • Deduplicate Meta message IDs and payment event IDs.
  • Retry outbound messages with bounded exponential backoff.
  • Stop retrying permanent policy or invalid-recipient errors.
  • Reconcile payment orders independently of WhatsApp delivery.
  • Let staff search by order/reference, not by payment screenshot.
  • Keep a dead-letter queue with alerting and a replay action.

Fraud controls

  • Show the exact merchant name before UPI app launch.
  • Never request a UPI PIN in chat or on your website.
  • Warn that receiving money does not require entering a PIN.
  • Reject amount or currency mismatches.
  • Rate-limit payment-session creation per conversation and customer.
  • Expire links and rotate them when order totals change.
  • Send links only from the merchant’s verified WhatsApp identity.

Metrics

Track the funnel without storing message bodies in metric labels:

orders_created
payment_links_sent
payment_pages_opened
upi_intents_launched
payments_verified
confirmations_sent
fulfilments_started

Segment by safe business dimensions such as merchant, provider, campaign template, and time—not by phone number.

The key architectural idea is simple: WhatsApp carries the conversation, the hosted page carries the payment choice, and your verified backend event carries truth. With that separation, a merchant can scale conversational sales without teaching staff to trust screenshots or manually match amounts.

Direct answers

Frequently asked questions

Can a merchant send a UPI payment link through WhatsApp?
A merchant can send an HTTPS checkout link that opens a secure, order-specific payment page. On that page the customer can tap a UPI intent button or scan a QR. This is more reliable and controllable than placing a raw upi URI directly in a message.
How does WhatsApp learn that the UPI payment succeeded?
WhatsApp does not decide the payment result. The payment provider sends a verified event to the merchant backend, the backend updates the order idempotently, and then the backend sends an appropriate WhatsApp confirmation message.
Should the customer's phone number be placed in the payment URL?
No. Use a random, short-lived opaque token that maps to the order on the server. Avoid sequential order IDs, phone numbers, email addresses, API keys, and signatures in public links.
Can the same WhatsApp template be used for every order?
A reviewed template can include approved variables, such as an order name or a dynamic URL suffix, subject to the current WhatsApp Business Platform rules. Keep the message transactional and obtain required customer consent.
What happens if the payment webhook arrives twice?
The backend stores a unique event or transaction identifier and performs the paid-order transition once. A duplicate delivery receives a successful acknowledgement but does not resend fulfilment or alter inventory again.

Build your payment flow

Explore the API and browser-only merchant tools.

Create UPI checkout orders, verify signed events, or test the free calculators and generators without exposing credentials.