CourseWhatsApp Automation for Business
ModulePayments and production
Duration12 min
Progress0/7
Course material0/7 · 0%
  1. 1What we are building, and the project setup8 min
  2. 2Connecting to the WhatsApp Cloud API12 min
  3. 3Sessions and the order flow state machine12 min
  4. 4The mini CRM: contacts, orders, and labels10 min
  5. 5Payments in chat with the Midtrans sandbox12 min
  6. 6Hardening: dedupe, retries, and human takeover10 min
  7. 7Deploying with Docker, off the laptop for good12 min

Lesson 5 / 7 · 12 min

Payments in chat with the Midtrans sandbox

Create an invoice, send the link, verify the payment webhook signature, auto-confirm. Checkpoint: a full paid order, end to end.

Register a free Midtrans account and switch the dashboard to Sandbox mode: real API, test money. Copy the sandbox Server Key into MIDTRANS_SERVER_KEY in .env. We will use Snap, their hosted payment page that accepts QRIS, virtual accounts, e-wallets, and cards, so we never touch card data ourselves.

Step 1: create the invoice (pay.js)

export async function createInvoice(orderId, amount) {
  const auth = Buffer
    .from(process.env.MIDTRANS_SERVER_KEY + ":")
    .toString("base64");

  const res = await fetch(
    "https://app.sandbox.midtrans.com/snap/v1/transactions",
    {
      method: "POST",
      headers: {
        Authorization: "Basic " + auth,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        transaction_details: {
          order_id: orderId,
          gross_amount: amount,
        },
      }),
    },
  );
  const data = await res.json();
  return data.redirect_url; // the hosted payment page
}
One POST, one URL back. In production you only change the domain (remove .sandbox) and the key.

Step 2: send the link from the flow

// server.js: after step(), handle the action from CONFIRM
import { createInvoice } from "./pay.js";

const { reply, action } = step(session, msg.text.body);
saveSession(session);
if (reply) await sendText(msg.from, reply);

if (action === "create_invoice") {
  const link = await createInvoice(
    session.data.orderId,
    session.data.total,
  );
  await sendText(
    msg.from,
    "Total Rp " + session.data.total.toLocaleString("id-ID") +
    ". Silakan bayar di sini ya: " + link,
  );
}
The flow decided WHAT should happen; the server decides HOW. Keeping actions out of flow.js keeps it testable.

Step 3: the payment webhook, with signature verification

// server.js
import crypto from "crypto";
import { db, addLabel } from "./db.js";

app.post("/payment/notify", async (req, res) => {
  res.sendStatus(200);
  const n = req.body;

  // Midtrans signs: sha512(order_id + status_code + gross_amount + ServerKey)
  const expected = crypto
    .createHash("sha512")
    .update(
      n.order_id + n.status_code + n.gross_amount +
      process.env.MIDTRANS_SERVER_KEY,
    )
    .digest("hex");
  if (expected !== n.signature_key) {
    return console.error("BAD SIGNATURE for", n.order_id);
  }

  if (n.transaction_status === "settlement" ||
      n.transaction_status === "capture") {
    // idempotent: only flips once, no matter how many callbacks arrive
    const r = db.prepare(
      "UPDATE orders SET status='paid', paid_at=unixepoch() " +
      "WHERE id = ? AND status != 'paid'",
    ).run(n.order_id);

    if (r.changes === 1) {
      const o = db.prepare("SELECT phone FROM orders WHERE id = ?")
        .get(n.order_id);
      addLabel(o.phone, "customer");
      await sendText(o.phone,
        "Pembayaran diterima. Pesanan Anda sedang diproses ya, terima kasih!");
    }
  }
});
Three non-negotiables in one handler: verify the signature, update idempotently, confirm to the customer.

Step 4: tell Midtrans where to call

In the Midtrans sandbox dashboard, set the Payment Notification URL to your ngrok address plus /payment/notify. Then place an order from your phone, open the link, and pay with one of the sandbox test methods listed in the Midtrans docs (test cards and simulated QRIS or virtual accounts).

Checkpoint: order from your phone, pay in the sandbox, and watch the bot confirm by itself, with the order flipped to paid in the database and the customer label applied. That loop, request to money to confirmation with zero human touches, is the heart of commerce automation.

What you take away

  • Snap gives a hosted payment page; you never handle card data
  • Verify the sha512 signature on every notification, no exceptions
  • The paid update is idempotent: WHERE status != 'paid' guards double callbacks
  • Sandbox proves the full loop with test money before any real rupiah moves

Curated sources

Hand-picked references for going deeper, reviewed by our team.

Related Etzal serviceWhatsApp AI chatbotPrefer it done for you? See how we help.
Payments in chat with the Midtrans sandbox - WhatsApp Automation for Business | Etzal Academy | Etzal Group