Saltar al contenido principal

SIFEN QR — Referencia Técnica v150

Fuente primaria: Manual Técnico SIFEN v150, Capítulo 13 (KuDE), §13.8.3–13.8.4 Fuente secundaria: Código Java de referencia rshk-jsifen-maven (DocumentoElectronico.java, SifenUtil.java) Última revisión: 2026-03-16


1. Descripción del Código QR

El QR (campo dCarQR en gCamFuFD) es una URL que permite al receptor consultar el DTE en el portal e-kuatia de la SET. Se genera después de firmar el XML porque depende del DigestValue de la firma.

El QR no forma parte del bloque firmado (gCamFuFD está fuera de <DE>), por lo que inyectarlo post-firma no invalida la firma digital.


2. URL Base por Ambiente

AmbienteURL base
Testhttps://ekuatia.set.gov.py/consultas-test/qr?
Producciónhttps://ekuatia.set.gov.py/consultas/qr?

3. Parámetros de la URL — Orden Obligatorio

El orden es fijo y mandatorio. Cambiar el orden altera el hash cHashQR.

#ParámetroFuente en XMLLong.HexNotas
1nVersionConstante3NoSiempre "150"
2IdDE@Id (CDC)44NoCDC completo incluyendo DV
3dFeEmiDEgDatGralOpe/dFeEmiDE38*Hex UTF-8 de yyyy-MM-dd'T'HH:mm:ss
4adRucRecgDatRec/dRucRec≤20NoSolo si iNatRec=1 (contribuyente)
4bdNumIDRecgDatRec/dNumIDRec≤20NoSi iNatRec=2 y no es exportación
4cdNumIDRec"0"1NoSi exportación (iTiOpe=4) o sin receptor
5dTotGralOpegTotSub/dTotGralOpe≤23No"0" si iTiDE=7 (remisión)
6dTotIVAgTotSub/dTotIVA≤23NoSolo si iTImp=1 o 5; si no → "0"
7cItemsConteo de gCamItem≤3NoNo incluido en el XML del DE (calculado)
8DigestValueSignature/SignedInfo/Reference/DigestValue88*Ver §4
9IdCSCConfiguración emisor4NoSiempre 4 dígitos con ceros: leftPad('0', 4)
10cHashQRCalculado64NoSHA-256 hex de params(1-9) + CSC. No incluido en el hash

* Para SHA-256: dFeEmiDE siempre 38 chars, DigestValue siempre 88 chars.


4. DigestValue — Codificación Correcta (CRÍTICO)

Regla del Manual (§13.8.3)

"Los siguientes campos deben ser convertidos a su equivalente hexadecimal: Fecha de Emisión y DigestValue de la Firma Digital"

El manual convierte la cadena Base64 (incluyendo el padding =) a hexadecimal, NO los bytes crudos.

Ejemplo del Manual (pág. 207)

Entrada (Base64)Salida (Hex)Chars
yzGYhUx1/XYYzksWB+fPR3Qc50c=797a4759685578312f5859597a6b7357422b6650523351633530633d56

Verificación: bin2hex('yzGYhUx1/XYYzksWB+fPR3Qc50c=') = 797a...633d ✓ (28 chars × 2 = 56 chars hex)

Para SHA-256 (32 bytes)

DigestValue XML (Base64): "Ee+Ot6s4WYE+lXV9yf2S39lgx2LYeIJKLZBwbu2daPY=" (44 chars)
DigestValue hex para QR: "45652b4f..." (88 chars)

44 chars de Base64 × 2 = 88 chars hex — siempre para SHA-256.

PHP correcto

// El $digestB64 viene del XML: <DigestValue>Ee+Ot6s4...</DigestValue>
$digestB64 = trim($digestNode->textContent);

// ✅ CORRECTO — convierte los bytes ASCII del string Base64 a hex (88 chars para SHA-256)
$digestHex = bin2hex(base64_encode((string) base64_decode($digestB64)));
// Equivalente simplificado (base64_decode + base64_encode = identidad):
// $digestHex = bin2hex($digestB64);

// ❌ INCORRECTO — hex de los bytes crudos del hash (produce 64 chars)
// $digestHex = bin2hex(base64_decode($digestB64));

// ❌ INCORRECTO — incluir el Base64 crudo sin hexificar
// $digestHex = $digestB64;

Java de referencia (SifenUtil.java)

// getDigestValue() retorna byte[] crudos (32 bytes SHA-256)
byte[] rawDigest = ((Reference) signedInfo.getReferences().get(0)).getDigestValue();

// encode() convierte a string Base64 y lo retorna como byte[] (bytes del string ASCII)
byte[] base64Bytes = Base64.getEncoder().encode(rawDigest);

// bytesToHex convierte esos bytes ASCII a hex lowercase → 88 chars
String digestValue = SifenUtil.bytesToHex(base64Bytes);

5. Fecha dFeEmiDE — Hex de UTF-8

