Saltar al contenido
Todos los artículos
dgiiJuan T. Pérez L.

7 trampas que rebotan tu certificación e-CF DGII (y cómo evitarlas)

Los 7 errores más comunes que hacen rebotar la certificación como Emisor de e-CF en DGII: razón social mal escrita, NCF Modificado faltante, QR Consulta Timbre con formato incorrecto, CodigoSeguridad mal computado, bug de signxml en Paso 8, RNC del emisor erróneo, ACECFs no encontrados. Cada uno con síntoma, causa y fix exacto.

Si estás certificándote como Emisor de e-CF en DGII y te aparece "Rechazado" en el portal sin mucha explicación, este post es para tú. Documentamos los 7 errores más comunes que vimos durante nuestra propia certificación (RNC 132901322, mayo 2026), cada uno con el síntoma exacto, la causa real y cómo se arregla.

No es una lista hipotética — cada uno de los 7 fue un rechazo real que tuvimos que diagnosticar y solucionar en 14 días. Algunos los descubrimos en horas; el peor nos costó 12 horas de sesión continua.

Trampa #1 — Razón Social en minúsculas o sin nombre comercial

Síntoma: después del Paso 5 (subir las 11 Representaciones Impresas), el auditor de DGII te rebota con un mensaje similar a:

"Le recordamos que la Razón Social del emisor debe coincidir con su RNC. RAZÓN SOCIAL TAL CUAL ESTÁ EN EL RNC DEL CONTRIBUYENTE (ADICIONAL EL NOMBRE COMERCIAL) EN LAS RI."

Causa: DGII compara la razón social impresa en tu PDF letra-por-letra contra su registro de RNC, que casi siempre está en mayúsculas. Si tu sistema usa el legal_name en formato "NexConnect Solutions SRL" (camel case), la comparación falla. Adicionalmente, DGII espera ver el nombre comercial encima de la razón social, no debajo ni ausente.

Fix:

  • En el generador de RIs, formatear la razón social como legal_name.upper() para igualar el formato del registro RNC.
  • Agregar el nombre_comercial del emisor (campo separado del RNC) como header en negrita encima de la razón social. Si tu sistema no tiene el nombre comercial como campo independiente, debes agregarlo al modelo de datos del emisor.
  • Ejemplo del bloque emisor correcto:
NEXCONNECT SOLUTIONS      ← nombre comercial, bold
NEXCONNECT SOLUTIONS SRL  ← razón social, mayúsculas exactas
Carretera Luperón Km 7    ← dirección
RNC: 132-90132-2          ← RNC formato xxx-xxxxx-x

Trampa #2 — NCF Modificado faltante en Nota Débito/Crédito

Síntoma: después de R1 corregido, vuelve a rebotar con:

"Nota de Débito y Nota de Crédito debe tener el NCF afectado. Colocar el e-NCF afectado en la RI conforme al formato de factura establecido por DGII."

Causa: los e-CF tipo 33 (Nota Débito) y 34 (Nota Crédito) deben mostrar en su Representación Impresa el NCF original que están modificando, en un slot específico del header. Si tu template de RI muestra "Fecha de Vencimiento" en esa posición (como sería normal en una factura tipo 31), DGII te rebota porque para tipos 33-34 ese slot va para NCFModificado.

Fix: en el generador de RIs, ramificar el header según ecf_type:

  • Tipos 31, 32, 41, 43, 44, 45, 46, 47: header muestra "Fecha Vencimiento Secuencia".
  • Tipos 33, 34: header muestra NCF Modificado: E32XXXXXXXXXX + leyenda "Corrige montos del NCF modificado".

El XML del e-CF tiene la información: el elemento InformacionReferencia/NCFModificado (junto con FechaNCFModificado y RazonModificacion). Tu PDF debe extraerlo de ahí.

Trampa #3 — QR Consulta Timbre con formato URL incorrecto

Síntoma: R3 rebota con:

