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

> ## Agent Instructions
> ## paymove integration rules for AI agents
>
> These rules are verified against the live API. Follow them exactly.
>
> 1. Amounts are INTEGERS in grosze (minor units). `1000` means 10.00 PLN. PLN is the only supported currency and there is no currency field in the API.
> 2. A float `price` (e.g. `12.99`) is SILENTLY TRUNCATED to 12 grosze and still returns HTTP 200. Always send an integer: `Math.round(pln * 100)`.
> 3. Authenticate with the `X-API-KEY` header. Never `Authorization: Bearer`. Sandbox keys start with `sk_test_`, production keys with `sk_live_`.
> 4. Server-side only. A call from a merchant page is rejected. Never put the key in frontend code.
> 5. Unknown request fields are silently ignored and still return HTTP 200 - a wrong body shape looks like success. Match the documented shape exactly.
> 6. A missing `externalId` returns HTTP 500 `Something went wrong`, not 400. A missing `price` or `details.returnUrl` returns HTTP 200 and a usable `redirectUrl` - no error at all. Validate the body yourself before sending it.
> 7. Always verify the `X-Paymove-Signature` header on incoming webhooks before trusting them: https://docs.paymove.io/en/webhook-signature.md
> 8. Errors are `{"status": <int>, "message": "<text>"}`. Branch on the HTTP status only - never on `message`, which is unstable and leaks internal class names.
> 9. There is no rate limiting, no HTTP 429, no HTTP 422, no `Idempotency-Key` header and no API versioning. Do not write code that handles them.
> 10. Re-POSTing an `externalId` that already exists returns HTTP 200 with the ORIGINAL `redirectUrl` and silently discards EVERY field you send - the new price, description and details are all ignored. Use `PATCH /api/pay/product/{productId}/subproduct/{externalId}` to change a price.
> 11. Webhooks fire only on `COMPLETED` by default. Registering a webhook requires both `retries` and `expectedCode` - omitting either returns HTTP 500, and `retries: 0` means no retry at all. Delivery counts as successful when the HTTP status equals `expectedCode` - the response body is never inspected.
> 12. Never fulfil an order on the `returnUrl` redirect. The checkout does not redirect there by itself: the customer has to click "back to shop", and inside the widget modal that redirect never happens. Fulfil only in the webhook handler, after verifying the signature.
> 13. Do not pin a version of `@paymove-io/sdk` - install the latest.
> 14. If the merchant has no `productId` yet, do not stop at a placeholder: ask for the API key, the `partnerId` and the shop name, propose defaults for `location` and `timezone` (`Europe/Warsaw`), offer the optional fields once, then create the product for them with `POST /api/product/pay` and take `productId` from `id` in the response. Do the same for the webhook: register it, then assign it to the product. Guided procedure: https://docs.paymove.io/skill.md (Steps 1-2).
> 15. Full documentation index: https://docs.paymove.io/llms.txt · Copy-paste quickstart: https://docs.paymove.io/en/quickstart.md · Agent skill: https://docs.paymove.io/skill.md

# Configuration

> Payment button data-* attributes: button text, bubble, language, on-screen position, several widgets on one page.

You configure the widget only with `data-*` attributes on the `<script>` tag. Each tag creates one independent widget.

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<script
  defer
  src="https://pay.sandbox.paymove.io/v1/sdk.js"
  data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
  data-widget-id="checkout"
  data-button-label="Pay online"
  data-bubble-title="Quick payment"
  data-bubble-description="BLIK, Google Pay or Apple Pay. No account needed."
  data-locale="en"
