Skip to main content

开发文档

所有文档

官方对接地址 文件统计

测试环境(带证书) 

发送票务地址:  

https://prewww10.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP

生产环境 (带证书)

发送票务地址:

https://www10.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP


<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<!-- 税务制度代码(01=普通IVA) -->
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<!-- 操作类型 -->
<sum1:TipoImpositivo>4</sum1:TipoImpositivo>
<!-- 税率4% -->
<sum1:BaseImponibleOimporteNoSujeto>10</sum1:BaseImponibleOimporteNoSujeto>
<!-- 不含税金额 -->
<sum1:CuotaRepercutida>0.4</sum1:CuotaRepercutida>
<!-- 税额 -->
</sum1:DetalleDesglose

常用节点意思介绍
节点 含义 可用值 / 格式 说明
<sum1:Subsanacion> 修正标志 S 或 省略 S 表示此记录为修正(例如纠正错误或重新提交)
<sum1:TipoFactura> 发票类型 F1 普通发票F2 简易发票R1 作废R2 更正R3 替换 可以参见 TipoFactura 段落

<sum1:ClaveRegimen> 税制代码 01 普通制度02 简易制度03 免税 取决于发票适用税法
<sum1:CalificacionOperacion> 交易类型 S1 受IVA约束N1 不受约束E1 免税 常见餐饮类一般使用 S1
<sum1:TipoImpositivo> 税率 4, 10, 21 , 0  几种税率 西班牙标准IVA税率
<sum1:TipoHuella> 指纹类型 01 表示 SHA-256 Base64
<sum1:TipoUsoPosibleSoloVerifactu> 是否专属VeriFactu SN 如果你的系统只用于VeriFactu,则为 S
<sum1:TipoUsoPosibleMultiOT> 是否支持多义务人 SN SaaS类一般 S
<sum1:IndicadorMultiplesOT> 是否有多个义务人使用中 SN 如果服务器下多家餐厅,则为 S
<sum1:NumeroInstalacion> 安装编号 数字或字符串 每个部署/门店唯一
<sum1:Huella> 指纹 Base64 编码字符串 SHA-256(关键字段串联)
TipoFactura 的几种类型
Código 名称 适用情形 关键点
F1 Factura completa
(Art. 6, 7.2 y 7.3 del RD 1619/2012)
标准完整发票,识别买方、含增值税细节。 是最常见的发票类型。必须包含:
- 客户身份(NIF / NombreRazon)
- 分项税额 (Desglose)
F2 Factura simplificada
(Art. 6.1.d RD 1619/2012)
金额较小、无需识别买方的发票(小票、餐饮等)。 通常对应 Ticket。买方信息可选。
F3 Factura emitida en sustitución de facturas simplificadas 当企业把之前已申报的简易发票(F2)替换为完整发票(F1)。 属于更正性质的“补开发票”,用于“由 ticket 转发票”。这正是你现在在做的情况。
R1 Factura rectificativa (Art. 80.1 y 80.2 y error fundado en derecho) 纠正税基、税率或金额错误(因判决、行政原因等)。 Rectificación 依据法条 80.1 / 80.2。
R2 Factura rectificativa (Art. 80.3) 因退货、折扣、无效合同等导致的修正。 对应业务更改(例如取消销售)。
R3 Factura rectificativa (Art. 80.4) 客户破产或无力偿付引起的修正。 专用于破产情形。
R4 Factura rectificativa (Resto) 其他未涵盖情形的更正发票。 一般性修正用途。
R5 Factura rectificativa simplificada 针对简易发票的修正。 通常用于 ticket 更正。


QR 生成地址 以及规则

 QR 尺寸 40*40 mm

以地址为准生成 QR 码,并在下方 打印: Factura verificable en la sede electrónica de la AEAT 

测试环境地址:

https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=XXXXXXXXY&numserie=YYYY...YYYY&fecha=DD- DD-AAAA&importe=NNNNNNNNN.DD

生产环境地址:
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=XXXXXXXXY&numserie=YYYY...YYYY&fecha=DD- DD-AAAA&importe=NNNNNNNNN.DD

XML 返回的资料进行拼接  " &"  =  " 链接  xia

nif= XXXXXXXXXXXX   &numseria=  123456  &fecha=     &importe= 

 URL base: https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR? 
 Parámetro nif: 89890001K 
 Parámetro numserie: 12345678&G33 
 Parámetro fecha: 01-01-2024 
 Parámetro importe: 241.4 

开发流程

