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>The credential rule, first
Section titled “The credential rule, first”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 serverconst order = await createIntent({ order_id: 'ORD-10432', amount_paise: 49900 });res.render('checkout', { pageToken: order.page_token });Your own buttons
Section titled “Your own buttons”<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.
mount(options)
Section titled “mount(options)”| 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.
What it does while it is running
Section titled “What it does while it is running”- Polls
/p/{token}/statusevery 2 seconds, and pauses while the tab is hidden. It reads theDateresponse header to correct for a wrong clock on the customer’s device rather than trustingDate.now(). - Heartbeats
/p/{token}/extendevery 60 seconds while a reservation is open and the tab is visible. - On a
429it 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
409from extend — the reservation lapsed — it goes back to “tap again to pay” rather than silently sending the customer to a stale amount.
The hosted page
Section titled “The hosted page”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
Section titled “Do not”- Do not put your API key in the page. Ever, for anything. There is no client-side use for it.
- Do not call
reserveon page load. Every open reservation is an amount nobody else can use. - Do not cache
upi_uriorapp_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.