"2026-03-12T10:00:00" → bin2hex() → "323032362d30332d31325431303a30303a3030"
  • Siempre 38 caracteres hex (19 chars ASCII × 2)
  • Formato: yyyy-MM-dd'T'HH:mm:ss (sin timezone, sin milisegundos)
// ✅ CORRECTO
$dFeEmiDE = bin2hex($dte->fecha_emision->format('Y-m-d\TH:i:s'));

6. Construcción del cHashQR

Paso 1 — urlParamsString (sin cHashQR):
"nVersion=150&Id={CDC}&dFeEmiDE={hex}&dRucRec={ruc}&dTotGralOpe={n}&dTotIVA={n}&cItems={n}&DigestValue={88hex}&IdCSC={4d}"

Paso 2 — concatenar CSC directamente (SIN "&" separador):
input = urlParamsString + CSC
ejemplo: "...&IdCSC=0001ABCD0000000000000000000000000000"

Paso 3 — SHA-256 hex lowercase:
cHashQR = hash('sha256', input) // 64 chars lowercase

Reglas críticas:

  • Sin & entre IdCSC=XXXX y el CSC
  • Sin trailing & en urlParamsString antes del hash
  • Construir el query string manualmente con implode('&', ...) — NO usar http_build_query (URL-encodea - y +)
// ✅ CORRECTO — construcción manual del query string
$parts = [];
foreach ($params as $k => $v) {
$parts[] = "{$k}={$v}";
}
$urlParams = implode('&', $parts);

$cHashQR = hash('sha256', $urlParams . $csc); // lowercase por defecto en PHP
$url = $baseUrl . '?' . $urlParams . '&cHashQR=' . $cHashQR;

7. Ejemplo Completo del Manual (§13.8.4)

Datos de entrada

ParámetroValor
nVersion150
Id0144444440170010010014528220170125158732 60988
dFeEmiDE2017-01-25T09:35:17
dRucRec88899990
dTotGralOpe300000
dTotIVA27272
cItems2
DigestValue (Base64)yzGYhUx1/XYYzksWB+fPR3Qc50c=
DigestValue (Hex)797a4759685578312f5859597a6b7357422b6650523351633530633d
IdCSC0001
CSCABCD0000000000000000000000000000

urlParamsString (Paso 1)

nVersion=150&Id=01444444401700100100145282201701251587326098 8&dFeEmiDE=323031372d30312d32355430393a33353a31 37&dRucRec=88899990&dTotGralOpe=300000&dTotIVA=27272&cItems=2&DigestValue=797a4759685578312f5859597a6b7357422b6650523351633530633d&IdCSC=0001

Concatenar CSC (Paso 2)

...&IdCSC=0001ABCD0000000000000000000000000000
↑ sin "&", CSC pegado directo

SHA-256 (Paso 3)

cHashQR = 97ddbb3c1e7d65af03a70ffe21f2b34846ab1c89e0566c35222086766b7374ed

8. Checklist de Diagnóstico — Error "QR no coincide"

#VerificaciónHerramienta
ADigestValue en URL tiene 88 chars (SHA-256)strlen($digestHex) === 88
BdFeEmiDE en URL tiene 38 charsstrlen($dFeEmiDE) === 38
CIdCSC tiene exactamente 4 dígitos con cerosstrlen($idCsc) === 4
DCSC en DB es el real (no placeholder)Consultar portal SIFEN → Mis Datos → CSC
ENo hay & entre urlParams y CSC en el hashRevisar concatenación
FNo hay trailing & al final de urlParamsUsar implode('&', ...)
GHex en minúsculashash('sha256', ...) es lowercase por defecto en PHP
HOrden de parámetros es el correctoVer tabla §3
IdTotIVA = "0" cuando iTImp ≠ 1 y ≠ 5Revisar lógica de impuesto
JRUC receptor incluye guión-DV si iNatRec=1"44444401-7" no "44444401"

9. Causa más probable del error en test

El CSC almacenado en DB es un placeholder ABCD0000000000000000000000000000.

SIFEN recalcula SHA-256(urlParams + CSC_real) y lo compara con cHashQR. Si el CSC es incorrecto, el hash nunca coincide.

Solución:

// Obtener CSC real del portal SIFEN (Mis Datos → Código de Seguridad del Contribuyente)
php artisan tinker --execute="
\$e = \App\Domains\Sifen\Models\SifenEmitter::find(1);
\$e->csc = 'CSC_REAL_DE_32_CHARS_AQUI';
\$e->save();
echo \$e->csc;
"

10. Inserción en el XML (§13.8.4.5)

El & de la URL se debe escapar como &amp; en el XML. PHP lo hace automáticamente al usar createTextNode() en DOMDocument:

// ✅ createTextNode escapa & → &amp; automáticamente
$dCarQrNode->appendChild($dom->createTextNode($urlQr));

// ❌ NO hacer esto (doble escapado):
$dCarQrNode->textContent = htmlspecialchars($urlQr, ENT_XML1);

En el XML firmado el resultado correcto es:

<dCarQR>https://...?nVersion=150&amp;Id=...&amp;cHashQR=...</dCarQR>