设置中 添加 加载电子证书按钮 +  密码框    ,    解析  +  使用 

 1 -   解析证书  ( 获取证书中的 ID,  姓名资料 )   
  • CN   : CERTIFICADO FISICA PRUEBAS -99999910G'       (-  前是公司/个人名字  - 后面是税号)
  • SN  :  公司或者个人名称  
  • O :  有这个字段代表公司   否则是个人 
  • SERIALNUMBER  - 后面是 税号
    C 是国家  
  • NotAfter   证书到期时间

参考一下代码

using System;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;

namespace CertInfoExtractor
{
    public enum CertType
    {
        Unknown,
        Personal,
        Organization
    }

    public class CertInfo
    {
        public CertType Type { get; set; }
        public string Name { get; set; }           // 姓名或公司名
        public string TaxNumber { get; set; }      // 税号 / NIF / CIF
        public DateTime NotAfter { get; set; }     // 到期时间
        public int DaysRemaining { get; set; }     // 剩余天数
    }

    public class CertParser
    {
        public static CertInfo Parse(string pfxPath, string password)
        {
            var cert = new X509Certificate2(pfxPath, password,
                X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);

            string subject = cert.Subject;
            string issuer = cert.Issuer;

            string cn = GetField(subject, "CN");
            string org = GetField(subject, "O");
            string serial = GetField(subject, "SERIALNUMBER");

            // 税号兜底正则
            if (string.IsNullOrEmpty(serial))
            {
                var m = Regex.Match(subject, @"(SERIALNUMBER|NIF|CIF|NIE|UID)\s*=\s*([A-Z0-9\-\/]{6,20})",
                    RegexOptions.IgnoreCase);
                if (m.Success)
                    serial = m.Groups[2].Value.Trim();
            }

            // 判断类型
            CertType type = DetectType(org, serial, issuer);

            // 名称:企业用 O,个人用 CN
            string name = (type == CertType.Organization && !string.IsNullOrEmpty(org))
                ? org
                : cn;

            var notAfter = cert.NotAfter;
            int daysRemaining = (int)(notAfter - DateTime.Now).TotalDays;

            return new CertInfo
            {
                Type = type,
                Name = name,
                TaxNumber = serial,
                NotAfter = notAfter,
                DaysRemaining = daysRemaining
            };
        }

        private static CertType DetectType(string org, string serial, string issuer)
        {
            bool hasOrg = !string.IsNullOrEmpty(org);
            bool looksLikeCompanyId = Regex.IsMatch(serial ?? "", @"^[A-Z]\d{7,8}", RegexOptions.IgnoreCase);
            bool looksLikePersonId = Regex.IsMatch(serial ?? "", @"\d{7,8}[A-Z]$", RegexOptions.IgnoreCase);

            if (hasOrg || looksLikeCompanyId)
                return CertType.Organization;
            if (looksLikePersonId)
                return CertType.Personal;

            if (issuer.Contains("Persona Física", StringComparison.OrdinalIgnoreCase))
                return CertType.Personal;
            if (issuer.Contains("Representante", StringComparison.OrdinalIgnoreCase))
                return CertType.Organization;

            return CertType.Unknown;
        }

        private static string GetField(string subject, string key)
        {
            var m = Regex.Match(subject, key + @"\s*=\s*([^,]+)", RegexOptions.IgnoreCase);
            return m.Success ? m.Groups[1].Value.Trim() : null;
        }

        // 测试入口
        public static void Main()
        {
            string pfxPath = @"C:\certs\yourcert.pfx";
            string password = "yourPassword";

            var info = Parse(pfxPath, password);

            Console.WriteLine("🔍 证书类型: " + info.Type);
            Console.WriteLine("📛 名称: " + info.Name);
            Console.WriteLine("🧾 税号: " + info.TaxNumber);
            Console.WriteLine("📅 到期时间: " + info.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"));
            Console.WriteLine("⏳ 剩余天数: " + info.DaysRemaining);
        }
    }
}

系统信息部分  SistemaInformatico XML 

               <sum1:SistemaInformatico> 
                  <sum1:NombreRazon>JIECHENG INFORMATICA SL</sum1:NombreRazon>                   
                  <sum1:NIF>B67287789</sum1:NIF> 
                  <sum1:NombreSistemaInformatico>JIECHENG TPV</sum1:NombreSistemaInformatico> 
                  <sum1:IdSistemaInformatico>J2</sum1:IdSistemaInformatico> 
                  <sum1:Version>1.0.03</sum1:Version> 
                  <sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>   // 主机1 副机 2
                  <sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu> 
                  <sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT> 
                  <sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT> 
               </sum1:SistemaInformatico> 

2- 解析证书后 参考文档生成指纹  生成记录指纹或哈希的技术规范详情 

发送发票 /小票 hash 计算

a) Datos de campos a utilizar en el caso de registros de facturación de alta
(y la “ruta” de su localización dentro del registro):
1. IDEmisorFactura (RegistroAlta/IDFactura/IDEmisorFactura)
2. NumSerieFactura (RegistroAlta/IDFactura/NumSerieFactura)
3. FechaExpedicionFactura
(RegistroAlta/IDFactura/FechaExpedicionFactura)
4. TipoFactura (RegistroAlta/TipoFactura)
5. CuotaTotal (RegistroAlta/CuotaTotal)
6. ImporteTotal (RegistroAlta/ImporteTotal)
7. Huella (RegistroAlta/Encadenamiento/RegistroAnterior/Huella)
8. FechaHoraHusoGenRegistro
(RegistroAlta/FechaHoraHusoGenRegistro)

