Error 0160 de SIFEN: «XML mal formado» cuando el XML está perfecto
Respuesta corta: el 0160 significa que tu XML no valida
contra el XSD de la SET. Tu editor te dice que está bien formado y es verdad — «bien
formado» y «válido contra el esquema» son cosas distintas.
La causa que a nadie se le ocurre: un salto de línea dentro de un texto
libre. Los tipos string del XSD llevan el patrón
.*[^\s].*, y en XML Schema el punto no casa saltos de
línea. Un \n en la descripción de un ítem tumba el documento
entero. No es un problema de longitud.
Por qué duele tanto
La SET devuelve un solo error bloqueante por intento. Arreglás uno y aparece el siguiente. Eso convierte la integración en una escalera a ciegas, y hace que valga oro saber de antemano cuáles son los peldaños.
El 0160 es el primero de todos, porque la validación de esquema va antes que
cualquier regla de negocio. Mientras no lo pases, la SET no te va a contar nada más.
La causa que nadie espera: un salto de línea
En el XSD de la SET, los campos de texto libre no son string a secas.
Heredan de un tipo llamado noEmptyString, y esta es su definición completa,
tal cual está en DE_Types_v150.xsd:
<xs:simpleType name="noEmptyString">
<xs:annotation>
<xs:documentation>Cadena no vacia</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:whiteSpace value="preserve" />
<xs:pattern value=".*[^\s].*" />
<xs:minLength value="1" />
</xs:restriction>
</xs:simpleType>
Las dos líneas resaltadas son el mecanismo completo del rechazo, y hay que leerlas juntas:
-
whiteSpace="preserve"le dice al validador que no toque el espacio en blanco. Si fueracollapse, el salto de línea se convertiría en un espacio antes de comparar y no habría problema. Al preservarlo, el\nllega vivo hasta el patrón. -
.*[^\s].*quiere decir «que haya al menos un carácter que no sea espacio». La intención es razonable: impedir campos llenos de espacios. Pero en XML Schema el punto no casa saltos de línea, y no existe un modificador «dotall» que se pueda activar. Así que en cuanto el valor contiene un\n, el patrón deja de casar y el documento no valida.
Y no es un campo aislado: en la versión 150 del esquema hay 79 campos que
heredan de noEmptyString — la descripción del ítem, la información adicional,
el número de lote, el chasis, el color… Cualquiera de ellos con un salto de línea produce
el mismo rechazo.
Y ese \n aparece solo, sin que nadie lo escriba a mano. El caso típico: un
ERP que guarda el nombre del producto y su descripción en campos separados y los
concatena con un salto de línea al armar la línea de la factura. En el sistema se ve
perfecto. En el XML es un rechazo.
No es longitud. Es el error de diagnóstico más común acá: se asume que
el texto es demasiado largo y se recorta. dDesProSer admite
2000 caracteres. Recortar no arregla nada y hace perder horas.
Cómo se arregla
Aplanando los espacios en blanco de todo texto libre justo antes de serializar. No solo la descripción del ítem: también la razón social, la dirección y cualquier observación — en todos aparece el mismo patrón en el XSD.
# Ruby, pero la idea es la misma en cualquier lenguaje:
# colapsar todo espacio en blanco (incluidos \n, \r y \t) a un espacio simple.
def texto_sifen(valor)
valor.to_s.gsub(/\s+/, " ").strip
end
Ojo con dos atajos que no alcanzan: strip a secas solo limpia los extremos y
deja el salto del medio, y reemplazar \n por "" pega las dos
palabras («ProductoDescripción»). Colapsar a un espacio simple es lo que da un texto que
además se lee bien en el KuDE.
Cómo se verifica
Sin mandar nada a la SET: validá el XML contra el XSD oficial en tu propia máquina. Es la diferencia entre averiguarlo en un segundo y averiguarlo en un viaje de ida y vuelta al servicio web.
# con xmllint (viene con libxml2)
xmllint --noout --schema DE_v150.xsd mi-documento.xml
# y un test que falle a propósito, para que no vuelva a pasar
assert_equal "Producto Descripción larga",
texto_sifen("Producto\nDescripción larga")
Las otras causas de 0160 que nos tocaron
Todas verificadas contra la SET, en orden de cuánto cuesta darse cuenta:
| Qué lo dispara | Por qué |
|---|---|
Código de actividad económica con el prefijo de pantalla, tipo C4_62010 |
El tipo del XSD es [0-9A-Z]{1,8}: el guion bajo no está permitido. Va el número CIIU pelado, 62010. Ese C4_ es formato de la pantalla de Marangatu, no del documento |
Un identificador de envío en cero (dId) |
Nos pasó por un recorte de cadena que devolvía nil y terminaba enviando 0. El servicio síncrono lo tolera; el de lote responde 0160. Si venís del síncrono, esto aparece justo al migrar |
| El sobre del lote mal armado | El elemento raíz es rEnvioLote —no el que sugiere el nombre del servicio—, el rLoteDE va sin su propio xmlns, y el fichero dentro del ZIP tiene que llamarse xml_file.xml |
Cabecera Content-Type equivocada |
El envío de lote espera application/xml; charset=utf-8. Y en el ambiente de test, la consulta de RUC exige text/xml y rechaza el application/soap+xml estándar — producción sí lo acepta. Misma operación, distinto ambiente |
En eventos, los atributos xsi en el nodo equivocado |
Van en gGroupGesEve, no en rGesEve. Colgados del nodo de más arriba, 0160 |
La conclusión práctica
El 0160 no se debuguea contra la SET, se debuguea contra el XSD. Los esquemas
son públicos y están en la documentación técnica del SIFEN: si los tenés en el proyecto y
validás antes de enviar, la mitad de esta lista deja de existir para vos.
Y cuando un rechazo te frene: un rechazo no consume el número ni el CDC. Se corrige y se reenvía el mismo documento. Vale saberlo antes de que entre el pánico.
Si además te toca una fecha de obligatoriedad: cómo se pide la prórroga y quién puede.
Verificado el 26 de julio de 2026 contra el Manual Técnico del SIFEN versión 150 y los XSD de esa misma versión, con documentos reales aprobados por la SET. La documentación técnica se actualiza: comprobá la versión vigente antes de dar algo por hecho.
Esto es información técnica de integración, no asesoramiento tributario. Para el criterio fiscal de tu caso, hablá con tu contador.