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

# REST API

> Create a gateway payment over plain HTTP — full request, headers and response.

The REST API lets you create payments within your main product. Each call creates a payment the customer can settle through the returned `redirectUrl`.

<Warning>
  Before integrating via the REST API, make sure you have configured your product and webhook. Go to [Configuration](/en/webhooks) to complete the required steps.
</Warning>

<Warning>
  Call the API server-side only. A request sent from your shop's page is rejected - and the key must never reach frontend code anyway.
</Warning>

## 1. Endpoint

```
POST https://gateway-api.sandbox.paymove.io/api/pay/product/{productId}/subproduct/pricing
```

| Environment | Base URL                                 |
| ----------- | ---------------------------------------- |
| Sandbox     | `https://gateway-api.sandbox.paymove.io` |
| Production  | `https://api.paymove.io`                 |

### Headers

| Header         | Value                                                            |
| -------------- | ---------------------------------------------------------------- |
| `Content-Type` | `application/json`                                               |
| `X-API-KEY`    | Your API key - `sk_test_…` in sandbox, `sk_live_…` in production |

## 2. Example call

```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",
      "email": "customer@example.com",
      "locale": "pl-PL",
      "triggerPayment": "BLIK",
      "qrStepEnabled": true
    }
  }'
```

The same call in Node.js:

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
const url = `https://gateway-api.sandbox.paymove.io/api/pay/product/${productId}/subproduct/pricing`;

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-KEY": process.env.PAYMOVE_API_KEY,
  },
  body: JSON.stringify({
    price: Math.round(10.0 * 100), // always an integer in grosze
    externalId: "order-123",
    details: {
      returnUrl: "https://merchant-shop.com/payment/success",
      productName: "Sports T-shirt",
      email: "customer@example.com",
      locale: "pl-PL",
      triggerPayment: "BLIK",
      qrStepEnabled: true,
    },
  }),
});

if (!response.ok) {
  const error = await response.json(); // { status, message }
  throw new Error(`Paymove ${response.status}: ${error.message}`);
}

const data = await response.json();
console.log(data.redirectUrl);
```

### Body parameters

| Field                    | Type      | Required | Description                                                                                                                                                                                                                                                                                             |
| ------------------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `price`                  | `integer` | Yes      | Amount in grosze as an **integer** (`1000` = 10.00 PLN)                                                                                                                                                                                                                                                 |
| `externalId`             | `string`  | Yes      | Unique order identifier on your side                                                                                                                                                                                                                                                                    |
| `details.returnUrl`      | `string`  | Yes      | Customer return URL after payment                                                                                                                                                                                                                                                                       |
| `details.productName`    | `string`  | No       | Product name shown at checkout                                                                                                                                                                                                                                                                          |
| `details.email`          | `string`  | No       | Customer email, prefilled at checkout                                                                                                                                                                                                                                                                   |
| `details.locale`         | `string`  | No       | Overrides the locale from the main product (e.g. `pl-PL`)                                                                                                                                                                                                                                               |
| `details.recipient`      | `string`  | No       | Recipient name displayed at checkout                                                                                                                                                                                                                                                                    |
| `details.triggerPayment` | `enum`    | No       | Payment method preselected in the checkout (Google Pay and Apple Pay also start automatically)                                                                                                                                                                                                          |
| `details.qrStepEnabled`  | `boolean` | No       | QR code on desktop -  the customer finishes the payment on their phone                                                                                                                                                                                                                                  |
| `details.autoclose`      | `integer` | No       | Milliseconds after a successful payment before the checkout returns to `details.returnUrl` on its own. The countdown is shown inside the "Back to store" button and any interaction from the customer cancels it. In the widget the modal closes instead of redirecting. No field = no automatic return |

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

<Info>
  The gateway settles in **PLN** only - there is no currency field in the request. A `currency` field, if you send one, is ignored.
</Info>

<Info>
  `details` is a free-form object - you may pass your own fields and Paymove will store them. The checkout, however, reads only: `returnUrl`, `redirectUrl`, `productName`, `email`, `locale`, `orderId`, `recipient`, `triggerPayment`, `qrStepEnabled` and `autoclose`. Any other field (e.g. `customerId`) is passed through and never displayed.
</Info>

<Warning>
  Unknown fields in the body are **silently ignored** and the API returns `200`. Sending `amount` instead of `price`, or `returnUrl` at the top level instead of inside `details`, raises no error - the payment is created with incomplete data. Match the structure above exactly.
</Warning>

### Response

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

| Field         | Type     | Description                                                        |
| ------------- | -------- | ------------------------------------------------------------------ |
| `redirectUrl` | `string` | Checkout URL -  redirect the customer here to complete the payment |

The response status is HTTP `200` (not `201`).

<Info>
  The `externalId` query parameter in the returned URL (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](/en/payment-status).
</Info>

### Errors

Every error has the shape `{"status": <int>, "message": "<text>"}`. Branch your logic **on the HTTP status**, never on the `message` text.

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "status": 401,
  "message": "Invalid API key"
}
```