></script>
```

## Attributes

| Attribute | Description | Limit / default |
| - | - | - |
| `data-product-id` | **Required.** Product ID (UUID) or its public alias (`shortName`) | up to 256 characters |
| `data-widget-id` | Instance ID - needed for [custom buttons](/en/app-widget/javascript-api#custom-button) and for controlling the widget from code | up to 128 characters: letters, digits, `_`, `-`; generated automatically if omitted |
| `data-button-label` | Button text. Empty - the button is not shown | up to 120 characters; empty by default |
| `data-bubble-title` | Title of the bubble above the button | up to 160 characters; empty by default |
| `data-bubble-description` | Bubble description | up to 500 characters; empty by default |
| `data-locale` | Language of the payment page in the modal, e.g. `pl`, `en`, `pl-PL` | [automatic choice](#language) by default |
| `data-external-id` | Opens the widget straight on the payment for a specific document (subproduct). Takes the subproduct ID assigned by paymove or, with `rawExternalIdEnabled: true`, your own `externalId` | up to 256 characters |
| `data-document-entry` | Shows a screen where the customer enters the document number themselves. Works for a product without product-level pricing. Flag without a value | cannot be combined with `data-external-id` |
| `data-left` | Distance of the button from the left edge of the window | 0-10000 px |
| `data-right` | Distance from the right edge | 0-10000 px |
| `data-top` | Distance from the top edge | 0-10000 px |
| `data-bottom` | Distance from the bottom edge | 0-10000 px |

Rules common to all attributes:

* **The configuration is read once**, when the script starts. Changing attributes later does not change the widget's text, language or position. To do that, remove the widget (`destroy()`) and insert the script again.
* **Text is plain text**, never HTML - tags are displayed literally.
* A whitespace-only value counts as empty.
* An invalid value or an exceeded limit prevents **only that instance** from starting and leaves a message prefixed with `[PaymoveWidget]` in the browser console that points to the invalid attribute.

## Button and bubble

The button appears when `data-button-label` is not empty. The widget has no default text - you always provide the label, in your page's language.

<Frame caption="The button alone - data-button-label=&#x22;Pay online&#x22;">
  <img src="https://mintcdn.com/paymove/ztZW1d97Uc2P1tsz/images/app-widget/en/button.png?fit=max&auto=format&n=ztZW1d97Uc2P1tsz&q=85&s=41d2b63b37779f7b9da81ecdf324637c" alt="Dark widget button with the paymove mark, the label “Pay online” and an arrow" width="259" data-path="images/app-widget/en/button.png" />
</Frame>

<Frame caption="The bubble above the button - data-bubble-title and data-bubble-description">
  <img src="https://mintcdn.com/paymove/ztZW1d97Uc2P1tsz/images/app-widget/en/button-bubble.png?fit=max&auto=format&n=ztZW1d97Uc2P1tsz&q=85&s=7c05f2d6f1038caf51ed528d322a5a73" alt="The “Quick payment” bubble with the text “BLIK, Google Pay or Apple Pay. No account needed.” above the “Pay online” button" width="315" data-path="images/app-widget/en/button-bubble.png" />
</Frame>

The bubble appears when you set `data-bubble-title` **or** `data-bubble-description` (or both). It works like this:

* it shows when the pointer hovers over the button or when you reach it with `Tab`; it does not show on touch screens,
* it stays visible while you move the pointer from the button onto the bubble, and hides when the pointer and focus leave the widget,
* `Esc` hides it temporarily, and the close icon hides it for the lifetime of that widget instance (until the page reloads),
* opening the modal hides the bubble,
* it takes no space in the page layout; when there is not enough room above the button, it shows below it,
* the animation takes 200 ms and is disabled when the system has reduced motion turned on.

<Info>
  The bubble requires the widget's button. Without `data-button-label` the bubble is not rendered, even if you set a title and description.
</Info>

### Without the widget button

Omit `data-button-label` if the modal should be opened by your own element - a menu link, a banner or a button in the content. The widget then displays nothing until you open it:

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<script
  defer
  src="https://pay.sandbox.paymove.io/v1/sdk.js"
  data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
  data-widget-id="checkout"
></script>

<button type="button" data-paymove-open="checkout">Support us</button>
```

