Documentación de la API
La API recibe un CFDI 4.0 ya sellado (sin timbrar), lo timbra y te regresa el XML timbrado. Funciona con cualquier lenguaje que pueda hacer una petición HTTPS con JSON.
- Dirección
https://ws.timbrando.mx/api/v1/- Formato
- JSON en UTF-8 (
Content-Type: application/json) - Acceso
- Usuario y password que te entrega tu proveedor
- Tamaño máximo
- 10 MB por petición
Modo pruebas y producción
Todo usuario nuevo empieza en modo pruebas: puedes timbrar con tu RFC real para integrar tu sistema, pero los timbres no tienen validez fiscal y no se cobran. Cuando estés listo, tu proveedor te pasa a producción; tu usuario y password no cambian. Todas las respuestas traen el campo "modo" (pruebas o produccion) para que sepas si el timbre es real.
Timbrar: POST /api/v1/timbrar
Petición:
{
"usuario": "tu_usuario",
"password": "tu_password",
"xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
"referencia": "FAC-1234"
}
| Campo | Descripción |
|---|---|
usuario, password | Tus credenciales. También puedes mandarlas por HTTP Basic Auth (encabezado Authorization). |
xml_base64 | El XML del CFDI 4.0 sellado, codificado en base64. |
referencia | Opcional. Tu folio interno (máx. 100 caracteres), para rastrear el movimiento. |
Respuesta exitosa (HTTP 200):
{
"ok": true,
"uuid": "6F1A2B3C-0000-4000-8000-1234567890AB",
"fecha_timbrado": "2026-10-21T12:00:00",
"xml_timbrado_base64": "PD94bWwg...",
"id_movimiento": 1234,
"modo": "produccion"
}
Importante: este servicio no guarda tus XML. Guarda el xml_timbrado_base64 en tu sistema en cuanto lo recibas.
Respuesta con error (todas tienen el mismo formato):
{
"ok": false,
"origen": "edicom",
"codigo": "CFDI40102",
"mensaje": "Texto del error",
"id_movimiento": 1235,
"modo": "produccion"
}
Códigos de respuesta
| HTTP | origen | Qué significa | ¿Se cobra? |
|---|---|---|---|
| 200 | — | Timbrado correctamente. | Sí (en producción) |
| 400 | validacion | Petición inválida: base64 inválido, XML mal formado, no es CFDI 4.0, RFC no dado de alta o sin permiso. | No |
| 401 | autenticacion | Usuario o password incorrectos. | No |
| 403 | autenticacion / validacion | Usuario bloqueado o desactivado; servicio suspendido para el RFC; límite mensual alcanzado. | No |
| 409 | duplicado | Ese mismo XML ya se timbró. Se regresan uuid y fecha_timbrado anteriores; busca el XML en tu sistema. | No |
| 422 | edicom | El PAC o el SAT rechazó el comprobante. codigo y mensaje son los que regresó EDICOM, tal cual. | No |
| 429 | servicio | Demasiadas peticiones por minuto; espera un momento. | No |
| 503 | servicio | El PAC no respondió. Si codigo es SIN_RESPUESTA, puede que sí se haya timbrado: no lo reenvíes sin confirmar con tu proveedor. | No, salvo que se confirme |
Códigos propios de validacion y autenticacion:
| codigo | Descripción |
|---|---|
PETICION_INVALIDA | El cuerpo no es JSON. |
SIN_CREDENCIALES, CREDENCIALES_INVALIDAS | Faltan o no son correctos el usuario y el password. |
USUARIO_BLOQUEADO | Demasiados intentos con password incorrecto; espera unos minutos. |
USUARIO_INACTIVO | El usuario fue desactivado. |
SIN_XML, BASE64_INVALIDO, XML_MUY_GRANDE | Problema con el campo xml_base64. |
XML_INVALIDO | XML mal formado o que no es CFDI 4.0 (el mensaje dice por qué). |
YA_TIMBRADO | El XML ya trae un TimbreFiscalDigital. |
RFC_NO_REGISTRADO, RFC_NO_AUTORIZADO, RFC_INACTIVO | El RFC emisor no está dado de alta, tu usuario no tiene permiso para timbrarlo, o está desactivado. |
SERVICIO_SUSPENDIDO | Servicio suspendido para ese RFC; contacta a tu proveedor. |
LIMITE_MENSUAL | Se alcanzó el límite mensual de timbres. |
DUPLICADO | Ese XML ya fue timbrado (HTTP 409). |
Probar la conexión: GET /api/v1/estado
Con HTTP Basic Auth. Regresa tu modo, los timbres del mes y los RFC que puedes timbrar.
curl -u tu_usuario:tu_password https://ws.timbrando.mx/api/v1/estado
{
"ok": true,
"usuario": "tu_usuario",
"cliente": "Transportes Ejemplo SA de CV",
"modo": "pruebas",
"timbres_mes": 12,
"rfcs": [{"rfc": "EKU9003173C9", "cliente": "Transportes Ejemplo SA de CV", "activo": true}]
}
Ejemplos
# Linux / macOS
XML64=$(base64 -w0 factura.xml)
curl -sS -X POST https://ws.timbrando.mx/api/v1/timbrar \
-H "Content-Type: application/json" \
-d "{\"usuario\":\"tu_usuario\",\"password\":\"tu_password\",\"xml_base64\":\"$XML64\",\"referencia\":\"FAC-1234\"}"import base64, requests
with open("factura.xml", "rb") as f:
xml64 = base64.b64encode(f.read()).decode()
r = requests.post("https://ws.timbrando.mx/api/v1/timbrar", json={
"usuario": "tu_usuario",
"password": "tu_password",
"xml_base64": xml64,
"referencia": "FAC-1234",
}, timeout=120)
datos = r.json()
if datos["ok"]:
with open(datos["uuid"] + ".xml", "wb") as f:
f.write(base64.b64decode(datos["xml_timbrado_base64"]))
print("Timbrado", datos["uuid"], "modo", datos["modo"])
else:
print("Error", r.status_code, datos["origen"], datos["codigo"], datos["mensaje"])<?php
$peticion = json_encode([
'usuario' => 'tu_usuario',
'password' => 'tu_password',
'xml_base64' => base64_encode(file_get_contents('factura.xml')),
'referencia' => 'FAC-1234',
]);
$ch = curl_init('https://ws.timbrando.mx/api/v1/timbrar');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $peticion,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$datos = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($datos['ok']) {
file_put_contents($datos['uuid'] . '.xml', base64_decode($datos['xml_timbrado_base64']));
echo "Timbrado {$datos['uuid']} ({$datos['modo']})";
} else {
echo "Error $http {$datos['origen']} {$datos['codigo']}: {$datos['mensaje']}";
}using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
var peticion = new {
usuario = "tu_usuario",
password = "tu_password",
xml_base64 = Convert.ToBase64String(File.ReadAllBytes("factura.xml")),
referencia = "FAC-1234"
};
var resp = await http.PostAsJsonAsync("https://ws.timbrando.mx/api/v1/timbrar", peticion);
var datos = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (datos.GetProperty("ok").GetBoolean()) {
var uuid = datos.GetProperty("uuid").GetString();
File.WriteAllBytes(uuid + ".xml", Convert.FromBase64String(datos.GetProperty("xml_timbrado_base64").GetString()));
Console.WriteLine($"Timbrado {uuid}");
} else {
Console.WriteLine($"Error {(int)resp.StatusCode} {datos.GetProperty("codigo")}: {datos.GetProperty("mensaje")}");
}// Node.js 18 o superior
const fs = require("fs");
const resp = await fetch("https://ws.timbrando.mx/api/v1/timbrar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
usuario: "tu_usuario",
password: "tu_password",
xml_base64: fs.readFileSync("factura.xml").toString("base64"),
referencia: "FAC-1234",
}),
signal: AbortSignal.timeout(120000),
});
const datos = await resp.json();
if (datos.ok) {
fs.writeFileSync(datos.uuid + ".xml", Buffer.from(datos.xml_timbrado_base64, "base64"));
console.log("Timbrado", datos.uuid, datos.modo);
} else {
console.log("Error", resp.status, datos.origen, datos.codigo, datos.mensaje);
}Recomendaciones
- Usa un tiempo de espera de al menos 90 segundos.
- Si recibes 503 con
SIN_RESPUESTA, no reenvíes de inmediato: consulta a tu proveedor con elid_movimiento. Si reenvías el mismo XML y ya se había timbrado, recibirás 409 con el UUID. - El límite es de 60 peticiones por minuto por usuario (puede variar según tu contrato).