En corto
- La autenticación es mTLS con certificado de cliente. No hay firma XML del mensaje ni credenciales en el sobre SOAP.
- El certificado va en variables de entorno del servidor y nunca dentro de la petición: es la identidad fiscal del obligado.
- La mayoría de los rechazos no son de datos, son de namespace y de prefijo. El código de error es genérico y engaña.
- El envío es todo o nada por lote, y la idempotencia depende de reutilizar el mismo identificador de mensaje en los reintentos.
- La AEAT responde HTTP 200 aunque rechace. Hay tres formas distintas de fallar y el parser tiene que distinguirlas.
- Sin reconciliar contra la consulta paginada no sabes si tu estado local dice la verdad.
- Es una integración abordable, pero no es media tarde. Aquí tienes con qué decidir.
Antes de seguir
Esta página es divulgación técnica sobre cómo funcionan los servicios web de SILICIE, no asesoramiento fiscal ni jurídico. Las referencias normativas están citadas con su artículo para que puedas comprobarlas en el BOE y en la sede electrónica de la AEAT. Contrasta siempre con la versión vigente de la norma y de los esquemas XSD antes de tomar decisiones, y consulta con tu asesor fiscal.
Qué hace falta antes de escribir la primera línea de código
Hay tres cosas que no se resuelven programando y conviene tenerlas resueltas antes. La primera es el CAE del establecimiento. La segunda es el ejercicio contable: se abre en la sede electrónica, no por web service, y la AEAT devuelve un identificador con formato fijo que viaja en la cabecera de todos los mensajes. No se deduce del CAE ni del año. Tu aplicación solo lo vincula y lo valida con una expresión regular antes de guardarlo. La tercera es el certificado electrónico del obligado o de un representante autorizado, en formato .p12 o .pfx.
Con eso ya se puede atacar el entorno de preproducción, que tiene hostname propio. Mi consejo: empieza por el servicio de consulta. Es de solo lectura, no ensucia nada y ya te obliga a acertar con el transporte y con la cabecera, que es donde se cae la mitad de los intentos.
- CAE del establecimiento obligado.
- Identificador de ejercicio contable devuelto por la sede electrónica.
- Certificado de cliente en .p12 o .pfx con su passphrase.
- Los XSD de la versión vigente descargados y leídos, no supuestos.
Cómo se autentica: mTLS con certificado, no usuario y contraseña
La autenticación de SILICIE se resuelve entera en la capa TLS. Abres la conexión presentando un certificado de cliente y ya está. No hay firma XML-DSig del mensaje, no hay credenciales dentro del sobre SOAP. Si vienes de otros servicios de la AEAT, esto despista bastante. Las cabeceras son concretas: Content-Type text/xml con charset UTF-8 y SOAPAction vacío, con sus comillas dobles.
La validación del certificado del servidor se queda activada en los dos entornos. Producción y preproducción tienen certificado de CA pública, así que desactivar la verificación no arregla nada: solo abre la puerta a un intermediario que se coloque en medio de tus presentaciones fiscales. TLS 1.2 como mínimo. Y los errores de conexión se devuelven al usuario sin detalle interno, para no filtrar rutas ni datos del certificado en un log o en una respuesta HTTP.
Por qué el certificado nunca puede viajar en la petición
Es una tentación clásica cuando montas un panel multiempresa: un formulario donde cada cliente sube su .p12 y la ruta lo usa para firmar. No lo hagas.
Un certificado electrónico es la identidad fiscal del obligado. Quien lo tiene presenta en su nombre, y lo que se presenta en SILICIE es contabilidad de impuestos especiales. Si el certificado entra por el cuerpo de una petición, ha pasado por el cliente, por la red, probablemente por un log de proxy y por el cuerpo de una traza de error. El fichero va en variables de entorno del servidor, junto con su passphrase, y solo lo toca el proceso que abre el socket TLS. Ninguna ruta acepta un certificado que llegue de fuera. Ningún endpoint lo devuelve. Ninguna traza lo imprime. Esta es la regla que menos se discute y la que más veces he visto rota en integraciones improvisadas.
Cómo se construye el mensaje: casi todos los errores son de namespace
Aquí es donde se va el tiempo de verdad. La AEAT valida contra XSD y devuelve un código genérico, el 1207, que dice qué nodo esperaba y cuál llegó. El mensaje induce a pensar que sobra un elemento cuando lo que está mal es el prefijo del namespace.
Los esquemas comunes de cabeceras, tipos, listas y errores también cambian de versión. Si migras a los servicios v2 y dejas los comunes apuntando a la ruta de v1, no pasa ni un envío ni una consulta: falla el primer elemento de la cabecera, así que el error ni siquiera habla del asiento. Centraliza todos los namespaces en un único objeto y no dejes literales sueltos por operación.
La regla práctica que me ahorra depurar a ciegas: abre el XSD y mira dónde está declarado el elemento, no de qué tipo es. Si aparece como elemento propio en el esquema de tipos, va con el prefijo de tipos. Si está declarado en línea dentro del complexType del mensaje, va con el prefijo del esquema de entrada, aunque su tipo viva en el otro fichero. Y el esquema de listas no declara elementos: solo contiene enumerados, así que los campos con valores tasados se emiten con el prefijo de tipos, nunca con el de listas.
- Los códigos de movimiento y de justificante se envían con su letra inicial, tal cual figuran en la plantilla oficial. Quitar el prefijo tumba el lote entero con un error que no dice qué formato esperaba.
- En la consulta algunos códigos vuelven sin esa letra, así que al importar hay que reponerla.
- Prueba siempre con un asiento que rellene todos los campos opcionales. Un campo declarado en línea que solo aparece cuando viene relleno pasa las pruebas iniciales y revienta semanas después, en un lote real.
Unidades, fechas y campos que no debes rellenar
La base imponible del impuesto se expresa en mililitros y en gramos (art. 64 sexies.1 Ley 38/1992). Los servicios web, en cambio, trabajan en litros y kilogramos. Presentar la cifra en mililitros creyendo que la unidad es esa multiplica por mil las existencias declaradas, y es uno de los descuadres que detecta la Inspección. La conversión tiene que estar centralizada, con decimales exactos y redondeo definido, y deshacerse al importar de vuelta. El redondeo tampoco es el mismo en los dos canales: el XML del servicio web y el fichero CSV oficial no llevan los mismos decimales.
Las fechas son dato fiscal. Si el proceso corre en UTC, la fecha del servidor y la peninsular no coinciden durante las primeras horas de la madrugada, y una fecha de registro contable puede presentarse con el día anterior. Con plazos contados en horas hábiles eso importa: el suministro general es de veinticuatro horas hábiles desde el movimiento (art. 5.1 Orden HAC/998/2019), y los cinco días hábiles son un régimen opcional que hay que solicitar antes del año natural (art. 6). Calcula siempre en la zona horaria de Madrid.
Último aviso: que un campo esté en el XSD no significa que aplique a tu establecimiento. Los campos del grupo de operaciones de fabricación existen y el servicio los acepta, pero rellenarlos en un depósito fiscal que no fabrica es motivo de reproche. Eso no lo valida el web service, lo valida un inspector meses después.
Lotes, reintentos e idempotencia sin duplicar asientos
Dentro de una llamada el modelo es todo o nada: si un asiento del lote falla, se rechaza el lote completo. Y la AEAT detecta reenvíos por el identificador de mensaje. Si generas uno nuevo en cada reintento, un lote que ya entró se procesa como nuevo y choca con los duplicados por referencia interna. El identificador se genera una vez, se guarda en base de datos y se reutiliza en los reintentos.
Hay un error que se malinterpreta siempre. Cuando reenvías un asiento ya aceptado, la AEAT responde con el código de referencia interna repetida. Eso no es un rechazo: es un me consta. Si tu endpoint marca el lote entero como rechazado, machaca el estado aceptado previo y deja huérfanos los números de asiento que Hacienda sí tiene. La pantalla vuelve a considerarlos pendientes, se reenvían, y bucle. Antes de degradar nada, filtra lo que ya tenga estado aceptado o número de asiento asignado.
Un detalle que tumba tandas enteras: una reintroducción exige el número largo del asiento original. Si el original todavía no está aceptado, no hay número que poner y la AEAT bloquea la presentación completa. Resuelve el mapa de dependencias antes de enviar, aparta lo que no se pueda resolver y ordena el lote por fecha y número para que las dependencias del mismo día salgan en orden.
La AEAT devuelve HTTP 200 aunque rechace
El código HTTP no dice nada. Un envío rechazado llega como 200 con un error funcional dentro del cuerpo. Un error de estructura llega como SOAP Fault. Un XML corrupto revienta el parser. Si das el 200 por bueno, marcas como aceptados asientos que Hacienda no ha registrado, y eso solo se descubre reconciliando.
El parser necesita tres ramas explícitas y necesita forzar el tratamiento como array de los elementos repetibles: muchas librerías devuelven un objeto en lugar de una lista cuando solo hay un elemento, y el código que itera se rompe o lo ignora en silencio. Cuidado también con los nombres de campo entre versiones: si el parser busca el nombre antiguo y el servicio devuelve el nuevo, un guard del tipo si existe el campo entonces guarda hace que el UPDATE se salte sin lanzar ninguna excepción. El síntoma no es un error, es un estado divergente.
Guarda de cada presentación el XML enviado, el recibido y el CSV de verificación que devuelve la AEAT. Cuando llega un requerimiento, o cuando hay que averiguar si un asiento entró de verdad, eso es lo único que vale.
Consulta, anulación y reconciliación contra lo que consta en la AEAT
La consulta cambió bastante en la versión 2: el filtro se aplanó y desaparecieron los envoltorios intermedios. Mi recomendación es no filtrar en el servidor. Los filtros rechazan valores con errores opacos y depurarlos cuesta más que el beneficio. Envía el filtro vacío, pagina el ejercicio entero y filtra en el cliente: es más robusto y permite combinar criterios que el servicio no soporta.
La consulta devuelve un máximo de mil asientos por llamada, con una bandera literal de continuación, y los movimientos recientes están en las últimas páginas. Quien solo pide la primera concluye que faltan asientos. Cada página puede tardar decenas de segundos, así que el timeout del cliente HTTPS y el de la función que lo llama tienen que ser coherentes: si uno corta antes, enmascara al otro y aparece como un fallo intermitente que solo se da cuando el ejercicio crece.
La anulación es el servicio que más iteraciones cuesta. Tiene subcarpeta propia en el namespace, bloques anidados y exige el número de asiento largo que asignó la AEAT, no tu referencia interna. Además la respuesta no trae lista de aceptados, así que si reutilizas el parser del alta el contador dice cero anulados aunque haya ido todo bien. Y anular no borra: la AEAT marca el original como baja y da de alta un asiento nuevo con la anotación de anulación, que si la importas entra como movimiento real y descuadra existencias.
La reconciliación cruza tu numeración local contra la referencia interna que devuelve la AEAT. Por eso renumerar asientos en bloque es destructivo: rompe el cruce para siempre y, si la secuencia retrocede, se reutilizan números.
¿Cuánto trabajo es esto de verdad?
Conviene ser franco, porque ayuda más que vender facilidad. La parte de abrir un socket TLS y mandar un SOAP es media tarde. Lo que cuesta es todo lo demás: leer los XSD elemento a elemento para acertar con los prefijos, montar el motor de lotes idempotente, distinguir los tres modos de fallo, construir la anulación, paginar y reconciliar, y encima meter las validaciones de negocio que el web service no hace pero la Inspección sí mira.
Si tienes un equipo con soltura en XML Schema y SOAP, tiempo para depurar contra preproducción y alguien que entienda la contabilidad de impuestos especiales, es perfectamente abordable dentro de casa. Si no tienes las tres cosas a la vez, lo que suele pasar es que la integración funciona con asientos de prueba y empieza a fallar cuando entra el primer lote real con campos opcionales, devoluciones y anulaciones.
Esto lo he construido dentro de SILICIE VapeTax, mi software para el impuesto sobre líquidos de vapeo y bolsas de nicotina, y cada punto de esta página sale de un error que me devolvió la AEAT y tuve que entender. Si prefieres partir de algo que ya presenta, esa es la alternativa.
Preguntas frecuentes
¿Hay entorno de pruebas para SILICIE?+
Sí. La AEAT publica un entorno de preproducción con hostname propio y los mismos esquemas. Se accede con el mismo certificado de cliente. Ojo: la preproducción sirve para validar estructura y transporte, no para reproducir el estado real de tu ejercicio contable. Antes de dar por buena la integración, prueba con un asiento que rellene todos los campos opcionales, no solo con asientos limpios.
¿Necesito firmar el XML con XML-DSig?+
No. La autenticación de SILICIE va entera en la capa de transporte, con certificado de cliente mTLS en formato .p12 o .pfx. No hay firma del mensaje ni credenciales dentro del sobre SOAP. Es una diferencia frente a otros servicios de la AEAT y despista a quien llega de ahí.
¿Puedo abrir el ejercicio contable por web service?+
No. El ejercicio contable se abre en la sede electrónica y la AEAT devuelve un identificador de formato fijo. Ese identificador es un dato de entrada obligatorio en la cabecera de todos los mensajes: alta, consulta y anulación. No se puede deducir del CAE ni del año. Tu aplicación solo lo vincula y lo valida.
¿Qué pasa si reenvío un asiento que la AEAT ya aceptó?+
La AEAT responde con el código 111001, referencia interna repetida para ese CAE. Eso no es un rechazo: significa que ya lo tiene. Si tu código lo trata como error y degrada el estado local a rechazado, pierdes el número de asiento que la AEAT te asignó y entras en un bucle de reenvíos. Nunca degrades un asiento que ya tenga número de asiento de la AEAT.
¿Cómo compruebo que lo que tengo en mi sistema coincide con lo que consta en Hacienda?+
Con el servicio de consulta, pidiendo el ejercicio completo con filtro vacío, paginando hasta que la bandera de continuación deje de venir y cruzando por la referencia interna, que es tu propia numeración de asiento. Hay que filtrar los asientos con estado de baja y las anotaciones de anulación, que la AEAT da de alta como asientos nuevos y ensucian el recuento si las importas.
¿Se puede anular un asiento presentado?+
Sí, con el servicio de anulación, y para eso hace falta el número de asiento largo que la AEAT asignó al presentarlo, no tu referencia interna. Si no lo guardaste, no puedes anular ni por web service ni por la sede. Guardar ese número desde el primer envío es la decisión de diseño más barata y más rentable de toda la integración.
¿Lo integras tú o te lo hago yo?
Si estás evaluando integrarte por servicio web y quieres una opinión técnica sobre el alcance real en tu caso, escríbenos. Te decimos con franqueza si compensa hacerlo con tu equipo interno o si sale mejor delegarlo, y qué partes son las que de verdad consumen tiempo.
Hablamos de tu integración