Full list: [Error codes](/en/errors).

### Preselecting a payment method

The `details.triggerPayment` field picks a payment method up front, so the customer does not have to find it in the list.

| Value         | Method        |
| ------------- | ------------- |
| `GPAY`        | Google Pay    |
| `APAY`        | Apple Pay     |
| `BLIK`        | BLIK          |
| `CARD`        | Card payment  |
| `TRANSFER`    | Bank transfer |
| `PAY_BY_LINK` | Pay by Link   |
| `PAYPO`       | PayPo         |

An unknown value is ignored -  the checkout then behaves normally.

<Warning>
  Auto-start - opening the payment as if the customer had clicked **Pay** - applies to **Google Pay and Apple Pay only**. For the other five methods the tile is merely preselected and the customer clicks **Pay** themselves. Do not design a flow that assumes BLIK or PayPo will start on their own.
</Warning>

<Info>
  Even for the wallets, auto-start requires the checkout to know the customer's email -  pass it in `details.email`. It also does not run inside the [embedded widget modal](/en/sdk/widget), or when the method is not available for your product. In each of those cases the method is only preselected.
</Info>

Auto-start applies only to the first display of the checkout. Once the customer picks a payment method themselves, it does not fire again -  until the page is reloaded.

<Warning>
  Google Pay and Apple Pay open a native browser sheet that normally requires a user gesture, so the automatic start may be blocked by the browser. The customer then sees the standard payment screen and pays manually.
</Warning>

### QR code step

The `details.qrStepEnabled` field turns on an extra opening screen on desktop: instead of the payment form the customer sees a QR code, scans it with their phone and finishes the payment there. Useful for BLIK and for wallets that only exist on mobile.

The step is opt-in -  omit the field or pass `false` and the checkout goes straight to the payment form.

<Info>
  The QR code appears on desktop only. On mobile, and whenever `details.triggerPayment` is set, the checkout skips the step regardless of this field.
</Info>

## 3. Redirect the customer

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

Once the payment completes, the checkout shows a confirmation screen with a "back to shop" button. Only a click on that button takes the customer to the address given in `details.returnUrl` - no query parameters are appended to it.

<Warning>
  Returning to `returnUrl` **is not proof of payment** - and it may never happen at all. A customer who closes the tab after paying never reaches your address, and `returnUrl` is an ordinary public URL that anyone can open without paying. Fulfil the order only after receiving and [verifying the webhook](/en/webhook-signature).
</Warning>

## 4. Reusing an `externalId`

`externalId` is the payment key on the Paymove side. Sending another request with the same `externalId` **creates no new payment and returns no error** - the API responds `200` with the `redirectUrl` of the payment created earlier.

<Warning>
  On a repeat call the **new `price` and other fields are silently discarded**. If you retry with a corrected amount, the original amount stands and nothing in the response signals it. Use a fresh, unique `externalId` for every order.
</Warning>

To change the amount of an existing payment, use a separate call:

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl --request PATCH \
  --url https://gateway-api.sandbox.paymove.io/api/pay/product/891412c8-8717-4449-9543-e34112bec470/subproduct/order-123 \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  --data '{ "price": 1500 }'
```

The call updates `price` only - all other fields are ignored.

## 5. What's next

<CardGroup cols={2}>
  <Card title="Webhook signature verification" icon="shield-check" href="/en/webhook-signature">
    A mandatory step before fulfilling an order.
  </Card>

  <Card title="Payment statuses" icon="list-check" href="/en/payment-status">
    Statuses, webhook payload and checking a payment's state.
  </Card>

  <Card title="Error codes" icon="triangle-exclamation" href="/en/errors">
    The complete list of API errors and how to handle them.
  </Card>

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