Using a Dynamic QR

Generate a per-transaction QR code with an embedded amount — the bridge encodes the checkout total and produces a single-use code ready for the POS display.

Audience: Developers, Integration Engineers · Read time: 4 min

A dynamic QR is generated per transaction — at checkout, on an invoice, or in a mobile app. Unlike a static sticker, it embeds a fixed amount in the code itself so the customer just confirms rather than typing a number. The bridge encodes the amount in EMVco Tag 54, sets the point-of-initiation to 12 (one-time), and produces a single-use payload that expires once the payment settles.

The merchant doesn't touch tag numbers, GUID prefixes, or tax condition codes. The bridge reads the merchant wallet — name, city, category, tax regime — and generates the complete network-compliant payload. Creating a dynamic QR is pointing at that wallet and saying what the checkout total is.

What you're building

You'll create a qr-dynamic anchor for a specific checkout amount. The bridge reads the merchant wallet hierarchy, encodes the amount in the payload, and writes a ready-to-scan code back to the anchor. The POS display reads that payload and renders the QR — the customer scans, confirms, and pays.

Prerequisites

  • A merchant wallet already created with a complete profile (name, city, category, tax regime, settlement account)
  • SDK credentials and a connection to a ledger
  • The qr-dynamic anchor schema deployed on the ledger
  • A QR bridge connected and configured with a processing policy for anchor enrichment

Create a dynamic QR code

Create the anchor with an amount

minka anchor create
PromptWhat to enter
HandleA QR identifier per transaction, e.g. qr0000099@starbucks.com
Schemaqr-dynamic
TargetThe store wallet, e.g. store1@starbucks.com
AmountThe payment amount in base units, e.g. 1500000 for 15,000.00 COP
SymbolThe currency handle, e.g. cop
Custom{"reference":"FACT-2024-001234"}
curl -X POST "https://{ledger-host}/v2/anchors" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {your-token}" \
  -H "x-ledger: {your-ledger}" \
  -d '{
    "data": {
      "handle": "qr0000099@starbucks.com",
      "schema": "qr-dynamic",
      "target": "store1@starbucks.com",
      "amount": "1500000",
      "symbol": { "handle": "cop" },
      "custom": {
        "reference": "FACT-2024-001234"
      }
    }
  }'
POST /v2/anchors
// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { response } = await sdk.anchor
  .init()
  .data({
    handle: 'qr0000099@starbucks.com',
    schema: 'qr-dynamic',
    target: 'store1@starbucks.com',
    amount: '1500000',
    symbol: { handle: 'cop' },
    custom: {
      reference: 'FACT-2024-001234',
    },
  })
  .hash()
  .sign([{ keyPair }])
  .send()
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();

