Timbrar TXT + PDF
Esta operación extiende la funcionalidad de timbrarTXT, permitiendo no solo generar y timbrar un CFDI a partir de un layout de texto plano (TXT), sino también obtener una representación en formato PDF del comprobante, personalizada con una plantilla y un logotipo.
Descripción de la Operación
Esta operación toma un layout TXT, lo convierte en un CFDI (versión 4.0), lo sella con los certificados proporcionados y lo timbra. Como resultado, devuelve una estructura JSON que contiene tanto el XML del CFDI timbrado como el contenido del PDF generado en formato Base64.
Parámetros de Entrada (Input)
| Parámetro | Tipo de Dato | Descripción |
|---|---|---|
apikey |
string |
Credencial de acceso al servicio (Solicita aquí). |
txtB64 |
string |
Cadena en formato Base64 que contiene el layout TXT con la información del CFDI a generar. Conversor Base 64 |
keyPEM |
string |
Contenido del archivo de la llave privada (.key) en formato PEM. |
cerPEM |
string |
Contenido del archivo del certificado de llave pública (.cer) en formato PEM. |
plantilla |
string |
Identificador numérico de la plantilla a utilizar para la generación del PDF. Galería de plantillas |
logob64 |
string |
Imagen (logo) de la empresa en formato PNG y codificada en Base64. Conversor Base 64 |
Parámetros de Salida (Output) - RespuestaTimbrado
| Atributo | Tipo de Dato | Descripción |
|---|---|---|
code |
string |
Código de respuesta de la operación. |
message |
string |
Mensaje detallado de la respuesta. |
data |
string |
JSON string que contiene el XML del CFDI timbrado y el contenido del PDF en Base64. Debe ser decodificado para acceder a sus valores. |
Estructura del TXT de Entrada
El TXT enviado en el parámetro txtB64 debe seguir la siguiente estructura de “pipe” (|). Cada línea representa una sección del CFDI y los campos se separan por |. Los valores vacíos se indican con un | consecutivo. Conversor Base 64
COMPROBANTE|4.0|Serie|Folio|Fecha|FormaPago|NoCertificado|CondicionesDePago|SubTotal|Descuento|Moneda|TipoCambio|Total|TipoDeComprobante|Exportacion|MetodoPago|LugarExpedicion|Confirmacion|
INFORMACIONGLOBAL|Periodicidad|Meses|Año|
CFDIRELACIONADOS|numeroCfdiRelacionados|TipoRelacion|
CFDIRELACIONADO|UUID-1|UUID-2|...|UUID-N|
EMISOR|Rfc|Nombre|RegimenFiscal|FacAtrAdquirente|
RECEPTOR|Rfc|Nombre|DomicilioFiscalReceptor|ResidenciaFiscal|NumRegIdTrib|RegimenFiscalReceptor|UsoCFDI|
CONCEPTOS|numeroConceptos|
CONCEPTO|ClaveProdServ|NoIdentificacion|Cantidad|ClaveUnidad|Unidad|Descripcion|ValorUnitario|Importe|Descuento|ObjetoImp|
IMPUESTOSCONCEPTO|numeroTraslados|numeroRetenciones|
TRASLADOCONCEPTO|Base|Impuesto|TipoFactor|TasaOCuota|Importe|
RETENCIONCONCEPTO|Base|Impuesto|TipoFactor|TasaOCuota|Importe|
ACUENTATERCEROS|RfcACuentaTerceros|NombreACuentaTerceros|RegimenFiscalACuentaTerceros|DomicilioFiscalACuentaTerceros|
INFORMACIONADUANERA|numeroNumerosPedimento|
NUMEROSPEDIMENTO|pedimento-1|pedimento-2|...|pedimento-N|
CUENTAPREDIAL|numeroNumeros|
NUMEROS|numero-1|numero-2|...|numero-N|
IMPUESTOS|numeroTraslados|numeroRetenciones|TotalImpuestosTrasladados|TotalImpuestosRetenidos|
TRASLADO|Base|Impuesto|TipoFactor|TasaOCuota|Importe|
RETENCION|Impuesto|Importe|
CORREO|correo1|correo2|...|correoN|
CAMPOS_EXTRA|numeroCamposExtra|
CAMPO_EXTRA|llave|valor|descripcion|Consulta el archivo completo aquí:
TXT
Estructura del JSON en el campo data
El campo data de la respuesta contiene un JSON con la siguiente estructura:
{
"XML": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y2ZkaT...",
"PDF": "<?xml version="1.0" encoding="UTF-8"?><cfdi:Comprobante..."
}XML: String con el contenido del CFDI timbrado.PDF: String en formato Base64 con el contenido del archivo PDF.
Ejemplo de Código
Herramienta svcutil
Descarga e instala la herramienta svcutil
Ejecuta el comando siguiente (DESARROLLO)
svcutil.exe https://dev.facturaloplus.com/ws/servicio.do?wsdl /out:ServicioTimbradoClient.cs /config:app.configEsto genera dos archivos: ServicioTimbradoClient.cs y la configuración en app.config
Implementación
// Se requiere la librería System.Text.Json
using System.Text.Json;
using System.Text;
/// <summary>
/// Genera dinámicamente el layout del CFDI en formato TXT.
/// </summary>
/// <returns>Un string con el TXT del CFDI.</returns>
private static string GetTxtLayout()
{
var sb = new StringBuilder();
sb.AppendLine("COMPROBANTE|4.0|F|123|2025-07-04T12:00:00|01|||100.00|0.00|MXN|1|116.00|I|01|PUE|64000|");
sb.AppendLine("EMISOR|ABC010101XYZ|Empresa Ejemplo SA de CV|601|");
sb.AppendLine("RECEPTOR|XAXX010101000|Publico en General|64000|||616|G03");
sb.AppendLine("CONCEPTOS|1");
sb.AppendLine(" CONCEPTO|01010101||1.0|ACT|Actividad|Producto de prueba|100.00|100.00||02");
sb.AppendLine(" IMPUESTOSCONCEPTO|1|0");
sb.AppendLine(" TRASLADOCONCEPTO|100.00|002|Tasa|0.160000|16.00");
sb.AppendLine("IMPUESTOS|1|0|16.00|");
sb.AppendLine(" TRASLADO|100.00|002|Tasa|0.160000|16.00");
return sb.ToString();
}
// Clase para deserializar la respuesta en el campo 'data'
public class RespuestaData
{
public string XML { get; set; }
public string PDF { get; set; }
}
public async Task<RespuestaTimbrado> TimbrarTxt2Async(string apiKey, string txtLayout, string keyPem, string cerPem, string plantilla, string logoB64)
{
var txtB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(txtLayout));
using var client = new ServicioTimbradoWSPortTypeClient("ServicioTimbradoWSPort");
// Asumiendo que la operación se llama 'timbrarTXT2'
var response = await client.timbrarTXT2Async(apiKey, txtB64, keyPem, cerPem, plantilla, logoB64);
return new RespuestaTimbrado { Code = response.code, Message = response.message, Data = response.data };
}
public class RespuestaTimbrado
{
public string? Code { get; set; }
public string? Message { get; set; }
public string? Data { get; set; }
}
// Ejemplo de uso
public async Task EjemploUsoTimbrarTxt2Async()
{
string apiKey = "TU_API_KEY_AQUI";
string txtLayout = GetTxtLayout(); // Generación dinámica
string keyPem = File.ReadAllText("ruta/a/llave.key.pem");
string cerPem = File.ReadAllText("ruta/a/certificado.cer.pem");
string plantilla = "1"; // ID de la plantilla a usar
string logoB64 = Convert.ToBase64String(File.ReadAllBytes("ruta/a/logo.png"));
var resultado = await TimbrarTxt2Async(apiKey, txtLayout, keyPem, cerPem, plantilla, logoB64);
if (resultado?.Code == "200" && !string.IsNullOrEmpty(resultado.Data))
{
Console.WriteLine("¡Timbrado con TXT y PDF Exitoso!");
// Deserializar el JSON de la respuesta
var respuestaData = JsonSerializer.Deserialize<RespuestaData>(resultado.Data);
Console.WriteLine("--- XML Timbrado ---");
Console.WriteLine(respuestaData.XML);
// Guardar el PDF
var pdfBytes = Convert.FromBase64String(respuestaData.PDF);
File.WriteAllBytes("comprobante.pdf", pdfBytes);
Console.WriteLine("
PDF guardado como comprobante.pdf");
}
else
{
Console.WriteLine($"Error: {resultado?.Code} - {resultado?.Message}");
}
}Herramienta wsimport
Java incluye la herramienta wsimport en el JDK para generar las clases cliente a partir de un WSDL.
Ejecuta el siguiente comando en tu terminal para el ambiente de DESARROLLO:
wsimport -keep -p com.facturaloplus.cliente https://dev.facturaloplus.com/ws/servicio.do?wsdl-keep: Conserva los archivos fuente .java generados.
-p: Especifica el paquete (package) donde se guardarán las clases.
Implementación
// Se recomienda usar una librería como Gson o Jackson para parsear el JSON de respuesta.
// import com.google.gson.Gson;
/**
* Genera dinámicamente el layout del CFDI en formato TXT.
*/
public static String getTxtLayout() {
return String.join(System.lineSeparator(),
"COMPROBANTE|4.0|F|123|2025-07-04T12:00:00|01|||100.00|0.00|MXN|1|116.00|I|01|PUE|64000|",
"EMISOR|ABC010101XYZ|Empresa Ejemplo SA de CV|601|",
"RECEPTOR|XAXX010101000|Publico en General|64000|||616|G03",
"CONCEPTOS|1",
" CONCEPTO|01010101||1.0|ACT|Actividad|Producto de prueba|100.00|100.00||02",
" IMPUESTOSCONCEPTO|1|0",
" TRASLADOCONCEPTO|100.00|002|Tasa|0.160000|16.00",
"IMPUESTOS|1|0|16.00|",
" TRASLADO|100.00|002|Tasa|0.160000|16.00"
);
}
// Clase para mapear el JSON en el campo 'data'
class RespuestaData {
String xml;
String pdf;
}
public CompletableFuture<RespuestaTimbrado> timbrarTxt2Async(String apiKey, String txtLayout, String keyPem, String cerPem, String plantilla, String logoB64) {
return CompletableFuture.supplyAsync(() -> {
try {
String txtB64 = Base64.getEncoder().encodeToString(txtLayout.getBytes(StandardCharsets.UTF_8));
ServicioTimbradoWSPortType port = new ServicioTimbradoWS().getServicioTimbradoWSPort();
// Asumiendo que la operación se llama 'timbrarTXT2'
Respuesta response = port.timbrarTXT2(apiKey, txtB64, keyPem, cerPem, plantilla, logoB64);
RespuestaTimbrado resultado = new RespuestaTimbrado();
resultado.setCode(response.getCode());
resultado.setMessage(response.getMessage());
resultado.setData(response.getData());
return resultado;
} catch (Exception e) {
throw new RuntimeException("Error en timbrarTXT2", e);
}
}, executor);
}
// Ejemplo de uso
public static void main(String[] args) {
// ... (inicialización de service, apiKey, etc.)
String txtLayout = getTxtLayout(); // Generación dinámica
String plantilla = "1";
String logoB64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get("ruta/a/logo.png")));
service.timbrarTxt2Async(apiKey, txtLayout, keyPem, cerPem, plantilla, logoB64).whenComplete((resultado, ex) -> {
if (ex == null && "200".equals(resultado.getCode())) {
System.out.println("¡Timbrado con TXT y PDF Exitoso!");
// Parsear el JSON de respuesta
// Gson gson = new Gson();
// RespuestaData data = gson.fromJson(resultado.getData(), RespuestaData.class);
// System.out.println("XML: " + data.xml);
// byte[] pdfBytes = Base64.getDecoder().decode(data.pdf);
// Files.write(Paths.get("comprobante.pdf"), pdfBytes);
System.out.println("PDF guardado correctamente.");
} else { /* ... manejo de error ... */ }
service.shutdown();
}).join();
}Herramienta Zeep
Para interactuar con servicios SOAP en Python, la librería zeep es una excelente opción. Proporciona una interfaz limpia y moderna.
Instala la librería usando pip:
pip install zeepImplementación
# ... (Definición de RespuestaTimbrado y configuración de logging)
import json
def get_txt_layout() -> str:
"""
Genera dinámicamente el layout del CFDI como un string de texto.
"""
return "
".join([
"COMPROBANTE|4.0|F|123|2025-07-04T12:00:00|01|||100.00|0.00|MXN|1|116.00|I|01|PUE|64000|",
"EMISOR|ABC010101XYZ|Empresa Ejemplo SA de CV|601|",
"RECEPTOR|XAXX010101000|Publico en General|64000|||616|G03",
"CONCEPTOS|1",
" CONCEPTO|01010101||1.0|ACT|Actividad|Producto de prueba|100.00|100.00||02",
" IMPUESTOSCONCEPTO|1|0",
" TRASLADOCONCEPTO|100.00|002|Tasa|0.160000|16.00",
"IMPUESTOS|1|0|16.00|",
" TRASLADO|100.00|002|Tasa|0.160000|16.00"
])
async def timbrar_txt2_async(self, api_key: str, txt_layout: str, key_pem: str, cer_pem: str, plantilla: str, logo_b64: str) -> RespuestaTimbrado:
txt_b64 = base64.b64encode(txt_layout.encode('utf-8')).decode('utf-8')
# Asumiendo que la operación se llama 'timbrarTXT2'
response = await self.async_client.service.timbrarTXT2(
apikey=api_key, txtB64=txt_b64, keyPEM=key_pem, cerPEM=cer_pem, plantilla=plantilla, logob64=logo_b64
)
return RespuestaTimbrado(code=response.code, message=response.message, data=response.data)
# Ejemplo de uso
async def main():
# ... (inicialización de service, apiKey, etc.)
layout = get_txt_layout() # Generación dinámica
plantilla = "1"
with open("ruta/a/logo.png", "rb") as image_file:
logo_b64 = base64.b64encode(image_file.read()).decode('utf-8')
resultado = await service.timbrar_txt2_async(api_key, layout, key_pem, cer_pem, plantilla, logo_b64)
if resultado.code == "200" and resultado.data:
print("¡Timbrado con TXT y PDF Exitoso!")
# Decodificar el JSON de la respuesta
respuesta_data = json.loads(resultado.data)
xml_timbrado = respuesta_data.get('xml')
pdf_b64 = respuesta_data.get('pdf')
print("--- XML Timbrado ---")
print(xml_timbrado)
# Guardar el PDF
with open("comprobante.pdf", "wb") as f:
f.write(base64.b64decode(pdf_b64))
print("
PDF guardado como comprobante.pdf")
else:
print(f"Error: {resultado.code} - {resultado.message}")Herramienta SoapClient
PHP tiene soporte nativo para SOAP a través de la extensión SOAP. Asegúrate de que la extensión php-soap esté habilitada en tu archivo php.ini.
Implementación
<?php
/**
* Genera dinámicamente el layout del CFDI como un string TXT.
*/
function getTxtLayout(): string {
return implode("
", [
"COMPROBANTE|4.0|F|123|2025-07-04T12:00:00|01|||100.00|0.00|MXN|1|116.00|I|01|PUE|64000|",
"EMISOR|ABC010101XYZ|Empresa Ejemplo SA de CV|601|",
"RECEPTOR|XAXX010101000|Publico en General|64000|||616|G03",
"CONCEPTOS|1",
" CONCEPTO|01010101||1.0|ACT|Actividad|Producto de prueba|100.00|100.00||02",
" IMPUESTOSCONCEPTO|1|0",
" TRASLADOCONCEPTO|100.00|002|Tasa|0.160000|16.00",
"IMPUESTOS|1|0|16.00|",
" TRASLADO|100.00|002|Tasa|0.160000|16.00"
]);
}
public function timbrarTxt2(string $apiKey, string $txtLayout, string $keyPem, string $cerPem, string $plantilla, string $logoB64): RespuestaTimbrado {
$respuesta = new RespuestaTimbrado();
try {
$txtB64 = base64_encode($txtLayout);
$soapClient = new SoapClient($this->wsdlUrl, ['trace' => 1, 'exceptions' => true]);
$params = [
'apikey' => $apiKey, 'txtB64' => $txtB64,
'keyPEM' => $keyPem, 'cerPEM' => $cerPem,
'plantilla' => $plantilla, 'logob64' => $logoB64
];
// Asumiendo que la operación se llama 'timbrarTXT2'
$response = $soapClient->timbrarTXT2($params);
$respuesta->code = $response->return->code ?? null;
$respuesta->message = $response->return->message ?? null;
$respuesta->data = $response->return->data ?? null;
} catch (Exception $e) { /* ... manejo de error ... */ }
return $respuesta;
}
// Ejemplo de uso
// ... (inicialización de service, apiKey, etc.)
$txtLayout = getTxtLayout(); // Generación dinámica
$plantilla = "1";
$logoB64 = base64_encode(file_get_contents("ruta/a/logo.png"));
$resultado = $service->timbrarTxt2($apiKey, $txtLayout, $keyPem, $cerPem, $plantilla, $logoB64);
if ($resultado->code === '200' && !empty($resultado->data)) {
echo "¡Timbrado con TXT y PDF Exitoso!
";
// Decodificar el JSON de respuesta
$data = json_decode($resultado->data, true);
$xml = $data['xml'];
$pdfB64 = $data['pdf'];
echo "--- XML Timbrado ---
";
echo $xml;
// Guardar el PDF
file_put_contents("comprobante.pdf", base64_decode($pdfB64));
echo "
PDF guardado como comprobante.pdf
";
} else {
echo "Error: {$resultado->code} - {$resultado->message}
";
}
?>
Respuesta (Response)
La estructura de la respuesta SOAP es similar a las anteriores, pero el contenido de la etiqueta data es un JSON que encapsula el XML y el PDF.
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope ...>
<SOAP-ENV:Body>
<ns1:timbrarTXT2Response xmlns:ns1="urn:ws_api">
<return xsi:type="tns:RespuestaTimbrado">
<code xsi:type="xsd:string">200</code>
<message xsi:type="xsd:string">OK</message>
<data xsi:type="xsd:string"><![CDATA[{
"xml": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y2ZkaT...",
"pdf": "JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvT3V0bGlu..."
}]]></data>
</return>
</ns1:timbrarTXT2Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>Códigos de respuesta
Los códigos de respuesta para el timbrado de CFDI son importantes para entender el resultado de la solicitud. A continuación se detallan los códigos más comunes que puedes recibir al realizar una solicitud de timbrado a través de la API.
| Código | Descripción |
|---|---|
| 200 | Solicitud procesada con éxito |
| 300 | API KEY Inválida o inexistente |
| 301 | XML mal formado |
| 302 | El sello del emisor no es válido |
| 303 | El RFC del CSD del emisor no corresponde al RFC del Emisor |
| 304 | CSD del Emisor ha sido revobado |
| 305 | La fecha de emisión no está dentro de la vigencia del CSD del Emisor |
| 306 | La llave utilizada para sellar debe ser un CSD |
| 307 | El CFDI contiene un timbre previo |
| 308 | El CSD del emisor no ha sido firmado por uno de los Certificados de autoridad del SAT |
| 401 | El rango de la fecha de generación no debe de ser mayor a 72 horas para la emisión del timbre |
| 402 | RFC del emisor no se encuentra en el régimen de contribuyentes (Lista de validación de régimen) LCO |