> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibepay.mn/llms.txt
> Use this file to discover all available pages before exploring further.

# The QR code

> What the cardholder shows you, why it changes after every payment, and how to handle it.

Vibepay has no plastic card. The cardholder's card is a pass in Apple Wallet or Google Wallet, and
the pass shows a QR code. That code is the payment credential.

## What you scan

A string beginning with `vqr_`:

```
vqr_AXk9Lm0pQr2sTu4vWx6yZa8bCd0eFg2h.Ij4kLm6nOp8qRs0t
```

Inside it is a cryptographically signed reference to a card. Your POS does not need to know any of
that — **pass the scanned string through byte for byte** as the `qrToken` field.

<Warning>
  Do not parse it, do not split on the `.`, do not strip the `vqr_` prefix, do not uppercase it,
  and do not re-encode it. The signature covers the exact bytes; any modification makes it invalid.
</Warning>

## It is single-use

This is the most important property of the system, and the one most likely to surprise you.

The moment a charge against a code commits, that code is retired — in the same instant, as part of
the same database write. The pass immediately begins showing a new one.

```mermaid theme={null}
flowchart LR
    A["Pass shows<br/>vqr_AAA…"] -->|scan| B[Charge ₮12,500]
    B -->|committed| C["vqr_AAA… is dead"]
    C --> D["Pass shows<br/>vqr_BBB…"]
    B -.->|retry with vqr_AAA…| E["409<br/>qr token already used"]
```

So a `409 qr token already used` does not mean something went wrong. It usually means **the
payment already succeeded** and you are looking at a duplicate scan or a retry. Confirm with
`GET /v1/transactions` before you either hand over goods or charge again.

This is also why you must never cache a scanned code, log it for later replay, or let a cashier
re-submit an old one from a screen history. It will not work, and the attempt is indistinguishable
from fraud.

## It also expires

A code stops being accepted 24 hours after the pass created it, whether or not it was ever
charged. That is a backstop against an old screenshot, not the main defence — the single-use rule
is.

In practice a pass refreshes its code well within that window, so an expired code almost always
means the customer's phone has been offline for a long time. The fix is the same either way:

<Info>
  **`TOKEN_EXPIRED` → ask the customer to open their Vibepay pass and show it again.** Opening the
  pass refreshes the code. Then scan and charge afresh — with a *new* idempotency key, since this
  is a new attempt at the same sale only if you kept the original key. See
  [Idempotency](/idempotency).
</Info>

## Scanner requirements

| Requirement           | Detail                                                                                              |
| --------------------- | --------------------------------------------------------------------------------------------------- |
| Symbology             | QR Code. Not Code 128, not PDF417.                                                                  |
| Screen scanning       | Must read from a phone screen, not just paper. Most modern imagers do; older laser scanners do not. |
| Output                | The raw decoded string, unmodified. Turn off any prefix/suffix or keyboard-wedge transformations.   |
| Duplicate suppression | Recommended. It stops one physical scan firing two charge attempts.                                 |

If your scanner appends a carriage return or a configured prefix, strip it in your integration
before sending — an extra byte is a `TOKEN_INVALID`.

## Errors you will see

| Code            | Status | What happened                                 | What to do                                                       |
| --------------- | ------ | --------------------------------------------- | ---------------------------------------------------------------- |
| `TOKEN_EXPIRED` | 422    | The code is older than 24 hours               | Ask for a refreshed pass, rescan                                 |
| `TOKEN_INVALID` | 422    | Not a Vibepay code, or the string was altered | Rescan; check scanner prefixes                                   |
| —               | 409    | The code was already spent                    | **Verify before recharging** — the payment probably went through |

<Card title="Now send the charge" icon="arrow-right" href="/charge" horizontal>
  Request fields, amount rules, and the response.
</Card>
