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

> Configure a payment gateway webhook and learn about every paid transaction before the customer returns to your store.

A webhook is how Paymove actively notifies your system about a completed payment - no API polling required. The setup takes two steps: **you register a webhook**, then **assign it to a product** (your store). From that moment on, every paid transaction in that store is delivered to the endpoint you specified.

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

<Info>
  Examples use the sandbox environment: `https://gateway-api.sandbox.paymove.io`. You need your `partnerId` and the `productId` of the store created in the [Payment Gateway: basic integration](/en/products/payment-gateway/tutorial) tutorial.
</Info>

## Step 1: Register a webhook

In `requestTemplate` you define the request body Paymove will send to your endpoint. You can use the variables `{{externalId}}` (your order identifier) and `{{price}}` (the 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": {
      "Authorization": ["Bearer abc123"],
      "Content-Type": ["application/json"]
    }
  }'
```

| Field              | Description                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------ |
| `name`             | Webhook name (visible in the configuration).                                               |
| `endpoint`         | URL Paymove sends the request to.                                                          |
| `method`           | HTTP method (e.g. `POST`).                                                                 |
| `requestTemplate`  | Request body template (variables substituted by Paymove).                                  |
| `responseTemplate` | Expected response structure from the partner.                                              |
| `expectedCode`     | Expected HTTP response code (e.g. 200).                                                    |
| `expectedResponse` | Expected response body (e.g. `{ "status": "ok" }`).                                        |
| `retries`          | Number of retries on failure. **Defaults to `0`** - set it explicitly if you want retries. |
| `partnerId`        | Partner identifier (assigned by Paymove).                                                  |
| `type`             | Event type - for payment notifications: `PAYMENT`.                                         |
| `headers`          | Headers attached to the request (e.g. `Authorization`, `Content-Type`).                    |

**Response (200):**

```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": {
    "Authorization": ["Bearer abc123"],
    "Content-Type": ["application/json"]
  },
  "signingSecret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

The `id` field in the response is the webhook identifier (`webhookId`) you will use in step 2.

<Warning>
  The response contains a `signingSecret` - a secret for verifying the signature of requests coming from Paymove. Store it securely on your side and never expose it publicly.
</Warning>

## Step 2: Assign the webhook to your store

The webhook only starts working once it is linked to a product. In the URL, replace `webhookId` (from step 1) and the `productId` of your store.

```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 (same as in step 1), confirming the link.

From now on, every completed payment in this store triggers your `endpoint`.

## Verify the configuration

You can list the products linked to a webhook with:

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

**Response (200):** an array of products linked to the webhook:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
[
  {
    "id": "891412c8-8717-4449-9543-e34112bec470",
    "name": "Sklep Testowy",
    "productType": "PAY",
    "status": "ACTIVE"
  }
]
```

## What does a payment notification look like?

After a completed payment, Paymove sends a request to your `endpoint` matching the `requestTemplate` and configured `headers`. For the template from step 1, the body looks like this:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "orderId": "order-123",
  "price": 1000
}
```

`orderId` is the `externalId` you passed when creating the payment - it lets you unambiguously match the notification to an order in your system.

<Info>
  The shape above is the result of the `requestTemplate`. With an empty template, Paymove sends the full payment object instead - the field set is described in [Configuration](/en/webhooks#webhook-payload).
</Info>

<Warning>
  Before trusting the notification, **verify the `X-Paymove-Signature` header**. Without it, anyone who knows your `endpoint` can send a forged notification and have an order fulfilled without paying. Ready-to-copy code: [Webhook signature verification](/en/webhook-signature).
</Warning>

Your system should respond with a status **exactly equal** to `expectedCode` (customarily `200` with a `{ "status": "ok" }` body, though the body is never inspected) - only then does Paymove consider the delivery successful. A `201` or `204` response against `expectedCode: 200` counts as a failure and is **not retried**. Deliveries that end in a 4xx, a 5xx or a network error are retried as many times as `retries` specifies - **`0` by default, meaning not at all**.

<Tip>
  Fulfil the order when you receive the webhook, not when the customer returns to your `returnUrl` - the customer may close the browser before coming back to your store, and the redirect itself can be triggered without paying.
</Tip>

## What's next?

<CardGroup cols={2}>
  <Card title="Payment Gateway: basic integration" icon="rocket" href="/en/products/payment-gateway/tutorial">
    The full payment flow: product, payment, customer redirect.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/en/webhooks">
    Payment notification configuration details.
  </Card>
</CardGroup>
