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

# Controlling the widget

> window.PaymoveWidget: custom buttons, opening and closing the modal from code and the paymove:completed event.

The script exposes the `window.PaymoveWidget` object. It is deliberately small - configuration stays in the `data-*` attributes, and from code you only open, close and clean up widgets.

| Method | What it does |
| - | - |
| `PaymoveWidget.ready()` | Returns a `Promise` that resolves once all inserted widgets have finished initialising |
| `PaymoveWidget.get(id)` | Returns the widget with the given `data-widget-id`, or `undefined` |
| `widget.open()` | Opens the modal |
| `widget.close()` | Closes the modal |
| `widget.destroy()` | Removes the widget: button, modal, iframe and all listeners |

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
await window.PaymoveWidget.ready();

const widget = window.PaymoveWidget.get("checkout");
widget?.open();
widget?.close();
widget?.destroy();
```

* `ready()` waits for widgets that are starting right now. A widget with invalid configuration does not block the others - `ready()` resolves anyway, and `get()` returns `undefined` for it.
* There is no `update()` method. You can't change the configuration after start - remove the widget and insert the script again.
* The script uses the `defer` (or `async`) attribute, so `window.PaymoveWidget` may not exist yet when your code runs. Access it only in the script's `onload` or after `DOMContentLoaded`.

## Custom button

Any element with the `data-paymove-open="<widget-id>"` attribute opens the modal of the given widget - no JavaScript needed:

```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"
></script>

<a href="/tickets" data-paymove-open="tickets">Buy tickets</a>
<button type="button" data-paymove-open="tickets">Buy tickets</button>
```

* A click on a link with `data-paymove-open` opens the modal instead of navigating to `href`. Put a fallback page in `href` - it will work if the script fails to load.
* A click with `Ctrl`, `Cmd`, `Shift` or `Alt` held is ignored, so "open in new tab" works as usual.
* Elements with the `disabled` attribute or `aria-disabled="true"` are ignored.
* The attribute also works on elements inside Shadow DOM and on elements added to the page later.

A button that is visible before the script loads won't react to clicks. If that matters, enable it only after `ready()`:

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<button type="button" data-paymove-open="tickets" disabled id="buy">Buy tickets</button>

<script>
  window.addEventListener("DOMContentLoaded", async () => {
    await window.PaymoveWidget?.ready();
    if (window.PaymoveWidget?.get("tickets")) {
      document.getElementById("buy").disabled = false;
    }
  });
</script>
```

The same effect from JavaScript:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
document.querySelector("#buy").addEventListener("click", () => {
  window.PaymoveWidget?.get("tickets")?.open();
});
```

## The `paymove:completed` event

After a successful payment the widget dispatches the `paymove:completed` event on `document`:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
document.addEventListener("paymove:completed", (event) => {
  const { widgetId, productId, orderId } = event.detail;
  // e.g. refresh the invoice list or show a thank-you message
});
```

| Field | Description |
| - | - |
| `widgetId` | `data-widget-id` of the widget in which the payment was made |
| `productId` | `data-product-id` of that widget |
| `orderId` | paymove payment ID - the same one the customer sees on the confirmation screen |

* The event arrives only once the payment page has confirmed the payment with the paymove API and displayed the confirmation screen.
* It arrives **once** per payment.
* After the payment the modal **stays open** so the customer can see the confirmation. They close it with the "End" button, the close icon, a click on the backdrop or the `Esc` key.

<Warning>
  `paymove:completed` is an event in the customer's browser - it can be dispatched manually from the console. Use it to refresh the UI, never as proof of payment. Fulfil and book orders based on the [webhook](/en/products/docpay/rest-api#6-webhooks).
</Warning>


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