数据需要以以上顺序生成 string 进行连接

如  nombreCampo1=valorCampo1&nombreCampo2=valorCampo2&nombreCampoN=valorCampoN

可参考一下代码 (官方代码 翻译  可能需要操作  合并xml )

using System;
using System.Security.Cryptography;
using System.Text;

public class GeneradorHuella
{
    /// <summary>
    /// 计算 SHA-256 哈希并输出 Base64(与 Java 逻辑完全一致)
    /// </summary>
    public static string GetHashVerifactu(string msg)
    {
        try
        {
            using (var sha = SHA256.Create())
            {
                byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(msg));
                return Convert.ToBase64String(hash);
            }
        }
        catch (Exception e)
        {
            throw new Exception("生成 SHA 哈希时发生错误", e);
        }
    }

    /// <summary>
    /// 生成 RegistroAlta 记录对应的字段拼接字符串
    /// 顺序与 Java 完全相同
    /// </summary>
    public static string GetReferenciaRegistroAlta(
        string nifEmisor,
        string numFacturaSerie,
        string fechaExpedicion,
        string tipoFactura,
        string cuotaTotal,
        string importeTotal,
        string huellaAnterior,
        string fechaHoraUsoRegistro)
    {
        var sb = new StringBuilder();

        sb.Append(GetValorCampo("IDEmisorFactura", nifEmisor, true))
          .Append(GetValorCampo("NumSerieFactura", numFacturaSerie, true))
          .Append(GetValorCampo("FechaExpedicionFactura", fechaExpedicion, true))
          .Append(GetValorCampo("TipoFactura", tipoFactura, true))
          .Append(GetValorCampo("CuotaTotal", cuotaTotal, true))
          .Append(GetValorCampo("ImporteTotal", importeTotal, true))
          .Append(GetValorCampo("Huella", huellaAnterior, true))
          .Append(GetValorCampo("FechaHoraUsoRegistro", fechaHoraUsoRegistro, false)); // 最后一项不加 &

        return sb.ToString();
    }

    /// <summary>
    /// 生成一个字段的格式:Nombre=Valor 或 Nombre=Valor&
    /// </summary>
    public static string GetValorCampo(string nombre, string valor, bool separador)
    {
        string campo = nombre + "=" + (valor == null ? "" : valor.Trim());

        if (separador)
            return campo + "&";
        else
            return campo;
    }

    /// <summary>
    /// 计算 Alta 记录的 huella(Base64 SHA-256)
    /// </summary>
    public static string CalcularHuellaAlta(
        string nifEmisor,
        string numFacturaSerie,
        string fechaExpedicion,
        string tipoFactura,
        string cuotaTotal,
        string importeTotal,
        string huellaAnterior,
        string fechaHoraUsoRegistro)
    {
        string referencia = GetReferenciaRegistroAlta(
            nifEmisor,
            numFacturaSerie,
            fechaExpedicion,
            tipoFactura,
            cuotaTotal,
            importeTotal,
            huellaAnterior,
            fechaHoraUsoRegistro);

        return GetHashVerifactu(referencia);
    }
}

其他方法:

📌 1. 主入口(你只需要调用这个)
string xml = File.ReadAllText("miFactura.xml");
string huella = VerifactuHashGenerator.CalcularHuellaDesdeXml(xml);

Console.WriteLine("Huella generada = " + huella);

🚀 2. VerifactuHashGenerator.cs(完整可运行)

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Xml;

public static class VerifactuHashGenerator
{
    // =============================
    // PUBLIC ENTRY
    // =============================
    public static string CalcularHuellaDesdeXml(string xml)
    {
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(xml);

        // Detectar tipo de registro (Alta / Evento / Anulación)
        string tipo = DetectarTipoRegistro(doc);

        // Obtener campos según el tipo
        var campos = tipo switch
        {
            "RegistroAlta" => ExtraerCamposRegistroAlta(doc),
            "RegistroEvento" => ExtraerCamposRegistroEvento(doc),
            "RegistroAnulacion" => ExtraerCamposRegistroAnulacion(doc),
            _ => throw new Exception("Tipo de registro no reconocido")
        };

        // Construir cadena ordenada
        string cadena = ConstruirCadena(campos);

        // Calcular hash HEX
        return Sha256Hex(cadena);
    }