"Las RI no han podido ser validadas debido a que al momento de realizar la lectura del código QR, presenta mensaje de '¿No fue encontrada la factura (e-CF)?'. Verificar construcción del código QR conforme a las documentaciones técnicas."

Causa: el QR Consulta Timbre tiene reglas específicas que la mayoría de implementaciones erran:

  • CamelCase exacto en los nombres de parámetros: RncEmisor, RncComprador, ENCF, FechaEmision, MontoTotal, FechaFirma, CodigoSeguridad. Si usas rncemisor o RNCEmisor o rnc_emisor falla.
  • Host correcto por tipo de e-CF:
  • Para tipos 31, 32 ≥RD$250mil, 33, 34, 41, 43, 44, 45, 46, 47: ecf.dgii.gov.do/ecf/ConsultaTimbre con los 7 parámetros arriba.
  • Para tipo 32 \<RD$250mil exclusivamente: fc.dgii.gov.do/eCF/ConsultaTimbreFC con sólo 4 parámetros (RncEmisor, ENCF, MontoTotal, CodigoSeguridad).
  • Path en CamelCase: /ecf/ConsultaTimbre, no /consultatimbre ni /ConsultaTimbre.
  • Encoding correcto: espacios como %20 (no +), dos-puntos sin encodear (:), / sin encodear.

Fix: generar el URL del QR con un branch sobre (ecf_type, monto_total):

def build_qr_url(ecf_type, ncf, rnc_emisor, rnc_comprador,
                 fecha_emision, fecha_firma, monto_total, codigo_seguridad):
    if ecf_type == 32 and monto_total < 250000:
        # tipo 32 <250k: host distinto, 4 parámetros
        base = "https://fc.dgii.gov.do/eCF/ConsultaTimbreFC"
        params = {
            "RncEmisor": rnc_emisor,
            "ENCF": ncf,
            "MontoTotal": f"{monto_total:.2f}",
            "CodigoSeguridad": codigo_seguridad,
        }
    else:
        # resto: host normal, 7 parámetros CamelCase
        base = "https://ecf.dgii.gov.do/ecf/ConsultaTimbre"
        params = {
            "RncEmisor": rnc_emisor,
            "RncComprador": rnc_comprador,
            "ENCF": ncf,
            "FechaEmision": fecha_emision,
            "FechaFirma": fecha_firma,
            "MontoTotal": f"{monto_total:.2f}",
            "CodigoSeguridad": codigo_seguridad,
        }
    return f"{base}?{urlencode(params, quote_via=quote)}"

El QR debe ser escaneable a tamaño físico de impresión — mínimo 25mm × 25mm, ≥150 DPI, ECC level L. El auditor de DGII escanea cada QR con un celular real.

Trampa #4 — CodigoSeguridad computado como hash

Síntoma: R4 rebota con un mensaje similar a R3 ("no fue encontrada la factura"). Cuando haces debugging, descubrís que escanear el QR con un celular muestra "factura no encontrada" — pero los demás parámetros (RNC, NCF, fecha, monto) coinciden con un e-CF que DGII tiene aceptado en su sistema.

Causa: el CodigoSeguridad que la mayoría de proveedores computa erróneamente como SHA-256(xml_signed)[:6] o algo similar. Pero no es un hash — son literalmente los primeros 6 caracteres del <ds:SignatureValue> del XML firmado, sin transformación.

Fix:

def compute_codigo_seguridad(xml_signed: bytes) -> str:
    """Extract the first 6 chars of the literal ds:SignatureValue."""
    from lxml import etree
    DSIG_NS = "http://www.w3.org/2000/09/xmldsig#"
    root = etree.fromstring(xml_signed)
    sv = root.find(f".//{{{DSIG_NS}}}SignatureValue")
    if sv is None or not sv.text:
        raise ValueError("Missing ds:SignatureValue in signed XML")
    # The SignatureValue is base64 with possible whitespace — strip and take first 6.
    return sv.text.strip().replace("\n", "")[:6]

