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

# Payment methods SDK

> Integrate the TapiPay payment widget into any site or app, with events and automatic height.

The **embed SDK** mounts the TapiPay payment widget inside your site. Unlike a [manual iframe](/en/payment-portal/embed-portal), it resizes itself, shows a skeleton while loading, and tells you through events what the user is doing.

It is the same SDK used by [autopay management](/en/payment-portal/autopay-sdk): a single option (`view`) decides which screen you mount.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @npmtapi/embed
  ```

  ```bash pnpm theme={null}
  pnpm add @npmtapi/embed
  ```

  ```bash yarn theme={null}
  yarn add @npmtapi/embed
  ```
</CodeGroup>

<Note>
  TypeScript types ship with the package. You do not need to install anything else.
</Note>

### Entrypoints

| Import                 | What it exposes                        |
| ---------------------- | -------------------------------------- |
| `@npmtapi/embed`       | `loadTapipay()` and all types          |
| `@npmtapi/embed/react` | `<TapipayWidget />` and `useTapipay()` |

`<TapipayWidget />` is the recommended path in React: it loads the SDK, mounts the widget and cleans it up on unmount. `useTapipay()` is the low level hook, for when you need to control `initialize()` and `destroy()` yourself.

If you are not using a bundler, you do not need to install anything: load the script and work with `window.tapipay`.

## Identify your company

Pass **one** of these two options, not both:

<ResponseField name="organization" type="string">
  Your company's `slug` or alias (for example, `acme-corp`). It matches the **Alias** field under **Settings → Organization** in the dashboard. In data-attrs: `data-organization`.
</ResponseField>

<ResponseField name="companyCode" type="string">
  The company code configured for your organization in TapiPay (for example, `MX-S-12345`). The format varies between integrations: if you do not know yours, confirm it with your TapiPay contact. In data-attrs: `data-company-code`.
</ResponseField>

<Tip>
  If you manage **many companies or sub-billers**, use `companyCode`: you do not need to know or maintain each company's `slug`. If you pass both, `companyCode` wins.
</Tip>

Every example on this page works the same if you replace `organization: "acme-corp"` with `companyCode: "MX-S-12345"`.

## Quick start

<Tabs>
  <Tab title="Script tag">
    No bundler. The script makes `window.tapipay` available once loaded.

    ```html theme={null}
    <div id="widget"></div>
    <script src="https://app.tapipay.la/embed.js"></script>
    <script>
      tapipay.initialize({
        container: "#widget",
        organization: "acme-corp",
        identifier: "CLI-00042"
      });

      tapipay.on("paymentSubmitted", function (data) {
        console.log("Payment submitted:", data);
      });
    </script>
    ```
  </Tab>

  <Tab title="Data-attrs">
    Zero JavaScript. The SDK looks for containers with `data-tapipay-widget` and mounts them once the DOM is ready.

    ```html theme={null}
    <div
      data-tapipay-widget
      data-organization="acme-corp"
      data-identifier="CLI-00042"
    ></div>
    <script src="https://app.tapipay.la/embed.js"></script>
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={null}
    import { TapipayWidget } from "@npmtapi/embed/react";

    export function PaymentPage({ clientId }: { clientId: string }) {
      return (
        <TapipayWidget
          organization="acme-corp"
          identifier={clientId}
          onPaymentSubmitted={(data) => console.log("Payment submitted:", data)}
          onCancelled={() => console.log("Payment cancelled by the user")}
        />
      );
    }
    ```
  </Tab>
</Tabs>

## Options

### `initialize(options)`

| Option              | Type                      | Required | Default      | Description                                                                                             |
| ------------------- | ------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------- |
| `container`         | `string \| HTMLElement`   | Yes      |              | CSS selector or DOM node reference where the widget mounts.                                             |
| `organization`      | `string`                  | Yes\*    |              | Your company `slug`. \*Required unless you pass `companyCode`.                                          |
| `companyCode`       | `string`                  | Yes\*    |              | Company code. \*Alternative to `organization`; if you pass both, this one wins.                         |
| `identifier`        | `string`                  | Yes      |              | End user identifier (the same one you send when creating the debt).                                     |
| `externalRequestId` | `string`                  | No       |              | Debt to preselect. Takes precedence over `selectionStrategy`.                                           |
| `selectionStrategy` | `"oldestCreated"`         | No       | most recent  | How to pick the pending debt when there are several. Omitted: the most recent one.                      |
| `view`              | `"payments" \| "autopay"` | No       | `"payments"` | Which screen to mount. With `"autopay"` it mounts [autopay management](/en/payment-portal/autopay-sdk). |

If `container`, `identifier` or both company identifiers are missing, the SDK mounts nothing and warns in the console.

### `destroy(container)`

Unmounts the widget: removes the iframe, the skeleton and the internal listeners.

```javascript theme={null}
tapipay.destroy("#widget");
```

Calling it matters in a SPA, when unmounting the component that holds the widget. `<TapipayWidget />` already does it for you. If you call `initialize()` again on a container that already has a widget, the SDK destroys it and mounts it again.

### `loadTapipay(options)`

| Option        | Type                     | Required | Default        | Description                    |
| ------------- | ------------------------ | -------- | -------------- | ------------------------------ |
| `environment` | `"production" \| "homo"` | No       | `"production"` | Which SDK environment to load. |

It is idempotent per environment: calling it several times returns the same promise and never injects the script twice. It is compatible with React Strict Mode.

## Events

```javascript theme={null}
// The payment popup opened
tapipay.on("paymentWindowOpened", function () {
  console.log("Payment window opened");
});