    // =============================
    // DETECCIÓN DE TIPO DE REGISTRO
    // =============================
    private static string DetectarTipoRegistro(XmlDocument doc)
    {
        if (doc.GetElementsByTagName("RegistroAlta").Count > 0) return "RegistroAlta";
        if (doc.GetElementsByTagName("RegistroEvento").Count > 0) return "RegistroEvento";
        if (doc.GetElementsByTagName("RegistroAnulacion").Count > 0) return "RegistroAnulacion";

        throw new Exception("No se encontró nodo RegistroAlta / RegistroEvento / RegistroAnulacion");
    }

    // =============================
    // CAMPO EXTRACTION (ALTA)
    // =============================
    private static List<(string, string)> ExtraerCamposRegistroAlta(XmlDocument doc)
    {
        var lista = new List<(string, string)>();

        lista.Add(("IDEmisorFactura", Get(doc, "IDEmisorFactura")));
        lista.Add(("NumSerieFactura", Get(doc, "NumSerieFactura")));
        lista.Add(("FechaExpedicionFactura", Get(doc, "FechaExpedicionFactura")));
        lista.Add(("TipoFactura", Get(doc, "TipoFactura")));
        lista.Add(("CuotaTotal", NormalizarNumero(Get(doc, "CuotaTotal"))));
        lista.Add(("ImporteTotal", NormalizarNumero(Get(doc, "ImporteTotal"))));
        lista.Add(("Huella", Get(doc, "Huella")));
        lista.Add(("FechaHoraUsoRegistro", Get(doc, "FechaHoraUsoRegistro")));

        return lista;
    }

    // =============================
    // CAMPO EXTRACTION (EVENTO)
    // =============================
    private static List<(string, string)> ExtraerCamposRegistroEvento(XmlDocument doc)
    {
        var lista = new List<(string, string)>();

        lista.Add(("IDEmisorFactura", Get(doc, "IDEmisorFactura")));
        lista.Add(("NumSerieFactura", Get(doc, "NumSerieFactura")));
        lista.Add(("TipoEvento", Get(doc, "TipoEvento")));
        lista.Add(("DescripcionEvento", Get(doc, "DescripcionEvento")));
        lista.Add(("Huella", Get(doc, "Huella")));
        lista.Add(("FechaHoraUsoRegistro", Get(doc, "FechaHoraUsoRegistro")));

        return lista;
    }

    // =============================
    // CAMPO EXTRACTION (ANULACION)
    // =============================
    private static List<(string, string)> ExtraerCamposRegistroAnulacion(XmlDocument doc)
    {
        var lista = new List<(string, string)>();

        lista.Add(("IDEmisorFactura", Get(doc, "IDEmisorFactura")));
        lista.Add(("NumSerieFactura", Get(doc, "NumSerieFactura")));
        lista.Add(("FechaExpedicionFactura", Get(doc, "FechaExpedicionFactura")));
        lista.Add(("TipoFactura", Get(doc, "TipoFactura")));
        lista.Add(("MotivoAnulacion", Get(doc, "MotivoAnulacion")));
        lista.Add(("Huella", Get(doc, "Huella")));
        lista.Add(("FechaHoraUsoRegistro", Get(doc, "FechaHoraUsoRegistro")));

        return lista;
    }

    // =============================
    // XML GETTER
    // =============================
    private static string Get(XmlDocument doc, string tag)
    {
        var list = doc.GetElementsByTagName(tag);
        if (list.Count == 0) return "";
        return list[0].InnerText.Trim();
    }

    // =============================
    // STRING BUILDING
    // =============================
    private static string ConstruirCadena(List<(string campo, string valor)> lista)
    {
        var sb = new StringBuilder();

        for (int i = 0; i < lista.Count; i++)
        {
            bool addAmp = i < lista.Count - 1;
            sb.Append(lista[i].campo)
              .Append("=")
              .Append(lista[i].valor);

            if (addAmp) sb.Append("&");
        }

        return sb.ToString();
    }

    // =============================
    // NORMALIZACIÓN DE NUMEROS
    // =============================
    private static string NormalizarNumero(string raw)
    {
        if (decimal.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
            return d.ToString("G29", CultureInfo.InvariantCulture);

        return raw?.Trim();
    }

    // =============================
    // SHA256 → HEX
    // =============================
    private static string Sha256Hex(string input)
    {
        using var sha = SHA256.Create();
        byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
        var sb = new StringBuilder();

        foreach (var b in bytes)
            sb.Append(b.ToString("X2"));

        return sb.ToString();
    }
}