---
title: QR Estático - Factura
slug: qr-estatico-factura
icon: {"faIcon":"fa-solid fa-file-invoice-dollar"}
createdAt: 2024-04-22T18:25:02.001Z
---

Mediante esta opción se puede generar de manera completamente offline un código QR estático que podrá ser impreso en una factura u enviado via email, whatsapp o el medio que fuese. Este QR se genera con un código de Referencia que evita que el mismo pueda ser pagado múltiples veces.

# Información General

## Paises

- 🇦🇷 Argentina



## Formato del QR

EMV QR Merchant Presented Mode ( MPM )

## Librerias para Generarlo

- PHP: [https://github.com/arcticfalcon/emv-qr-cps](https://github.com/arcticfalcon/emv-qr-cps)
- C#: [https://github.com/juanroman-zz/emvqr](https://github.com/juanroman-zz/emvqr)
- .NET: [https://www.nuget.org/packages/emv-qrcps/1.0.1](https://www.nuget.org/packages/emv-qrcps/1.0.1)
- Go: [https://github.com/dongri/emv-qrcode](https://github.com/dongri/emv-qrcode)
- NodeJS: [https://github.com/steplix/emv-qrcps](https://github.com/steplix/emv-qrcps)
- Java: [https://github.com/mvallim/emv-qrcode](https://github.com/mvallim/emv-qrcode)

# Pasos

1\) Obtener primero el Punto de Venta sobre el cual se generarán los QR. Esto puede hacerse desde la Consola en "Administrar POS". De aquí se debe obtener el "ID".

2\) Solicitar a nuestro equipo todos los datos indicados en la tabla.

3\) Utilizando alguna de las librerias generar el QR con la siguiente información

| Nombre del Campo                                          | Campo | Sub-Campo | Tipo       | Valor                                                                                                                              |
| --------------------------------------------------------- | ----- | --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Payload Format Indicator / Indicador de Formato           | 00    |           | Fijo       | 01                                                                                                                                 |
| Point of Initiation Method / Metodo de Iniciación         | 01    |           | Fijo       | 11                                                                                                                                 |
| Reversed Domain / Dominio Reverso                         | 41    |           | Sub-Campos |                                                                                                                                    |
|                                                           |       | 00        | Fijo       | io.sugaway.interoperable                                                                                                           |
|                                                           |       | 01        | Fijo       | prisma                                                                                                                             |
|                                                           |       | 98        | Fijo       | 30598910045                                                                                                                        |
|                                                           |       | 99        | Fijo       | 01                                                                                                                                 |
| Merchant ID / ID Del Comercio                             | 50    |           | Sub-Campos |                                                                                                                                    |
| *CUIT*                                                    |       | 00        | Dinámico   | Solicitar a Soporte                                                                                                                |
| *CUIT Formateado*                                         |       | 01        | Dinámico   | Campo anterior en formato XX-XXXXXXXX-X                                                                                            |
| Merchant Account / Cuenta del Comercio                    | 51    |           | Sub-Campos |                                                                                                                                    |
| *CVU*                                                     |       | 00        | Dinámico   | Solicitar a Soporte                                                                                                                |
| Merchant Category Code / Código de Categoria del Comercio | 52    |           | Dinámico   | Solicitar a Soporte                                                                                                                |
| Currency / Moneda                                         | 53    |           | Fijo       | 032                                                                                                                                |
| Total                                                     | 54    |           | Dinámico   | Total de la operación en formato NNNN.DD. Donde N es la parte entera y D los decimales. Este total debe tener siempre 2 decimales. |
| Country Code / Código de País                             | 58    |           | Fijo       | AR                                                                                                                                 |
| Merchant Name / Nombre del Comercio                       | 59    |           | Dinámico   | Solicitar a Soporte                                                                                                                |
| Merchant City / Ciudad del Comercio                       | 60    |           | Dinámico   | Solicitar a Soporte                                                                                                                |
| Zip Code / Código Postal                                  | 61    |           | Dinámico   | Solicitar a Soporte                                                                                                                |
|                                                           | 80    | 01        | Dinámico   | D1=1775150280;T2=150.00;D2=1775409480; Donde T2 es el Total 2. D1 y D2 son los vencimientos como Timestamp en Milisegundos         |
|                                                           | 81    | 01        | Dinámico   | Firma del campo 80                                                                                                                 |
| Additional Data / Datos Adicionales                       | 62    |           | Sub-Campos |                                                                                                                                    |
| Point of Sale / Punto de Venta                            |       | 01        | Dinámico   | Dato obtenido en el punto 1 del tutorial. Este dato siempre va a ser el mismo en todos los QR. Solicitar a Soporte.                |
| Reference / Referencia                                    |       | 07        | Dinámico   | Código único de operación, este campo permitirá que la factura o la operación no pueda ser abonada múltiples veces.                |

## Ejemplo

Código generado en el formato especificado

```javascript
00020101021141490024io.sugaway.interoperable98113059891004599020150280009121231234011112-123123-4512600229999999999999999999999520473995913Mi Comercio 26004CABA5802AR6108C1082ABF5303032540410.5626201183T3WN9P9G22KT7UHDA0536B3B3ACB6-CB14-4A3F-A05D-91E013D810D863041F39
```

## Generación de Firma

### Estructura

El QR estático firmado utiliza los campos estándar EMV QR Merchant Presented Mode más tres campos adicionales para importes con vencimientos firmados:

| Campo | Nombre              | Contenido                                         |
| ----- | ------------------- | ------------------------------------------------- |
| 54    | Amount              | Primer importe (T1)                               |
| 80    | Unreserved Template | Subcampo 01: payload firmado en base64url         |
| 81    | Unreserved Template | Subcampo 01: firma ECDSA del payload en base64url |

### Formato del Payload (Campo 80.01)

El contenido del subcampo 01 del campo 80 es un string codificado en **base64url sin padding**. Al decodificarlo se obtiene:

:::BlockQuote
T1=&#x7B;monto};D1=&#x7B;timestamp};T2=&#x7B;monto};D2=&#x7B;timestamp};
:::

Donde:

- T&#x7B;n} es un importe con 2 decimales (ej: 100.00)
- D&#x7B;n} es el timestamp de vencimiento del importe T&#x7B;n} en formato Unix epoch en segundos
- n es un índice secuencial comenzando en 1
- T1 se replica además en el campo 54 del QR (campo estándar EMV Amount)

**Importante:** T1 se incluye tanto en el campo 54 como dentro del payload firmado. Esto garantiza que si alguien modifica el campo 54 del QR, la verificación de firma falla.

**Ejemplo decodificado:**

:::BlockQuote
T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;
:::

Esto define:

- $100.00 válido hasta el timestamp 1775150280
- $150.00 válido hasta el timestamp 1775236680

Al momento del pago, el sistema selecciona el primer importe cuyo vencimiento aún no haya expirado. Si todos los importes están vencidos, se rechaza la operación. Actualmente se soportan hasta **2 importes con vencimientos**.

### Codificación Base64url sin Padding

Se utiliza la variante URL-safe de Base64 definida en RFC 4648 §5:

1. Codificar el string en Base64 estándar
2. Reemplazar + por -
3. Reemplazar / por \_
4. Eliminar los = del final (padding)

Para decodificar, realizar el proceso inverso: reemplazar - por +, \_ por /, agregar = hasta que el largo sea múltiplo de 4, y decodificar Base64.

### Generación de la Firma (Campo 81.01)

**Algoritmo:** ECDSA con SHA-256 sobre curva P-256 (prime256v1)

**Tamaño de la firma:** 64 bytes (86 caracteres en base64url)

**Proceso:**

1. Construir el payload en texto plano: T1=&#x7B;amount};D1=&#x7B;ts};T2=&#x7B;amount};D2=&#x7B;ts};
2. Codificar en base64url sin padding → este valor va en el campo 80.01
3. Firmar el **valor codificado en base64url** (el string del paso 2) con ECDSA-SHA256 usando la clave privada
4. Codificar la firma en base64url sin padding → este valor va en el campo 81.01

**Nota:** Se firma el string base64url (paso 2), no el texto plano original. Esto es importante para que la verificación sea consistente.

### Ejemplos por Lenguaje

JavaScript (Web Crypto API)

:::BlockQuote
// 1. Construir payload
const payloadRaw = "T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;";

// 2. Codificar en base64url
const payloadB64 = btoa(payloadRaw)
&#x20; .replace(/\\+/g, '-').replace(/\\//g, '\_').replace(/=+$/, '');

// 3. Importar clave privada
function pemToArrayBuffer(pem) \{
&#x20; const b64 = pem.replace(/-----BEGIN \[\w\s]+-----/g, '')
&#x20;   .replace(/-----END \[\w\s]+-----/g, '').replace(/\s/g, '');
&#x20; const binary = atob(b64);
&#x20; const bytes = new Uint8Array(binary.length);
&#x20; for (let i = 0; i \< binary.length; i++) bytes\[i] = binary.charCodeAt(i);
&#x20; return bytes.buffer;
}

const key = await crypto.subtle.importKey(
&#x20; 'pkcs8', pemToArrayBuffer(privateKeyPem),
&#x20; \{ name: 'ECDSA', namedCurve: 'P-256' },
&#x20; false, \['sign']
);

// 4. Firmar
const sig = await crypto.subtle.sign(
&#x20; \{ name: 'ECDSA', hash: 'SHA-256' },
&#x20; key, new TextEncoder().encode(payloadB64)
);

// 5. Codificar firma en base64url
const signatureB64 = btoa(String.fromCharCode(...new Uint8Array(sig)))
&#x20; .replace(/\\+/g, '-').replace(/\\//g, '\_').replace(/=+$/, '');
:::

Python

:::BlockQuote
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils

payload\_raw = "T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;"

\# Codificar en base64url
payload\_b64 = base64.urlsafe\_b64encode(payload\_raw\.encode()).rstrip(b'=').decode()

\# Cargar clave privada
private\_key = serialization.load\_pem\_private\_key(pem\_bytes, password=None)

\# Firmar (DER → raw r||s para compatibilidad con WebCrypto)
der\_sig = private\_key.sign(payload\_b64.encode(), ec.ECDSA(hashes.SHA256()))
r, s = utils.decode\_dss\_signature(der\_sig)
raw\_sig = r.to\_bytes(32, 'big') + s.to\_bytes(32, 'big')

\# Codificar firma en base64url
signature\_b64 = base64.urlsafe\_b64encode(raw\_sig).rstrip(b'=').decode()
:::

PHP

:::BlockQuote
$payloadRaw = "T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;";

// Codificar en base64url
$payloadB64 = rtrim(strtr(base64\_encode($payloadRaw), '+/', '-\_'), '=');

// Cargar clave privada
$privateKey = openssl\_pkey\_get\_private($pemString);

// Firmar
openssl\_sign($payloadB64, $signature, $privateKey, OPENSSL\_ALGO\_SHA256);

// Codificar firma en base64url
$signatureB64 = rtrim(strtr(base64\_encode($signature), '+/', '-\_'), '=');
:::

Go

:::BlockQuote
import (
&#x20;   "crypto/ecdsa"
&#x20;   "crypto/rand"
&#x20;   "crypto/sha256"
&#x20;   "crypto/x509"
&#x20;   "encoding/base64"
&#x20;   "encoding/pem"
&#x20;   "math/big"
&#x20;   "strings"
)

payloadRaw := "T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;"

// Codificar en base64url
payloadB64 := strings.TrimRight(
&#x20;   base64.URLEncoding.EncodeToString(\[]byte(payloadRaw)), "=")

// Cargar clave privada
block, \_ := pem.Decode(\[]byte(pemString))
key, \_ := x509.ParsePKCS8PrivateKey(block.Bytes)
ecKey := key.(\*ecdsa.PrivateKey)

// Firmar
hash := sha256.Sum256(\[]byte(payloadB64))
r, s, \_ := ecdsa.Sign(rand.Reader, ecKey, hash\[:])

// Convertir a raw r||s (32 bytes cada uno para P-256)
rBytes := r.FillBytes(make(\[]byte, 32))
sBytes := s.FillBytes(make(\[]byte, 32))
rawSig := append(rBytes, sBytes...)

// Codificar firma en base64url
signatureB64 := strings.TrimRight(
&#x20;   base64.URLEncoding.EncodeToString(rawSig), "=")
:::

C# (.NET)

:::BlockQuote
using System.Security.Cryptography;
using System.Text;

var payloadRaw = "T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;";

// Codificar en base64url
var payloadB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadRaw))
&#x20;   .Replace("+", "-").Replace("/", "\_").TrimEnd('=');

// Cargar clave privada
using var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(pemString);

// Firmar (IEEE P1363 = raw r||s)
var sig = ecdsa.SignData(
&#x20;   Encoding.UTF8.GetBytes(payloadB64),
&#x20;   HashAlgorithmName.SHA256,
&#x20;   DSASignatureFormat.IeeeP1363FixedFieldConcatenation);

// Codificar firma en base64url
var signatureB64 = Convert.ToBase64String(sig)
&#x20;   .Replace("+", "-").Replace("/", "\_").TrimEnd('=');
:::

Java

:::BlockQuote
import java.security.\*;
import java.security.spec.\*;
import java.util.Base64;

String payloadRaw = "T1=100.00;D1=1775150280;T2=150.00;D2=1775236680;";

// Codificar en base64url
String payloadB64 = Base64.getUrlEncoder().withoutPadding()
&#x20;   .encodeToString(payloadRaw\.getBytes("UTF-8"));

// Cargar clave privada
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(
&#x20;   Base64.getDecoder().decode(pemBase64Content));
KeyFactory kf = KeyFactory.getInstance("EC");
PrivateKey privateKey = kf.generatePrivate(spec);

// Firmar
Signature signer = Signature.getInstance("SHA256withECDSAinP1363Format");
signer.initSign(privateKey);
signer.update(payloadB64.getBytes("UTF-8"));
byte\[] sig = signer.sign();

// Codificar firma en base64url
String signatureB64 = Base64.getUrlEncoder().withoutPadding()
&#x20;   .encodeToString(sig);
:::

### Verificación

Al recibir el QR, el sistema:

1. Lee el campo 54 (primer importe), campo 80.01 (payload base64url) y campo 81.01 (firma base64url)
2. Obtiene la clave pública del certificado activo de la entidad para el message type qr
3. Si la entidad no tiene certificado propio, busca el del operador padre (Payment Facilitator)
4. Verifica la firma ECDSA-SHA256 del payload usando la clave pública
5. Decodifica el payload base64url para obtener los importes y vencimientos
6. Valida que T1 del payload coincida con el campo 54 del QR
7. Selecciona el monto correspondiente según el vencimiento vigente
8. Si todos los importes están vencidos, rechaza la operación

### Ejemplo completo de QR

:::BlockQuote
Campo 00:    01                                     (Payload Format Indicator)
Campo 01:    11                                     (Static QR)
Campo 41:    \[anchor point con datos del administrador]
Campo 50:    \[CUIT del comercio]
Campo 51:    \[CVU/CBU]
Campo 52:    7399                                   (MCC)
Campo 53:    032                                    (ARS)
Campo 54:    100.00                                 (Primer importe)
Campo 58:    AR
Campo 59:    MI COMERCIO
Campo 60:    BUENOS AIRES
Campo 61:    C1082
Campo 62:    \[POS ID + Referencia única]
Campo 80:    01&#x7B;len}&#x7B;payload\_base64url}             (Signed Payload)
Campo 81:    01&#x7B;len}&#x7B;signature\_base64url}            (Signature ECDSA)
Campo 63:    \[CRC]
:::

### Certificados

Los certificados se gestionan desde la consola en la sección **Firmas**. Para QR se utiliza el algoritmo **ECDSA** con curva **P-256** (prime256v1).

Al crear un certificado:

- Se genera un par de claves (pública y privada) ECDSA P-256
- La clave pública se almacena en el servidor para verificación
- La clave privada se entrega al usuario una única vez para que firme los QR desde su dispositivo
- La clave privada es un archivo PEM que debe guardarse de forma segura

Para submerchants operados por un Payment Facilitator: si el submerchant no tiene certificado propio, el sistema utiliza automáticamente el certificado del operador padre.

### Restricciones

| Restricción       | Valor                                        |
| ----------------- | -------------------------------------------- |
| Algoritmo         | ECDSA SHA-256 (único permitido para QR)      |
| Curva             | P-256 / prime256v1 (única permitida para QR) |
| Codificación      | base64url sin padding (RFC 4648 §5)          |
| Formato de firma  | IEEE P1363 (raw r‖s, 64 bytes)               |
| Largo campo 80.01 | ≤ 95 caracteres (payload base64url)          |
| Largo campo 81.01 | 86 caracteres (firma 64 bytes en base64url)  |
| Largo firma       | 86 caracteres base64url (64 bytes raw)       |
| Máximo importes   | 2 (T1/D1 + T2/D2)                            |