Implicación crítica: si tu sistema firma el XML múltiples veces (por ejemplo, una al emitir y otra al generar el PDF), el CodigoSeguridad cambia entre firmas (porque la firma incluye el FechaHoraFirma que se setea cada vez). Tienes que persistir el XML firmado de Paso 4 y usarlo verbatim al generar la RI en Paso 5. Si lo re-firmas, el QR apunta a un código que DGII no tiene indexado y rebota.

Trampa #5 — signxml rechaza firmas válidas de DGII (Paso 8)

Síntoma: en Paso 8 (Inicio Autenticación), DGII te envía un XML semilla firmado con su certificado Viafirma TEST. Tu sistema corre signxml.verify(seed_signed) y devuelve InvalidSignature. Pero si validás manualmente con cryptography puro, la firma matemáticamente es válida.

Causa: signxml tiene un bug interno en su pipeline de canonicalización — no maneja bien algunos casos de canonicalización exclusiva c14n cuando hay múltiples elementos con namespaces heredados, que es exactamente lo que la herramienta TypeScript oficial de DGII produce. La firma es correcta; el verificador es el que falla.

Fix: bypass de signxml. Verificar manualmente usando cryptography puro:

def manual_verify_dgii_signature(xml_bytes: bytes, cert) -> bool:
    from io import BytesIO
    from lxml import etree
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    from cryptography.exceptions import InvalidSignature
    import base64, hashlib

    DSIG_NS = "http://www.w3.org/2000/09/xmldsig#"
    root = etree.fromstring(xml_bytes)
    sig = root.find(f".//{{{DSIG_NS}}}Signature")
    if sig is None:
        return False

    # 1. Compute reference digest (SHA-256 of c14n(document - Signature))
    root_copy = etree.fromstring(xml_bytes)
    sig_copy = root_copy.find(f".//{{{DSIG_NS}}}Signature")
    sig_copy.getparent().remove(sig_copy)
    buf = BytesIO()
    etree.ElementTree(root_copy).write_c14n(buf, exclusive=False)
    expected_digest = base64.b64decode(
        sig.find(f".//{{{DSIG_NS}}}DigestValue").text.strip()
    )
    if hashlib.sha256(buf.getvalue()).digest() != expected_digest:
        return False

    # 2. Verify RSA signature over c14n(SignedInfo)
    si = sig.find(f".//{{{DSIG_NS}}}SignedInfo")
    # Fresh subtree extraction — materializes inherited xmlns as attrs
    si_fresh = etree.fromstring(etree.tostring(si))
    buf2 = BytesIO()
    etree.ElementTree(si_fresh).write_c14n(buf2, exclusive=False)
    sig_value = base64.b64decode(
        sig.find(f".//{{{DSIG_NS}}}SignatureValue").text.strip()
    )
    try:
        cert.public_key().verify(
            sig_value, buf2.getvalue(),
            padding.PKCS1v15(), hashes.SHA256(),
        )
        return True
    except InvalidSignature:
        return False

La parte crítica es "fresh subtree extraction + inclusive c14n" para SignedInfo — fue la única combinación que matchó los bytes que DGII firmó tras probar 12 variantes.

Trampa #6 — RNCEmisor del JWT en lugar del cuerpo del e-CF

Síntoma: en Paso 9 (Inicio Recepción), DGII envía e-CFs a tu endpoint. Tu sistema los acepta y responde con ARECF firmado. Pero el portal DGII muestra: "El acuse de recibo no es válido."

Causa: tu sistema toma el RNCEmisor para la respuesta ARECF del JWT que devolvió Paso 8 (el campo sub). Pero el sub del JWT contiene el subject-serial del certificado, que no siempre es un RNC. Para certificados emitidos a personas naturales (cédula), el sub es de la forma IDCDO-XXXXXXXXXXXXX (17 caracteres). El validador XSD de DGII espera RNCType (numérico de 9 o 11 dígitos) y rechaza cualquier valor que no haga match con esa regex.

Fix: extraer el <RNCEmisor> del cuerpo del e-CF que DGII te envió, no del JWT:

