Paylink Widget (@palpluss/paylink)
Embed a Palpluss payment link into any website as a modal overlay or inline iframe. The widget is pure browser-side — your backend language (Node.js, PHP, Python, Laravel, Django, etc.) does not matter.
@palpluss/paylink is a frontend-only browser package. It has no dependency on @palpluss/sdk and runs entirely in the visitor’s browser. Your server never handles raw payment credentials.
Getting Your Paylink ID
Every integration — whether you use the npm package, the CDN script, or a raw iframe — starts with a paylink ID. Here is how to generate one from the Palpluss Console.
Step 1 — Open Create Pay Link
Log in to the Palpluss Console and click Create Pay Link in the left sidebar under the SERVICES section.
You will see the Generate Pay Link form with the following fields:
| Field | Required | Description |
|---|
| Payment Channel | Optional | Select a Paybill or Till to receive the payment. Leave empty to use your tenant default channel. |
| Amount (KES) | Optional | Pre-fill the amount. If left empty the customer enters it themselves on the payment page. |
| Customer Phone | Optional | Pre-fill the customer’s M-PESA number so they don’t have to type it. |
| Customer Name | Optional | Pre-fill the customer’s name on the payment page. |
| Reference | Optional | An invoice number, order ID, or any internal reference you want attached to the transaction. |
Click Advanced Options to set an expiry date or additional restrictions.
Step 2 — Generate the link
Once you have filled in the values you need, click the Generate Pay Link button at the bottom of the form.
A modal will appear confirming the link was created:
PAY LINK URL
https://link.palpluss.com/1fbb7b69-ac0e-4439-a08d-40bfd9ee13de
Status ACTIVE
Expires Never
Step 3 — Copy only the ID
The full URL has this format:
https://link.palpluss.com/{paylink-id}
You only need the ID portion — the UUID that comes after the last /. In the example above that is:
1fbb7b69-ac0e-4439-a08d-40bfd9ee13de
This is the value you pass to pay(), the <PalplussPayment> component, or the src attribute of the raw iframe:
// ✅ correct — just the ID
await pay('1fbb7b69-ac0e-4439-a08d-40bfd9ee13de')
// ❌ wrong — do not pass the full URL
await pay('https://link.palpluss.com/1fbb7b69-ac0e-4439-a08d-40bfd9ee13de')
You can click Preview in the modal to open the payment page in a new tab and confirm it looks correct before sharing it with customers.
How It Works
Your page Palpluss
───────────────────────────── ──────────────────────
pay('abc-123')
│
▼
Creates full-screen overlay
Injects <iframe src="https://link.palpluss.com/abc-123?embed=1">
│
│ ◄──── postMessage: RESIZE ──────────────── iframe adjusts height
│ ◄──── postMessage: PAYMENT_SUCCESS ─────── payment complete
│
▼
cleanup() — removes overlay, resolves Promise
pay() creates a modal overlay and injects an <iframe> pointing to https://link.palpluss.com/{id}?embed=1.
- The embedded page communicates back via
window.postMessage.
- On success the Promise resolves with
{ txId, amount, phone }. On close/cancel it rejects.
The iframe origin is https://link.palpluss.com — your page never handles raw M-PESA credentials or STK callbacks.
Installation (JavaScript / TypeScript only)
If you use PHP, Python, Ruby, or any other server-side language, skip this section and jump directly to Any Backend — Raw iframe. The npm package is only needed for JavaScript/TypeScript projects.
npm install @palpluss/paylink
React peer dependencies are only needed if you import @palpluss/paylink/react:
npm install react react-dom
Quick Start
import { pay } from '@palpluss/paylink'
const result = await pay('your-paylink-id')
// result → { txId: 'TXN_...', amount: 500, phone: '2547...' }
Get your paylink ID from the Palpluss Console → Payment Links.
API Reference
pay(paylinkId, options?)
Opens a full-screen modal containing the payment iframe.
Returns a Promise<PaymentResult> that:
- resolves when the customer completes payment
- rejects with
Error('Payment modal closed by user') when the modal is dismissed
| Parameter | Type | Required | Description |
|---|
paylinkId | string | Yes | The paylink ID from your Palpluss dashboard |
options | PayOptions | No | Optional callbacks |
PayOptions
| Option | Type | Description |
|---|
onSuccess | (data: PaymentResult) => void | Called immediately when payment succeeds, before the Promise resolves |
onClose | () => void | Called when the user dismisses the modal without paying |
const result = await pay('abc-123', {
onSuccess: (data) => console.log('paid', data),
onClose: () => console.log('modal closed'),
})
openModal(paylinkId, options, resolve, reject)
Low-level function used internally by pay(). Exposed for advanced cases where you need direct control of the Promise lifecycle.
Returns a cleanup() function that tears down the modal immediately.
import { openModal } from '@palpluss/paylink'
const cleanup = openModal(
'abc-123',
{},
(result) => console.log('resolved', result),
(err) => console.log('rejected', err),
)
// Force-close from outside
cleanup()
<PalplussPayment> (React)
Import from @palpluss/paylink/react. Renders the payment iframe inline — no overlay, no modal — so you control layout and placement.
import { PalplussPayment } from '@palpluss/paylink/react'
<PalplussPayment
id="abc-123"
onSuccess={({ txId, amount }) => console.log(txId, amount)}
className="my-payment-frame"
/>
| Prop | Type | Required | Description |
|---|
id | string | Yes | Paylink ID |
onSuccess | (data: PaymentResult) => void | No | Called on successful payment |
className | string | No | CSS class applied to the <iframe> |
The iframe starts at minHeight: 560px and auto-resizes via RESIZE postMessage events.
Usage Examples (JavaScript / TypeScript)
Vanilla JS / TypeScript
React — modal
React — inline embed
CDN script tag
import { pay } from '@palpluss/paylink'
document.getElementById('pay-btn')!.addEventListener('click', async () => {
try {
const { txId, amount } = await pay('abc-123')
window.location.href = `/thank-you?ref=${txId}&amount=${amount}`
} catch {
// user closed the modal — no action needed
}
})
import { pay } from '@palpluss/paylink'
export function CheckoutButton({ paylinkId }: { paylinkId: string }) {
async function handleClick() {
try {
const result = await pay(paylinkId)
console.log('Paid', result.amount, 'KES — ref:', result.txId)
} catch {
// modal dismissed
}
}
return <button onClick={handleClick}>Pay with M-PESA</button>
}
Use @palpluss/paylink/react when you want the payment form embedded directly in your page layout instead of a floating modal.import { PalplussPayment } from '@palpluss/paylink/react'
export function CheckoutPage() {
function handleSuccess({ txId, amount }: { txId: string; amount: number }) {
console.log(`Payment complete — ${amount} KES, ref: ${txId}`)
}
return (
<div className="checkout">
<h1>Complete your payment</h1>
<PalplussPayment id="abc-123" onSuccess={handleSuccess} />
</div>
)
}
No build step required. Drop this into any HTML page:<script src="https://cdn.palpluss.com/paylink.min.js"></script>
<button onclick="Palpluss.pay('abc-123').then(r => alert('Paid! Ref: ' + r.txId))">
Pay with M-PESA
</button>
Everything is available on the window.Palpluss global:Palpluss.pay('abc-123')
.then(result => console.log(result))
.catch(() => console.log('cancelled'))
Any Backend — Raw Iframe
This section is for PHP, Python, Ruby, Go, .NET, and any other server-side language. The @palpluss/paylink npm package is entirely optional — it is just a convenience wrapper around a standard <iframe>. You can embed the payment widget using plain HTML and a small <script> block in any templating engine, CMS, or static site.
How it works without the npm package
The paylink widget is an <iframe> hosted at https://link.palpluss.com/{paylink-id}?embed=1. The ?embed=1 query parameter tells the Palpluss page it is running inside an embedded context. Once loaded, the iframe sends two types of postMessage events back to your page:
| Event type | Payload | Purpose |
|---|
RESIZE | { height: number } | Resize the iframe to fit its content — eliminates scrollbars |
PAYMENT_SUCCESS | { txId, amount, phone } | Payment was completed successfully |
Your page listens for these messages and reacts accordingly. No SDK or npm package required.
Minimal HTML snippet
Replace abc-123 with your actual paylink ID from the Palpluss Console.
<!-- Palpluss Payment Widget -->
<iframe
id="palpluss-pay"
src="https://link.palpluss.com/abc-123?embed=1"
style="width: 100%; border: none; min-height: 600px; display: block;"
allow="payment"
></iframe>
<script>
window.addEventListener('message', function(e) {
if (!e.data || typeof e.data !== 'object') return;
// Auto-resize the iframe to fit its content
if (e.data.type === 'RESIZE') {
document.getElementById('palpluss-pay').style.height = e.data.height + 'px';
}
// Payment completed successfully
if (e.data.type === 'PAYMENT_SUCCESS') {
console.log('Payment confirmed!', {
txId: e.data.txId,
amount: e.data.amount,
phone: e.data.phone,
});
// Examples — uncomment what fits your use case:
// window.location.href = '/thank-you?ref=' + e.data.txId;
// document.getElementById('download-btn').disabled = false;
}
});
</script>
The allow="payment" attribute grants the iframe access to the Payment Request API in supporting browsers. It does not affect functionality in environments that don’t support it.
Framework examples
In your Blade template, output the paylink ID from your controller or config. Everything else is plain HTML:{{-- resources/views/checkout.blade.php --}}
<iframe
id="palpluss-pay"
src="https://link.palpluss.com/{{ $paylinkId }}?embed=1"
style="width: 100%; border: none; min-height: 600px; display: block;"
allow="payment"
></iframe>
<script>
window.addEventListener('message', function(e) {
if (!e.data || typeof e.data !== 'object') return;
if (e.data.type === 'RESIZE') {
document.getElementById('palpluss-pay').style.height = e.data.height + 'px';
}
if (e.data.type === 'PAYMENT_SUCCESS') {
// Redirect to a thank-you page with the transaction reference
window.location.href = '/thank-you?ref=' + e.data.txId + '&amount=' + e.data.amount;
}
});
</script>
Pass $paylinkId from your controller:// app/Http/Controllers/CheckoutController.php
public function show()
{
return view('checkout', [
'paylinkId' => config('services.palpluss.paylink_id'),
]);
}
Render the paylink ID from your Django view into the template:# views.py
from django.shortcuts import render
def checkout(request):
return render(request, 'checkout.html', {
'paylink_id': 'abc-123', # or load from settings / DB
})
{# templates/checkout.html #}
<iframe
id="palpluss-pay"
src="https://link.palpluss.com/{{ paylink_id }}?embed=1"
style="width: 100%; border: none; min-height: 600px; display: block;"
allow="payment"
></iframe>
<script>
window.addEventListener('message', function(e) {
if (!e.data || typeof e.data !== 'object') return;
if (e.data.type === 'RESIZE') {
document.getElementById('palpluss-pay').style.height = e.data.height + 'px';
}
if (e.data.type === 'PAYMENT_SUCCESS') {
window.location.href = '/thank-you/?ref=' + e.data.txId;
}
});
</script>
# app.py
from flask import Flask, render_template
import os
app = Flask(__name__)
@app.route('/checkout')
def checkout():
return render_template('checkout.html', paylink_id=os.getenv('PALPLUSS_PAYLINK_ID'))
{# templates/checkout.html #}
<iframe
id="palpluss-pay"
src="https://link.palpluss.com/{{ paylink_id }}?embed=1"
style="width: 100%; border: none; min-height: 600px; display: block;"
allow="payment"
></iframe>
<script>
window.addEventListener('message', function(e) {
if (!e.data || typeof e.data !== 'object') return;
if (e.data.type === 'RESIZE') {
document.getElementById('palpluss-pay').style.height = e.data.height + 'px';
}
if (e.data.type === 'PAYMENT_SUCCESS') {
window.location.href = '/thank-you?ref=' + e.data.txId;
}
});
</script>
If you have no server-side framework at all, hard-code the paylink ID directly. This works on any static site, CDN, or HTML file:<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
</head>
<body>
<h1>Complete your payment</h1>
<iframe
id="palpluss-pay"
src="https://link.palpluss.com/abc-123?embed=1"
style="width: 100%; border: none; min-height: 600px; display: block;"
allow="payment"
></iframe>
<script>
window.addEventListener('message', function(e) {
if (!e.data || typeof e.data !== 'object') return;
if (e.data.type === 'RESIZE') {
document.getElementById('palpluss-pay').style.height = e.data.height + 'px';
}
if (e.data.type === 'PAYMENT_SUCCESS') {
alert('Payment confirmed! Reference: ' + e.data.txId);
}
});
</script>
</body>
</html>
postMessage event reference
Your message event listener receives event.data with the following shapes:
// Auto-resize event — fired as the payment form grows or shrinks
{
type: 'RESIZE',
height: 640 // new iframe height in pixels
}
// Payment success event — fired once the customer completes payment
{
type: 'PAYMENT_SUCCESS',
txId: 'TXN_01J8ABC123', // transaction reference — store this
amount: 500, // amount paid in KES
phone: '254712345678' // M-PESA number used
}
Always check e.data.type before acting on a message. Other libraries on your page (analytics, chat widgets, etc.) also use postMessage and your listener will receive those events too.
Security note on postMessage
The iframe is served from https://link.palpluss.com. For production applications, validate the message origin before trusting the payload:
window.addEventListener('message', function(e) {
// Only trust messages from the Palpluss iframe origin
if (e.origin !== 'https://link.palpluss.com') return;
if (e.data.type === 'PAYMENT_SUCCESS') {
// safe to act on
}
});
TypeScript Types
import type { PaymentResult, PayOptions, IframeEvent } from '@palpluss/paylink'
type PaymentResult = {
txId: string // transaction reference
amount: number // amount paid in KES
phone: string // M-PESA number used
}
type PayOptions = {
onSuccess?: (data: PaymentResult) => void
onClose?: () => void
}
// postMessage events sent by the embedded iframe
type IframeEvent =
| { type: 'RESIZE'; height: number }
| { type: 'PAYMENT_SUCCESS'; txId: string; amount: number; phone: string }
How the Modal Works Internally
When pay() is called:
- Overlay — a
position: fixed; inset: 0 div with a semi-transparent backdrop is appended to document.body. z-index is set to 2147483647 (maximum) so it sits above everything.
- Card — a centered white card (
max-width: 860px) holds the close button and the iframe.
- Iframe — src is
https://link.palpluss.com/{id}?embed=1. The ?embed=1 query parameter tells the Palpluss page it is running inside an SDK modal.
- Body scroll lock —
document.body.style.overflow = 'hidden' while the modal is open, restored on cleanup.
- postMessage listener — listens for two event types from the iframe origin:
RESIZE — adjusts iframe.style.height for seamless auto-height.
PAYMENT_SUCCESS — extracts { txId, amount, phone }, calls onSuccess, removes the modal, resolves the Promise.
- Close handlers — the modal closes (and the Promise rejects) on:
- Clicking the ✕ button
- Clicking outside the card on the backdrop
- Pressing
Escape
- Cleanup — removes the overlay from the DOM, restores scroll, removes all event listeners.
Relation to @palpluss/sdk
| Package | Runtime | Who uses it | What it does |
|---|
@palpluss/sdk | Node.js | Backend developers | Server-side API calls — create paylinks, STK push, webhooks |
@palpluss/paylink | Browser | Frontend developers | Embed a payment link modal or inline iframe on any site |
Raw <iframe> | Any browser | All languages | Same as @palpluss/paylink but without npm — for PHP, Python, etc. |
These are completely independent. A typical full-stack integration uses @palpluss/sdk on the server to create the paylink, and then the paylink widget (or raw iframe) on the frontend to collect payment:
Backend (Node.js / PHP / Python / ...) Frontend (Browser)
───────────────────────────────────── ────────────────────────────────
@palpluss/sdk OR REST API @palpluss/paylink OR raw iframe
└─ createPaylink() ─────────────► └─ pay(id) / <iframe src="...">
└─ handleWebhook() ◄───────────── resolves on PAYMENT_SUCCESS