curl --request POST \
--url https://api.ipsofactura.com/cfdi/timbrar-sellado \
--header 'Content-Type: application/json' \
--header 'x-api: <api-key>' \
--data '
{
"xml_cfdi_base64": "<string>",
"idempotency_key": "<string>"
}
'import requests
url = "https://api.ipsofactura.com/cfdi/timbrar-sellado"
payload = {
"xml_cfdi_base64": "<string>",
"idempotency_key": "<string>"
}
headers = {
"x-api": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({xml_cfdi_base64: '<string>', idempotency_key: '<string>'})
};
fetch('https://api.ipsofactura.com/cfdi/timbrar-sellado', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ipsofactura.com/cfdi/timbrar-sellado",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'xml_cfdi_base64' => '<string>',
'idempotency_key' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.ipsofactura.com/cfdi/timbrar-sellado"
payload := strings.NewReader("{\n \"xml_cfdi_base64\": \"<string>\",\n \"idempotency_key\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.ipsofactura.com/cfdi/timbrar-sellado")
.header("x-api", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"xml_cfdi_base64\": \"<string>\",\n \"idempotency_key\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ipsofactura.com/cfdi/timbrar-sellado")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"xml_cfdi_base64\": \"<string>\",\n \"idempotency_key\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"cadena_original_sat": "||1.1|...||",
"folio": "101",
"id": "9a4f07ff-3abc-435f-b7a6-88c2bc65b4c1",
"numero_certificado_sat": "00001000000500000000",
"sello_cfdi": "...",
"sello_sat": "...",
"serie": "A",
"stamped_at": "2026-08-28T10:00:00",
"uuid": "6128396f-c09b-4ec6-8699-43da5a244971"
}Timbrar un CFDI ya sellado
Recibe un CFDI 4.0 que tú ya sellaste con tu propio CSD y lo timbra ante el SAT. El reparto de trabajo se invierte respecto de POST /cfdi/timbrar-xml: aquí tú armas y sellas el documento, Ipsofactura sólo verifica el sello y lo lleva al PAC.
Es la ruta para quien no quiere entregar su llave privada en custodia: la empresa emisora no necesita tener ningún CSD cargado en Ipsofactura. El certificado viaja dentro del propio comprobante.
El XML se manda en xml_cfdi_base64 (base64 de los bytes UTF-8 del XML; se aceptan saltos de línea). Debe traer Sello, Certificado y NoCertificado.
Qué se verifica antes de timbrar (todo falla con 400, nunca con un timbrado a medias):
- El documento cumple los XSD del SAT (
XSD_VALIDATION_ERROR). - El
Certificadoembebido es de tu RFC emisor, está vigente y su número de serie coincide conNoCertificado(SEAL_VERIFICATION_FAILED). - El
Selloverifica contra la cadena original recalculada de los bytes que nos mandaste (SEAL_VERIFICATION_FAILED). Sella exactamente lo que envías: cualquier cambio posterior a la firma la invalida.
El documento se timbra tal cual. No se le toca un solo atributo, así que Ipsofactura no asigna folio consecutivo en esta ruta: Serie y Folio son los que traiga el documento (ambos son opcionales en el esquema del SAT; si no vienen, se timbra sin ellos). Escribir un Folio en un comprobante ya firmado rompería su sello. Si quieres el folio consecutivo de Ipsofactura, usa POST /cfdi/timbrar-xml.
Sin Serie ni Folio el Sello es la identidad. Un comprobante sellado que no trae ninguno de los dos no tiene identidad fiscal con la que deduplicarlo, así que Ipsofactura la deriva de su propio Sello — que firma exactamente esos bytes. Reenviar el mismo documento devuelve el CFDI ya timbrado (201) en vez de timbrar otro; un reenvío simultáneo recibe 409 mientras el primero está en curso. Es la misma mecánica que POST /cfdi/timbrar aplica cuando deriva la llave de serie + folio: la llave derivada es interna y no se devuelve en la respuesta.
Sólo tipos I y E. Un complemento de pago (P) se rechaza: la parcialidad y el saldo insoluto se derivan de los pagos ya registrados, y recalcularlos invalidaría tu sello.
Requiere un PAC que acepte comprobantes pre-sellados. Si en ese momento no hay ninguno disponible, la respuesta es 503 — nunca un timbrado re-sellado por nuestro lado.
Tamaño máximo: 2 MB de XML decodificado (REQUEST_TOO_LARGE, 413).
idempotency_key (opcional, ≤ 100 caracteres): un reenvío con la misma llave devuelve la respuesta del primer intento con la cabecera Idempotency-Replayed: true.
curl --request POST \
--url https://api.ipsofactura.com/cfdi/timbrar-sellado \
--header 'Content-Type: application/json' \
--header 'x-api: <api-key>' \
--data '
{
"xml_cfdi_base64": "<string>",
"idempotency_key": "<string>"
}
'import requests
url = "https://api.ipsofactura.com/cfdi/timbrar-sellado"
payload = {
"xml_cfdi_base64": "<string>",
"idempotency_key": "<string>"
}
headers = {
"x-api": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({xml_cfdi_base64: '<string>', idempotency_key: '<string>'})
};
fetch('https://api.ipsofactura.com/cfdi/timbrar-sellado', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ipsofactura.com/cfdi/timbrar-sellado",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'xml_cfdi_base64' => '<string>',
'idempotency_key' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.ipsofactura.com/cfdi/timbrar-sellado"
payload := strings.NewReader("{\n \"xml_cfdi_base64\": \"<string>\",\n \"idempotency_key\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.ipsofactura.com/cfdi/timbrar-sellado")
.header("x-api", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"xml_cfdi_base64\": \"<string>\",\n \"idempotency_key\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ipsofactura.com/cfdi/timbrar-sellado")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"xml_cfdi_base64\": \"<string>\",\n \"idempotency_key\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"cadena_original_sat": "||1.1|...||",
"folio": "101",
"id": "9a4f07ff-3abc-435f-b7a6-88c2bc65b4c1",
"numero_certificado_sat": "00001000000500000000",
"sello_cfdi": "...",
"sello_sat": "...",
"serie": "A",
"stamped_at": "2026-08-28T10:00:00",
"uuid": "6128396f-c09b-4ec6-8699-43da5a244971"
}Authorizations
API key emitida por Ipsofactura, enviada en el header x-api.
Headers
Alternativa a idempotency_key en el cuerpo: reenviar la misma llave devuelve la respuesta del primer intento (Idempotency-Replayed: true) en lugar de timbrar otra vez, y la misma llave con un cuerpo distinto es 422 IDEMPOTENCY_KEY_REUSED. Máximo 100 caracteres ASCII imprimibles, sin espacios. Si además envías el campo en el cuerpo, los dos valores deben coincidir; si no, la respuesta es 400 VALIDATION_ERROR.
100"order-4711"
Body
CFDI 4.0 ya sellado con tu propio CSD (nodo cfdi:Comprobante), codificado en base64 y en UTF-8. Debe traer Sello, Certificado y NoCertificado: Ipsofactura no sella este documento, sólo verifica el sello y lo timbra tal cual. Máximo 2 MB una vez decodificado.
2908052Llave de idempotencia opcional (máximo 100 caracteres). Un reenvío con la misma llave devuelve la respuesta del primer intento en lugar de timbrar otra vez.
100^[\x21-\x7E]+$Response
CFDI timbrado
Show child attributes
Show child attributes
La clave de idempotencia con la que se timbró este CFDI, si se envió una.