开发文档
所有文档
- 计费记录设计文档
- 验证和错误
- Web 服务描述文档(v1.0.3)
(1.673 KB) - Web 服务描述文档(v1.0.3,英文版)
(1,715 KB) - Web 服务的 WSDL
- Web 服务示意图
- 生成记录指纹或哈希的技术规范详情(v0.1.2)
(1.11 MB) - 发票二维码技术规格说明文档(v0.4.7)
(1.9 MB) - 生成账单记录电子签名的技术规范
- 生成账单记录电子签名的技术规范(v0.1.5)
(1.46 MB) 这个暂不开发 NO VERIFACTU - ZIP文件:包含账单记录签名样本的附件
(5.74 KB)
- 生成账单记录电子签名的技术规范(v0.1.5)
- 无法核实的计费记录验证服务说明文档(v0.2)
(576 KB) 这个暂不开发 NO VERIFACTU - 开发公司常见问题解答(更新于 2025 年 9 月 19 日)
(666 KB) - 负责任声明示例
(708 KB)
测试环境(带证书)
发送票务地址:
https://prewww10.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
生产环境 (带证书)
发送票务地址:
https://www10.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
常用节点意思介绍
| 节点 | 含义 | 可用值 / 格式 | 说明 |
|---|---|---|---|
<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 | S 或 N |
如果你的系统只用于VeriFactu,则为 S |
<sum1:TipoUsoPosibleMultiOT> |
是否支持多义务人 | S 或 N |
SaaS类一般 S |
<sum1:IndicadorMultiplesOT> |
是否有多个义务人使用中 | S 或 N |
如果服务器下多家餐厅,则为 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
生产环境地址:
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 计算
数据需要以以上顺序生成 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();
}
}
以下是针对西班牙税务局 (AEAT) Verifactu SOAP 服务的完整实现,使用 XML 和客户端证书发送请求:
对接流程:
可直接用于 Veri*factu(AEAT tikeV1.0)发送 XML 的 C# 代码模板,包括:
-
加载 p12 客户端证书
-
创建 HttpClient
-
构造 SOAP XML
-
发送到 AEAT(测试 / 生产)
-
读取返回结果
-
兼容 Veri*factu Alta(RegistroAlta)、Envio、Consulta 等所有操作
1. 初始化 HttpClient(含证书)
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Text;
public class VeriFactuClient
{
private readonly HttpClient _client;
public VeriFactuClient(string certPath, string certPassword, bool ignoreSsl = false)
{
var handler = new HttpClientHandler();
// 加载 p12证书
var cert = new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.MachineKeySet);
handler.ClientCertificates.Add(cert);
// 忽略 SSL(仅测试环境)
if (ignoreSsl)
{
handler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
_client = new HttpClient(handler);
}