Details and the JavaScript version: [Custom button](/en/app-widget/javascript-api#custom-button).

## Position

Without position attributes the button is pinned to the **bottom-right corner** of the screen, respecting safe-area insets on devices with a screen notch.

The `data-left`, `data-right`, `data-top` and `data-bottom` attributes set the distance of the button from the edges of the browser window (`fixed` position). They accept a number or a `px` value: `24` and `24px` mean the same.

<CodeGroup>
  ```html Bottom-left corner theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
  <script
    defer
    src="https://pay.sandbox.paymove.io/v1/sdk.js"
    data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
    data-button-label="Pay online"
    data-left="24"
    data-bottom="24"
  ></script>
  ```

  ```html Higher, above a chat theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
  <script
    defer
    src="https://pay.sandbox.paymove.io/v1/sdk.js"
    data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
    data-button-label="Pay online"
    data-right="24"
    data-bottom="96"
  ></script>
  ```

  ```html Top-left corner theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
  <script
    defer
    src="https://pay.sandbox.paymove.io/v1/sdk.js"
    data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
    data-button-label="Pay online"
    data-left="24"
    data-top="96"
  ></script>
  ```
</CodeGroup>

* Each attribute works on its own. `data-left="24"` moves the button to the left edge and keeps the default distance from the bottom.
* Choose **one** edge per axis: `left` or `right`, `top` or `bottom`. Providing both opposite edges prevents the widget from starting.
* Position applies only to the button. The bubble adjusts to it by itself, and the modal is always centred.
* Pick distances so the button doesn't cover other floating elements - a chat, a cookie banner, a "back to top" button.

## Language

`data-locale` sets the language of the payment page in the modal. It accepts a language code (`pl`, `en`) or a code with a region (`pl-PL`, `en-US`).

* Polish and English are available for every product, and some products also support other languages, including German, Ukrainian, Czech and Slovak.
* When the given language is not available, the payment page uses the language the customer chose earlier, then the browser language.
* Without `data-locale` the language is chosen the same way.
* The customer can change the language during the flow.
* The texts you provide - `data-button-label` and the bubble - **are not translated**. Pass them in your page's language.
* Messages of the widget itself (loading, error, "Try again", "Close") take their language from `data-locale`, then from the `lang` attribute on `<html>`, then from the browser.

### Texts from an i18n library

A `t("…")` literal written in HTML is not evaluated. On a multilingual site, build the script tag in JavaScript and set the attributes **before** inserting it into the document:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
const script = document.createElement("script");
script.src = "https://pay.sandbox.paymove.io/v1/sdk.js";
script.setAttribute("data-product-id", productId);
script.setAttribute("data-widget-id", "checkout");
script.setAttribute("data-locale", locale);
script.setAttribute("data-button-label", t("widget.button"));
script.setAttribute("data-bubble-title", t("widget.title"));
script.setAttribute("data-bubble-description", t("widget.description"));
document.head.append(script);
```

After the page language changes, remove the widget (`destroy()`) and insert the script again with the new texts.

## Several widgets on one page

Each `<script>` tag creates a separate widget with its own button. You can combine different products, also from different [script URLs](/en/app-widget#script-urls) - e.g. tickets and parking on an event page:

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<script
  defer
  src="https://go.sandbox.paymove.io/v1/sdk.js"
  data-product-id="EVENT_PRODUCT_ID"
  data-widget-id="tickets"
  data-button-label="Buy tickets"
  data-right="24"
  data-bottom="24"
></script>
<script
  defer
  src="https://park.sandbox.paymove.io/v1/sdk.js"
  data-product-id="PARKING_ID"
  data-widget-id="parking"
  data-button-label="Pay for parking"
  data-right="24"
  data-bottom="88"
></script>
```

* Give each widget a **different** `data-widget-id`. Another script with the same ID is ignored.
* Widgets don't lay themselves out - without position attributes all buttons land in the same corner and overlap. Space them out with the `data-*` attributes.
* Only **one modal** can be open on a page. Opening a second widget closes the first.
* A configuration error in one widget does not block the others.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.