// The user completed the form and came back to the callback
tapipay.on("paymentSubmitted", function (data) {
  console.log("Payment submitted:", data);
});

// The user closed the popup without paying
tapipay.on("paymentCancelled", function () {
  console.log("Payment cancelled by the user");
});

// Remove a listener
tapipay.off("paymentSubmitted", handler);
```

<Warning>
  `paymentSubmitted` means **submitted, not confirmed**. Its `status` is always `"pending"`: the user completed the form, but the payment is not settled yet. Never mark a debt as paid based on this event. The real status arrives through the [payment notification](/en/webhooks/payment-notification) to your backend.
</Warning>

### Payloads

```typescript theme={null}
interface TapipayPaymentWindowOpenedEvent {
  type: "paymentWindowOpened";
}

interface TapipayPaymentSubmittedEvent {
  type: string;                    // "TAPIPAY_CALLBACK"
  status: "pending";               // always pending
  data: Record<string, string>;    // callback params (may come back empty)
}

type TapipayPaymentCancelledEvent = Record<string, never>;  // no fields
```

### In React

Events are passed as props. Watch out for the third name: the event is called `paymentCancelled`, but the prop is `onCancelled`.

| Prop                    | Matching event        |
| ----------------------- | --------------------- |
| `onPaymentWindowOpened` | `paymentWindowOpened` |
| `onPaymentSubmitted`    | `paymentSubmitted`    |
| `onCancelled`           | `paymentCancelled`    |

`<TapipayWidget />` also accepts every `initialize()` option (except `container`), plus `environment`, `className` and `style` for the container.

## Other frameworks

<Tabs>
  <Tab title="Next.js">
    ```tsx theme={null}
    "use client";
    import { TapipayWidget } from "@npmtapi/embed/react";
    import { useRouter } from "next/navigation";

    export default function PayPage() {
      const router = useRouter();
      return (
        <TapipayWidget
          organization="acme-corp"
          identifier="CLI-00042"
          onPaymentSubmitted={() => router.push("/processing")}
        />
      );
    }
    ```

    The widget is a client component: it needs `"use client"`.
  </Tab>

  <Tab title="Vue 3">
    ```vue theme={null}
    <template>
      <div ref="widgetRef"></div>
    </template>

    <script setup>
    import { loadTapipay } from "@npmtapi/embed";
    import { onMounted, onUnmounted, ref } from "vue";

    const props = defineProps({ organization: String, identifier: String });
    const widgetRef = ref(null);
    let tapipay = null;

    onMounted(async () => {
      tapipay = await loadTapipay();
      tapipay.initialize({
        container: widgetRef.value,
        organization: props.organization,
        identifier: props.identifier
      });
      tapipay.on("paymentSubmitted", (data) => console.log("Payment submitted:", data));
    });

    onUnmounted(() => tapipay?.destroy(widgetRef.value));
    </script>
    ```
  </Tab>

  <Tab title="Angular">
    ```typescript theme={null}
    import { loadTapipay, TapipayInstance } from "@npmtapi/embed";
    import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from "@angular/core";

    @Component({
      selector: "app-tapipay-widget",
      template: `<div #container></div>`
    })
    export class TapipayWidgetComponent implements OnInit, OnDestroy {
      @Input() organization!: string;
      @Input() identifier!: string;
      @ViewChild("container", { static: true }) containerRef!: ElementRef;

      private tapipay: TapipayInstance | null = null;

      async ngOnInit() {
        this.tapipay = await loadTapipay();
        this.tapipay.initialize({
          container: this.containerRef.nativeElement,
          organization: this.organization,
          identifier: this.identifier
        });
      }

      ngOnDestroy() {
        this.tapipay?.destroy(this.containerRef.nativeElement);
      }
    }
    ```
  </Tab>
</Tabs>

## Staging environment

<Tabs>
  <Tab title="npm">
    ```typescript theme={null}
    import { loadTapipay } from "@npmtapi/embed";

    const tapipay = await loadTapipay({ environment: "homo" });
    tapipay.initialize({
      container: "#widget",
      organization: "acme-corp",
      identifier: "CLI-00042"
    });
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={null}
    <TapipayWidget
      organization="acme-corp"
      identifier="CLI-00042"
      environment="homo"
    />
    ```
  </Tab>

  <Tab title="Script tag">
    Point `src` at the staging host.

    ```html theme={null}
    <script src="https://homo.tapipay.la/embed.js"></script>
    ```
  </Tab>
</Tabs>

| Environment  | Script                             | Widget origin             |
| ------------ | ---------------------------------- | ------------------------- |
| `production` | `https://app.tapipay.la/embed.js`  | `https://app.tapipay.la`  |
| `homo`       | `https://homo.tapipay.la/embed.js` | `https://homo.tapipay.la` |

## What the SDK mounts

Knowing which URL the SDK builds helps you debug: if the widget stays blank, open that URL directly in the browser and you will see the real error.

| View                        | Iframe URL                                     |
| --------------------------- | ---------------------------------------------- |
| Payments, by `organization` | `{origin}/s/{organization}/embed/{identifier}` |
| Payments, by `companyCode`  | `{origin}/c/{companyCode}/embed/{identifier}`  |
| Autopay                     | the same, with `/autopay` appended             |

`externalRequestId` and `selectionStrategy` are added as query params. For example, `initialize()` with `organization: "acme-corp"`, `identifier: "CLI-00042"` and `externalRequestId: "INV-2026-001"` mounts:

```text theme={null}
https://app.tapipay.la/s/acme-corp/embed/CLI-00042?externalRequestId=INV-2026-001
```
