# Use QR code payments

Create one-time QR payments programmatically with the Payments API. Each payment gets its own short-lived QR code — ideal for dynamic checkouts, kiosks, and invoices generated by your system.

Looking for no-code QR payments?

To create and manage **permanent QR codes** (reusable, printable, with fixed amounts and callback notifications) from the dashboard, see [stores and points of sale](https://docs.monei.com/manage-account/stores-and-points-of-sale/.md). To charge in person from a phone, see the [MONEI Pay app](https://docs.monei.com/monei-pay/using-the-app/.md).

## Permanent vs one-time QR codes[​](#permanent-vs-one-time "Direct link to Permanent vs one-time QR codes")

|                 | Permanent                                                                                                | One-Time                                                                 |
| --------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Created via** | [Dashboard points of sale](https://docs.monei.com/manage-account/stores-and-points-of-sale/.md#qr-codes) | Payments API / MONEI Pay app                                             |
| **URL format**  | `https://secure.monei.com/codes/{code_id}`                                                               | `https://secure.monei.com/payments/{payment_id}/qr`                      |
| **Reusable**    | Yes, same QR for multiple transactions                                                                   | No, one payment per QR                                                   |
| **Amount**      | Customer enters (manual) or fixed                                                                        | Pre-set per payment                                                      |
| **Expiration**  | Never (can be disabled)                                                                                  | 7 days by default (or custom `expireAt` via API, any time in the future) |
| **Best for**    | Static displays, printed materials, tables                                                               | Dynamic checkout, invoices, mobile POS                                   |

This guide covers the **one-time** flow. For permanent codes, [configure a QR-type point of sale](https://docs.monei.com/manage-account/stores-and-points-of-sale/.md#points-of-sale) — no code required.

## Before you begin[​](#before-you-begin "Direct link to Before you begin")

* You'll need a MONEI account. Find your API keys in [MONEI Dashboard → Settings → API Access](https://dashboard.monei.com/settings/api).
* Use [test mode keys](https://docs.monei.com/testing/.md) for integration testing.
* Ensure relevant payment methods are enabled in your account settings.
* Monitor test payments in [Dashboard → Payments](https://dashboard.monei.com/payments) (enable Test Mode toggle).

Your API key is on [MONEI Dashboard → Settings → API Access](https://dashboard.monei.com/settings/api):

![API Access page in the MONEI Dashboard settings, showing your account ID and API key](/img/dashboard/en/settings-api-access.png)

API Access page in the MONEI Dashboard settings, showing your account ID and API key

## 1. Create a payment (server-side)[​](#1-create-a-payment-server-side "Direct link to 1. Create a payment (server-side)")

Create a [Payment](https://docs.monei.com/apis/rest/schemas/payment/.md) on your server with an amount and currency.

* cURL
* Node.js
* PHP
* Python

POST https\://api.monei.com/v1/payments

```
curl --request POST 'https://api.monei.com/v1/payments' \

--header 'Authorization: YOUR_API_KEY' \

--header 'Content-Type: application/json' \

--data-raw '{

    "amount": 110,

    "currency": "EUR",

    "orderId": "14379133960355",

    "callbackUrl": "https://example.com/checkout/callback"

}'
```

(Replace `YOUR_API_KEY` with your actual MONEI API key)

server.js

```
import {Monei} from '@monei-js/node-sdk';



// Replace YOUR_API_KEY with your actual MONEI API key

const monei = new Monei('YOUR_API_KEY');



const payment = await monei.payments.create({

  amount: 110,

  currency: 'EUR',

  orderId: '14379133960355',

  callbackUrl: 'https://example.com/checkout/callback'

});



// You will need the paymentId from the response to generate the QR code URL

const paymentId = payment.id;



// Construct the QR code URL

const qrCodeUrl = `https://secure.monei.com/payments/${paymentId}/qr`;
```

server.php

```
<?php

require_once 'vendor/autoload.php';



use Monei\Model\CreatePaymentRequest;

use Monei\MoneiClient;



// Replace YOUR_API_KEY with your actual MONEI API key

$monei = new MoneiClient('YOUR_API_KEY');



$payment = $monei->payments->create(

  new CreatePaymentRequest([

    'amount' => 110,

    'currency' => 'EUR',

    'order_id' => '14379133960355',

    'callback_url' => 'https://example.com/checkout/callback'

  ])

);



// You will need the paymentId from the response to generate the QR code URL

$paymentId = $payment->getId();



// Construct the QR code URL

$qrCodeUrl = "https://secure.monei.com/payments/{$paymentId}/qr";

?>
```

server.py

```
import Monei

from Monei import CreatePaymentRequest



# Replace YOUR_API_KEY with your actual MONEI API key

monei = Monei.MoneiClient(api_key="YOUR_API_KEY")



payment = monei.payments.create(

    CreatePaymentRequest(

        amount=110,

        currency="EUR",

        order_id="14379133960355",

        callback_url="https://example.com/checkout/callback"

    )

)



# You will need the paymentId from the response to generate the QR code URL

payment_id = payment.id



# Construct the QR code URL

qr_code_url = f"https://secure.monei.com/payments/{payment_id}/qr"
```

**Required Parameters:**

* **amount** `positive integer`: Amount in the smallest currency unit (e.g., 110 for €1.10).
* **currency** `string`: Three-letter [ISO currency code](https://en.wikipedia.org/wiki/ISO_4217) (e.g., `EUR`).
* **orderId** `string`: Your unique order identifier.
* **callbackUrl** `string`: Your server endpoint URL for asynchronous webhook notifications.

**Optional Parameters:**

* **allowedPaymentMethods** `array`: Restrict available payment methods (e.g., `["card", "bizum"]`)
* **description** `string`: Payment description shown on the payment page
* **customer** `object`: Pre-fill customer info (`email`, `name`, `phone`)
* **metadata** `object`: Custom key-value pairs for tracking/reconciliation
* **storeId** `string`: Associate payment with a store (for grouping and user access control)
* **pointOfSaleId** `string`: Link payment to a POS (for grouping and user access control)
* **expireAt** `integer`: Unix timestamp for custom expiration (default: 7 days from creation; must be in the future)

Check all available [request parameters](https://docs.monei.com/apis/rest/payments-create/.md).

The API response includes the `payment.id`, which you'll use in the next step.

## 2. Display the QR code[​](#display-qr "Direct link to 2. Display the QR code")

Use the `payment.id` from Step 1 to present the QR code to your customer.

![QR](/img/qr.svg)![QR demo](/img/qr-demo.png)

**Option 1: Embed QR Image Directly**

Construct the QR code image URL: `https://secure.monei.com/payments/{payment_id}/qr`

You can render it directly on a webpage or display:

```
<img

  src="https://secure.monei.com/payments/{{payment_id}}/qr?format=svg&size=300"

  alt="Scan to Pay"

  width="300"

  height="300"

/>
```

* Replace `{{payment_id}}` with the actual ID.
* Use `?format=svg` for SVG (default is `png`).
* Use `?size=400` to specify size (min: 100, max: 1000, default: 300).

![Example QR](https://secure.monei.com/codes/RYGJ0ZFK/qr?format=svg\&size=300)

**Option 2: Redirect to Hosted Page with QR**

The [Payment object](https://docs.monei.com/apis/rest/schemas/payment/.md) returned in Step 1 also contains `payment.nextAction.redirectUrl`. Append `?qr=1` to this URL to get a link to a MONEI-hosted page displaying the QR code.

Example: `https://secure.monei.com/payments/{payment_id}?qr=1`

![Hosted Payment Page QR](/assets/images/qr-demo-2-b4a9016b32b61c1d1b67a35812d432c0.png)

**Customer Interaction:**

The customer scans the QR code with their phone and completes the payment on the MONEI payment page using their chosen method.

warning

The QR code payment link is valid until the payment expires — **7 days** by default, or the custom `expireAt` you set (any time in the future). After that, you must create a new payment request.

## 3. Process the webhook notification (server-side)[​](#webhook "Direct link to 3. Process the webhook notification (server-side)")

MONEI sends the final, authoritative payment status via an asynchronous HTTP POST request to the `callbackUrl` you provided in Step 1. The request body contains the full [Payment object](https://docs.monei.com/apis/rest/schemas/payment/.md) in JSON format.

This webhook ensures you get the definitive status even if the customer closes their browser or loses connection after scanning.

**Crucially, you must:**

1. **Verify the `MONEI-Signature` header** included in the request. This confirms the webhook genuinely came from MONEI. See the [Verify Signatures guide](https://docs.monei.com/guides/verify-signature/.md) for implementation details.
2. **Return a `200 OK` HTTP status code** immediately upon receiving the webhook to acknowledge receipt. Any other status code tells MONEI the notification failed.

If MONEI doesn't receive a `200 OK`, it will retry sending the webhook.

Once the signature is verified, inspect the `status` field in the Payment object to confirm payment success (`SUCCEEDED`) and fulfill the order, or handle failures.

## Alternative: poll payment status[​](#polling "Direct link to Alternative: poll payment status")

For kiosk or display scenarios where you need real-time status updates, poll the payment status instead of (or in addition to) webhooks.

* cURL
* Node.js
* PHP
* Python

GET https\://api.monei.com/v1/payments/{payment\_id}

```
curl --request GET 'https://api.monei.com/v1/payments/{payment_id}' \

--header 'Authorization: YOUR_API_KEY'
```

server.js

```
const payment = await monei.payments.get(paymentId);

console.log(payment.status);
```

server.php

```
<?php

$payment = $monei->payments->get($paymentId);

echo $payment->getStatus();

?>
```

server.py

```
payment = monei.payments.get(payment_id)

print(payment.status)
```

**Status values:** `PENDING`, `PENDING_PROCESSING`, `SUCCEEDED`, `FAILED`, `CANCELED`, `EXPIRED`

Polling Best Practice

Poll every 2-3 seconds. Stop when status is no longer `PENDING`/`PENDING_PROCESSING` or when the QR expires.

## Customization[​](#customization "Direct link to Customization")

You can customize the appearance of the QR code (color, icon) and the hosted payment page in your [MONEI Dashboard → Settings → Branding](https://dashboard.monei.com/settings/branding). The **Icon** you upload there is also the icon MONEI puts at the centre of your QR codes, and **Primary color** is the colour they are drawn in:

![Branding page in the MONEI Dashboard settings, where you customize your payment page](/img/dashboard/en/settings-branding.png)

Branding page in the MONEI Dashboard settings, where you customize your payment page

## Testing[​](#testing "Direct link to Testing")

* Use your [test mode API keys](https://docs.monei.com/testing/.md) for development
* Enable **Test Mode** in your [Dashboard](https://dashboard.monei.com/payments) to view test payments
* Use [test card numbers](https://docs.monei.com/testing/.md#test-card-numbers) to simulate different scenarios
* Verify webhook delivery in [MONEI Dashboard → Settings → Webhooks](https://dashboard.monei.com/settings/webhooks)

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

**QR code expired** QR codes are valid until the payment expires (7 days by default). Create a new payment if the code expires.

**Payment method not showing** Check that the method is enabled in your account and not filtered by `allowedPaymentMethods`.

**Webhook not received** Verify your `callbackUrl` is publicly accessible, returns `200 OK`, and check webhook logs in Dashboard.
