> ## Documentation Index
> Fetch the complete documentation index at: https://docs.palpluss.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Paylink Widget

> Embed a Palpluss payment link into any website — as a modal overlay or an inline iframe — with two lines of code.

# 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.

<Note>
  **`@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.
</Note>

***

## 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](https://console.palpluss.com) 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:

```ts theme={null}
// ✅ 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')
```

<Tip>
  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.
</Tip>

***

## 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
```

1. `pay()` creates a modal overlay and injects an `<iframe>` pointing to `https://link.palpluss.com/{id}?embed=1`.
2. The embedded page communicates back via `window.postMessage`.
3. 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)

<Note>
  If you use PHP, Python, Ruby, or any other server-side language, skip this section and jump directly to [Any Backend — Raw iframe](#any-backend--raw-iframe). The npm package is only needed for JavaScript/TypeScript projects.
</Note>

<CodeGroup>
  ```bash npm theme={null}
  npm install @palpluss/paylink
  ```

  ```bash pnpm theme={null}
  pnpm add @palpluss/paylink
  ```

  ```bash yarn theme={null}
  yarn add @palpluss/paylink
  ```
</CodeGroup>

React peer dependencies are only needed if you import `@palpluss/paylink/react`:

```bash theme={null}
npm install react react-dom
```

***

## Quick Start

```ts theme={null}
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               |

```ts theme={null}
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.

```ts theme={null}
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.

```tsx theme={null}
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)

<Tabs>
  <Tab title="Vanilla JS / TypeScript">
    ```ts theme={null}
    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
      }
    })
    ```
  </Tab>

  <Tab title="React — modal">
    ```tsx theme={null}
    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>
    }
    ```
  </Tab>

  <Tab title="React — inline embed">
    Use `@palpluss/paylink/react` when you want the payment form embedded directly in your page layout instead of a floating modal.

    ```tsx theme={null}
    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>
      )
    }
    ```
  </Tab>

  <Tab title="CDN script tag">
    No build step required. Drop this into any HTML page:

    ```html theme={null}
    <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:

    ```js theme={null}
    Palpluss.pay('abc-123')
      .then(result => console.log(result))
      .catch(() => console.log('cancelled'))
    ```
  </Tab>
</Tabs>

***

## Any Backend — Raw Iframe

<Info>
  **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.
</Info>

### 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.

```html theme={null}
<!-- 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>
```

<Note>
  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.
</Note>

***

### Framework examples

<Tabs>
  <Tab title="PHP / Laravel Blade">
    In your Blade template, output the paylink ID from your controller or config. Everything else is plain HTML:

    ```php theme={null}
    {{-- 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:

    ```php theme={null}
    // app/Http/Controllers/CheckoutController.php
    public function show()
    {
        return view('checkout', [
            'paylinkId' => config('services.palpluss.paylink_id'),
        ]);
    }
    ```
  </Tab>

  <Tab title="Python / Django">
    Render the paylink ID from your Django view into the template:

    ```python theme={null}
    # views.py
    from django.shortcuts import render

    def checkout(request):
        return render(request, 'checkout.html', {
            'paylink_id': 'abc-123',   # or load from settings / DB
        })
    ```

    ```html theme={null}
    {# 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>
    ```
  </Tab>

  <Tab title="Python / Flask">
    ```python theme={null}
    # 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'))
    ```

    ```html theme={null}
    {# 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>
    ```
  </Tab>

  <Tab title="Static HTML / No framework">
    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:

    ```html theme={null}
    <!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>
    ```
  </Tab>
</Tabs>

***

### postMessage event reference

Your `message` event listener receives `event.data` with the following shapes:

```js theme={null}
// 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
}
```

<Warning>
  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.
</Warning>

***

### Security note on postMessage

The iframe is served from `https://link.palpluss.com`. For production applications, validate the message origin before trusting the payload:

```js theme={null}
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

```ts theme={null}
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:

1. **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.
2. **Card** — a centered white card (`max-width: 860px`) holds the close button and the iframe.
3. **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.
4. **Body scroll lock** — `document.body.style.overflow = 'hidden'` while the modal is open, restored on cleanup.
5. **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.
6. **Close handlers** — the modal closes (and the Promise rejects) on:
   * Clicking the ✕ button
   * Clicking outside the card on the backdrop
   * Pressing `Escape`
7. **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
```
