> ## 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, and `retries` defaults to `0` (no retries) unless you set it explicitly. 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. 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

# Payment Gateway: basic integration

> From zero to a working checkout: create a store, generate a payment and redirect the customer to the Paymove gateway.

The basic Paymove payment gateway integration has three steps: **once**, you create a product (representing your shop) and configure a webhook; then **for each order** you create a payment and redirect the customer to the returned `redirectUrl`.

All requests are authorized with an API key sent in the `X-API-KEY` header.

<Info>
  Examples use the sandbox environment: `https://gateway-api.sandbox.paymove.io`. You will receive your API key and `partnerId` from Paymove.
</Info>

## Step 1: Create product (shop)

The product represents your shop in the Paymove system - all payments are created within it. You create it **only once**, when starting the integration.

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl --request POST \
  --url https://gateway-api.sandbox.paymove.io/api/product/pay \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  --data '{
    "partnerId": "78562c79-2f5c-4415-8af4-c871eea92ef2",
    "productType": "PAY",
    "name": "Test Shop",
    "shortName": "SHOP1",
    "location": "Warsaw",
    "timezone": "Europe/Warsaw",
    "productMetadata": { "locale": "pl-PL" }
  }'
```

**Response (200):**

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "id": "891412c8-8717-4449-9543-e34112bec470",
  "name": "Test Shop",
  "location": "Warsaw",
  "partner": {
    "id": "78562c79-2f5c-4415-8af4-c871eea92ef2",
    "name": "Partner Name Sp. z o.o.",
    "productTypes": ["PAY"]
  },
  "timezone": "Europe/Warsaw",
  "shortName": "SHOP1",
  "emailEnabled": true,
  "smsEnabled": false,
  "fee": {
    "id": "01f9754e-78e3-4b2c-8186-d6862598a95a",
    "minimum": 30,
    "amount": 5,
    "fixed": false
  },
  "productType": "PAY",
  "status": "ACTIVE",
  "creator": "PAYMOVE",
  "reviewEnabled": false,
  "createdAt": 1783246791.745352526,
  "updatedAt": 1783246791.745352526
}
```

The `id` field in the response is the **`productId`** - your shop identifier used in the next steps.

## Step 2: Configure webhook

The webhook notifies your system about a completed payment. First register it, then assign it to the product. In `requestTemplate` you can use the variables `{{externalId}}` (your orderId) and `{{price}}` (amount) - Paymove substitutes them when sending.

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl --request POST \
  --url https://gateway-api.sandbox.paymove.io/api/pay/plugin/webhook \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  --data '{
    "name": "OrderPaymentHook",
    "endpoint": "https://merchant-shop.com/api/payments/webhook",
    "method": "POST",
    "requestTemplate": {
      "orderId": "{{externalId}}",
      "price": "{{price}}"
    },
    "responseTemplate": { "status": "ok" },
    "expectedCode": 200,
    "expectedResponse": "{ \"status\": \"ok\" }",
    "retries": 3,
    "partnerId": "78562c79-2f5c-4415-8af4-c871eea92ef2",
    "type": "PAYMENT",
    "headers": { "Content-Type": ["application/json"] }
  }'
```

**Response (200):** the webhook object with an assigned `id` (`webhookId`) and a `signingSecret` field:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "id": "6b23ecd9-14c8-47fc-add0-b71ec50e9d66",
  "name": "OrderPaymentHook",
  "endpoint": "https://merchant-shop.com/api/payments/webhook",
  "method": "POST",
  "requestTemplate": {
    "orderId": "{{externalId}}",
    "price": "{{price}}"
  },
  "responseTemplate": { "status": "ok" },
  "expectedCode": 200,
  "expectedResponse": "{ \"status\": \"ok\" }",
  "retries": 3,
  "type": "PAYMENT",
  "headers": { "Content-Type": ["application/json"] },
  "signingSecret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

<Warning>
  The `signingSecret` is used to verify the signature of requests coming from Paymove. Store it securely in your system.
</Warning>

Then assign the webhook to the product (replace `webhookId` and `productId`):

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl --request POST \
  --url https://gateway-api.sandbox.paymove.io/api/pay/plugin/webhook/6b23ecd9-14c8-47fc-add0-b71ec50e9d66/products/891412c8-8717-4449-9543-e34112bec470 \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
```

