Skip to content

Web

One script, one call. It handles reserving at the tap, opening the right UPI app, polling for confirmation and keeping the reservation alive while the customer is still on the page.

<script src="https://moneylanded.com/sdk/v1/moneylanded.js"></script>

Your API key never goes in a browser. vp_… is a server-side credential and anyone holding it can create and cancel orders on your account.

What the browser gets is the page token: 128 bits, scoped to one order, and useless for anything else. Your backend calls POST /v1/intents, keeps the key, and passes only page_token to the page. Everything the SDK does goes through /p/{token}/….

// your server
const order = await createIntent({ order_id: 'ORD-10432', amount_paise: 49900 });
res.render('checkout', { pageToken: order.page_token });
<button id="pay-phonepe">PhonePe</button>
<button id="pay-gpay">Google Pay</button>
<button id="pay-other">Other UPI app</button>
<script>
var ml = moneylanded.mount({
token: 'THE_PAGE_TOKEN',
renderButtons: false,
onConfirmed: function (s) { location.href = '/thanks'; },
onReserved: function (r) { console.log('reserved', r.paid_paise, 'off by', r.discount_paise); },
onError: function (e) { console.warn('moneylanded', e); }
});
document.getElementById('pay-phonepe').onclick = function () { vp.pay('phonepe'); };
document.getElementById('pay-gpay').onclick = function () { vp.pay('gpay'); };
document.getElementById('pay-other').onclick = function () { vp.pay('other'); };
</script>

vp.pay(app) reserves an amount and then opens the link — one tap, one round trip. Do not reserve on page load; reserve when they tap. That is the whole reason the discount stays at a paisa or two instead of growing to the size of your traffic.

option
token Required. The page token.
base Origin to call. Defaults to wherever the script was loaded from, so you almost never set it.
renderButtons false to suppress all DOM handling and drive it yourself.
onConfirmed(status) Paid. status is the pay-page status object.
onStatus(status) Every successful poll.
onReserved(reservation) After a successful reserve: paid_paise, discount_paise, upi_uri, app_links, vpa, qr_svg.
onError(err) A reserve failed, or the network did.

Returns:

pay(app, open) Reserve and open. app is phonepe, gpay, paytm, bhim, other or qr. Pass open as false to reserve without navigating — that is what the desktop QR does.
refresh() Poll status once, now.
stop() Stop polling and heartbeating. Call it if you tear the checkout down yourself.
token, base What it is using.

A callback that throws is swallowed — a bug in your success handler will not take the payment flow down with it.

  • Polls /p/{token}/status every 2 seconds, and pauses while the tab is hidden. It reads the Date response header to correct for a wrong clock on the customer’s device rather than trusting Date.now().
  • Heartbeats /p/{token}/extend every 60 seconds while a reservation is open and the tab is visible.
  • On a 429 it backs off for a minute and shows “busy”. Between the poll and the heartbeat it is 31 requests a minute against a limit of 60, so there is room for your own calls but not for a second poller.
  • On a 409 from extend — the reservation lapsed — it goes back to “tap again to pay” rather than silently sending the customer to a stale amount.

If you do not want to build a checkout at all, page_url from POST /v1/intents is a complete one, running this same SDK. It shows your name, the description and the price, four app buttons on mobile, a QR on desktop, and a copy-the-UPI-ID fallback behind “pay another way”. It has no input fields of any kind and asks the customer for nothing.

It redirects to your success URL on confirmation, if you have set one in Settings.

  • Do not put your API key in the page. Ever, for anything. There is no client-side use for it.
  • Do not call reserve on page load. Every open reservation is an amount nobody else can use.
  • Do not cache upi_uri or app_links. Reserving again after a lapse can return a different amount, and a link carrying the old one will land in your unplaced list.
  • Do not add your own second poller. The token limit is 60 a minute and the SDK is already using half of it.