String json = """
    {
      "data": {
        "handle": "qr0000099@starbucks.com",
        "schema": "qr-dynamic",
        "target": "store1@starbucks.com",
        "amount": "1500000",
        "symbol": { "handle": "cop" },
        "custom": {
          "reference": "FACT-2024-001234"
        }
      }
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://{ledger-host}/v2/anchors"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer {your-token}")
    .header("x-ledger", "{your-ledger}")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());
using System.Net.Http;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer {your-token}");
client.DefaultRequestHeaders.Add("x-ledger", "{your-ledger}");

var json = """
    {
      "data": {
        "handle": "qr0000099@starbucks.com",
        "schema": "qr-dynamic",
        "target": "store1@starbucks.com",
        "amount": "1500000",
        "symbol": { "handle": "cop" },
        "custom": {
          "reference": "FACT-2024-001234"
        }
      }
    }
    """;

var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
    "https://{ledger-host}/v2/anchors", content);
var body = await response.Content.ReadAsStringAsync();

The curl examples use a Bearer token for authentication. In production, every mutation also requires a cryptographic signature — see Sign Payments for the full signing flow. The TypeScript SDK handles signing automatically.

The bridge reads store1@starbucks.com, sees type: "store", and walks up to main@starbucks.com for the merchant-level fields. It resolves the MCC from the category, detects the alias type from the taxId, selects the correct national standard by country code, and generates the TLV payload — this time also encoding the amount in Tag 54 and marking the code as single-use.

FieldWhat it does
amountThe payment amount in the currency's base units. 1500000 means 15,000.00 COP (2 decimal places).
symbolThe currency. The bridge maps the handle to the ISO 4217 numeric code for the payload (cop170).
referenceYour invoice or order number — included in the payload so you can reconcile the payment against your system.

The amount encoding uses the currency profile's decimal configuration — for COP, 2 decimal places, so 1500000 base units → 15000.00 in the Tag 54 value. For currencies with no decimal places (like CLP), the conversion is direct. The symbol handle maps to the ISO 4217 numeric code written into the payload (cop170, usd840).

A standard regime merchant gets IVA and INC condition codes computed automatically from the wallet's tax configuration. You can override the merchant's default tax treatment for a specific transaction by passing custom.taxRegime on the anchor — for example, "exempt" for a promotional sale skips the tax block entirely, while "simplified" selects the condition code for small merchants. This lets you handle edge cases without changing the wallet's default configuration.

Display at checkout

Read the anchor and display the QR on the POS screen:

minka anchor read qr0000099@starbucks.com
curl "https://{ledger-host}/v2/anchors/qr0000099@starbucks.com" \
  -H "Authorization: Bearer {your-token}" \
  -H "x-ledger: {your-ledger}"
GET /v2/anchors/qr0000099@starbucks.com
// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { anchor } = await sdk.anchor.read('qr0000099@starbucks.com')

console.log(anchor.data.custom.qr.payload)      // full TLV string — encode into a QR image
console.log(anchor.data.custom.qr.paymentId)     // network transaction ID for inbound routing
console.log(anchor.data.custom.qr.generatedAt)   // when the payload was generated

// renderQRImage(anchor.data.custom.qr.payload) // your QR rendering library
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://{ledger-host}/v2/anchors/qr0000099@starbucks.com"))
    .header("Authorization", "Bearer {your-token}")
    .header("x-ledger", "{your-ledger}")
    .GET()
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());
using System.Net.Http;

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer {your-token}");
client.DefaultRequestHeaders.Add("x-ledger", "{your-ledger}");

var response = await client.GetAsync(
    "https://{ledger-host}/v2/anchors/qr0000099@starbucks.com");
var body = await response.Content.ReadAsStringAsync();

The paymentId follows the format CO.COM.RBM.TRXID{timestamp}{random} — this is the identifier the external network uses to route inbound payments back to this anchor. The bridge also writes it to meta.labels for reverse lookup when the payment arrives.

The customer scans, sees the amount pre-filled in their bank app, confirms, and pays. There's nothing for the cashier to verify — the amount on the customer's screen matches the checkout total exactly.

Dynamic QR codes are single-use. Once the payment settles, the anchor is consumed and the paymentId is marked as used by the network. If a customer scans after settlement, their bank app will return an error indicating the QR is no longer valid. For timeout or abandon scenarios, you can delete the anchor explicitly or let it expire via a TTL policy on the schema.

Amount encoding

The ledger stores amounts as integers in the currency's base unit — there are no floating-point values. The bridge converts to the display amount for the EMVco payload using the currency's decimal configuration.

For Colombian pesos (COP), the decimal count is 2, so the bridge divides by 100 to produce the Tag 54 value: 150000015000.00. For Chilean pesos (CLP), the decimal count is 0, so 15000 stays 15000. The symbol handle (cop, usd, clp) maps to the ISO 4217 numeric code embedded in the currency tag of the payload.

If you pass an amount that doesn't match the currency's expected precision — for example, 1500001 when the currency has 2 decimal places — the bridge will reject the anchor with a validation error before generating any payload.

Tax override per transaction

The merchant wallet carries a default taxRegime (typically standard, simplified, or exempt) that the bridge uses for every QR it generates. For most transactions this is correct and you don't need to think about it.

For edge cases — a promotional item sold tax-free, a transaction between registered entities that changes the applicable regime — pass custom.taxRegime directly on the dynamic anchor:

{
  "data": {
    "handle": "qr0000100@starbucks.com",
    "schema": "qr-dynamic",
    "target": "store1@starbucks.com",
    "amount": "50000",
    "symbol": { "handle": "cop" },
    "custom": {
      "reference": "PROMO-2024-0042",
      "taxRegime": "exempt"
    }
  }
}

The per-transaction override does not change the merchant's wallet configuration. It applies only to this anchor's payload.

Static vs. dynamic

A static QR lives as a permanent sticker on the counter — one anchor, unlimited scans, the customer enters the amount. A dynamic QR is generated per transaction — one anchor, one payment, the amount is embedded and locked.

Use dynamic when the checkout total is known at the time of display: POS terminal, invoice, in-app payment request. Use static when the merchant handles a wide range of amounts without a POS system, or when the infrastructure to generate a QR per transaction isn't available yet. See Collect with a Static QR for the static flow.

What you learned

  • How to create a qr-dynamic anchor with an amount, currency, and reference
  • How the bridge encodes the amount into EMVco Tag 54 using the currency's decimal configuration
  • How symbol maps to ISO 4217 numeric codes in the payload
  • How to override the tax regime per transaction without changing the merchant wallet
  • Why dynamic QR codes are single-use and what happens after settlement

Common errors

ErrorCauseFix
amount-exceeds-limitThe amount exceeds the maximum allowed by the schema or symbol configurationCheck the symbol's maximum transaction amount with minka symbol read {symbol}
anchor-expiredThe dynamic QR's TTL has elapsed before the customer scanned itGenerate a new anchor — dynamic QRs are single-use and time-limited
handle-already-existsAn anchor with this handle already existsUse a unique handle per transaction (e.g., include a timestamp or order ID)

Next steps

On this page