**Response (200):** the webhook object confirming the link.

## Step 3: Create payment

For each order, create a payment within the product. Replace `productId` in the URL with the one from step 1.

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl --request POST \
  --url https://gateway-api.sandbox.paymove.io/api/pay/product/891412c8-8717-4449-9543-e34112bec470/subproduct/pricing \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  --data '{
    "price": 1000,
    "externalId": "order-123",
    "details": {
      "returnUrl": "https://merchant-shop.com/payment/success",
      "productName": "Sports T-shirt",
      "customerId": "user-567",
      "email": "customer@example.com",
      "locale": "pl-PL",
      "triggerPayment": "BLIK"
    }
  }'
```

| Field                    | Required | Description                                                                                                                                                                                                                                       |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `price`                  | Yes      | Amount in grosze as an **integer** (e.g. `1000` = 10.00 PLN). The currency is always PLN.                                                                                                                                                         |
| `externalId`             | Yes      | Unique order identifier in your system.                                                                                                                                                                                                           |
| `details.returnUrl`      | Yes      | Customer return URL after payment.                                                                                                                                                                                                                |
| `details.productName`    | No       | Product name shown at checkout.                                                                                                                                                                                                                   |
| `details.customerId`     | No       | Free-form passthrough value - the checkout does not display it.                                                                                                                                                                                   |
| `details.email`          | No       | Customer email, prefilled at checkout.                                                                                                                                                                                                            |
| `details.locale`         | No       | Overrides the main product locale (e.g. `pl-PL`).                                                                                                                                                                                                 |
| `details.triggerPayment` | No       | Method preselected in the checkout -  `BLIK`, `GPAY`, `APAY`, `TRANSFER`, `PAY_BY_LINK` or `PAYPO`. Only `GPAY` and `APAY` start automatically (and only with `details.email`); the rest are preselected only. Details: [REST API](/en/rest-api). |
| `details.qrStepEnabled`  | No       | `true` makes the checkout open on a QR code on desktop, so the customer finishes the payment on their phone. Details: [REST API](/en/rest-api).                                                                                                   |
| `details.autoclose`      | No       | Milliseconds after a successful payment before the checkout returns to `returnUrl` on its own, counting down inside the "Back to store" button. Details: [REST API](/en/rest-api).                                                                |

<Warning>
  `price` must be an integer. A decimal value (e.g. `12.99`) is **silently truncated to 12 grosze** and the API still returns `200`. Convert złoty amounts with `Math.round(amount * 100)`.
</Warning>

**Response (200):**

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "redirectUrl": "https://checkout.sandbox.paymove.io/891412c8-8717-4449-9543-e34112bec470?externalId=ec6RtwTZKb"
}
```

<Info>
  The `externalId` query parameter inside `redirectUrl` (here `ec6RtwTZKb`) is a **10-character payment hash generated by Paymove**, not your own `externalId` (`order-123`). Store it - it is the value you use when checking the payment status.
</Info>

## Step 4: Redirect the customer

Redirect the customer to the returned `redirectUrl`:

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
window.location.href = data.redirectUrl;
```

The customer sees the Paymove checkout page with the amount, your shop and product name, and payment methods (BLIK, Google Pay, Apple Pay, bank transfer). After the payment completes:

1. Paymove sends a webhook to your `endpoint` - by default only for the `COMPLETED` status.
2. Your system **verifies the `X-Paymove-Signature` header** ([how to do it](/en/webhook-signature)), responds with `200` and `{ "status": "ok" }`, then fulfills the order.
3. The customer sees the confirmation screen and can head back to the shop at `returnUrl`.

<Warning>
  Never fulfil an order in the `returnUrl` handler. The redirect is driven by the customer's browser and can be triggered without paying. The only trustworthy confirmation is a verified webhook.
</Warning>

## What's next?

<CardGroup cols={2}>
  <Card title="Payment Gateway: webhooks" icon="webhook" href="/en/products/payment-gateway/tutorial-webhook">
    Extend your integration with webhooks and react to every paid transaction.
  </Card>

  <Card title="JavaScript SDK" icon="js" href="/en/sdk/javascript">
    Ready-made client for Node.js integration.
  </Card>
</CardGroup>