def extract_rnc_emisor(xml_bytes: bytes) -> str:
    """Pull <RNCEmisor> from the inbound e-CF body, not the JWT sub."""
    from lxml import etree
    root = etree.fromstring(xml_bytes)
    el = root.find(".//RNCEmisor")
    if el is None or not el.text:
        # Fallback to xpath with local-name() for namespaced docs
        els = root.xpath("//*[local-name()='RNCEmisor']")
        el = els[0] if els else None
    if el is None or not el.text:
        raise ValueError("Missing <RNCEmisor> in inbound e-CF")
    return el.text.strip()

Después, usar este RNC al construir el ARECF, no el jwt_payload["sub"].

Trampa #7 — ACECF para eNCF no recibido en Paso 9

Síntoma: en Paso 10 (Inicio Aprobación Comercial), DGII envía ACECFs a tu endpoint para que los firmes y devuelvas. Algunos de esos ACECFs corresponden a eNCFs que tu sistema nunca recibió en Paso 9. Tu lógica devuelve 404 ("invoice not found") porque buscas el ReceivedInvoice en tu base de datos y no existe.

Causa: DGII no envía exactamente los mismos eNCFs en Paso 9 y Paso 10. Su sistema de pruebas puede generar nuevos eNCFs (especialmente tipo 45 — Gubernamental) que aparecen sólo en Paso 10. Devolver 404 fue una lectura literal de la spec, pero DGII espera que tu sistema sea más resiliente.

Fix: implementar lookup-or-create. Si el ACECF entrante referencia un eNCF que no existe en tu base, generás un ReceivedInvoice placeholder a partir de los datos del ACECF:

received = ReceivedInvoice.objects.filter(
    organization=organization,
    rnc_emisor=rnc_emisor_from_xml,  # ← extracted from inbound, see Trampa #6
    encf=encf,
).first()

if received is None:
    # Auto-create placeholder from inbound ACECF body
    received = ReceivedInvoice.objects.create(
        organization=organization,
        rnc_emisor=rnc_emisor_from_xml,
        rnc_comprador=rnc_comprador_from_xml,
        encf=encf,
        ecf_type=int(encf[1:3]),  # E45 -> 45
        fecha_emision=timezone.now().date(),
        razon_social_emisor=cert_cn or "DGII TEST",
        monto_total_cents=0,
        raw_xml=xml_bytes.decode("utf-8", errors="replace"),
        arecf_estado=ReceivedInvoice.ARECFEstado.RECIBIDO,
    )

# Now build + sign + return ACECF response

Importante para producción: una vez certificado, deberías reconsiderar este lookup-or-create — en producción, recibir un ACECF para un eNCF que nunca emitiste podría ser una señal de fraude o sistema mal configurado. Considerá loggear estos casos y revisar manualmente. Pero durante la certificación, devolver 404 te traba.

Cómo evitar las 7 trampas

La forma corta: usar un proveedor que ya pasó por ellas. Los 7 errores arriba no son hipotéticos — los vivimos en mayo de 2026 durante nuestra propia certificación. Cada uno está parchado en el stack productivo de NexConnect, y nuestros clientes parten desde la línea de llegada en vez de descubrirlos uno por uno.

La forma larga: si vas a construir tu propio receiver y emisor, valida cada uno de estos 7 puntos contra tu implementación antes de tocar el portal de DGII. Cada rebote en Paso 6 te cuesta 3-7 días. Cada bug en Paso 8 te cuesta horas.

Si quieres ahorrarte ambas cosas, escríbenos a ventas@nexconnect.do. O lee el post sobre la maratón de 12 horas donde detallamos la sesión en la que resolvimos las trampas 5, 6 y 7 en una sola noche.


Cada una de las 7 trampas está documentada con código que pasó certificación real con DGII en mayo de 2026. Si te encuentras con un escenario que se parece pero no termina de hacer match con el síntoma de ninguno de los 7, contáctanos — vimos varios casos atípicos durante el proceso.

Usamos cookies para entender cómo navegas el sitio y mejorar la experiencia. No vendemos tus datos. Política de privacidad.