> ## Documentation Index
> Fetch the complete documentation index at: https://developers.ipsofactura.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Timbrado por XML

> Envía tu propio CFDI 4.0 sin sellar; Ipsofactura lo sella con el CSD en custodia y lo timbra.

## Qué es

Si tu sistema ya arma el CFDI 4.0 (por ejemplo tu ERP), no tienes por qué desarmarlo a JSON
para que Ipsofactura lo vuelva a construir del otro lado. `POST /cfdi/timbrar-xml` recibe
**tu XML sin sellar** y hace exactamente lo que no puedes hacer tú: sellarlo con el CSD que
tenemos en custodia y timbrarlo, por el mismo pipeline que usa la ruta JSON.

<Note>
  Reparto de trabajo: **tú armas el documento, nosotros lo sellamos y timbramos.** No envíes
  `Sello`, `NoCertificado` ni `Certificado` — el sellado es nuestro.
</Note>

## El documento es la fuente de verdad

A diferencia de `POST /cfdi/timbrar` (donde tú *describes* un comprobante y Ipsofactura
completa los defaults que falten), aquí el XML es un comprobante **terminado**. Un valor
que ya viene en el documento no se sobrescribe. Los únicos huecos que Ipsofactura llena son:

* **`Folio`**, si el documento no lo trae: se asigna el siguiente consecutivo para ese
  emisor + serie (igual que en la ruta JSON).
* **Datos del emisor** que el documento deje vacíos, cuando el XSD del SAT los permite
  opcionales (en la práctica esto casi nunca aplica: `cfdv40.xsd` ya exige `Nombre` y
  `RegimenFiscal` del emisor).

Todo lo demás —incluido, por ejemplo, el `DomicilioFiscalReceptor` de un receptor
extranjero— se conserva tal cual lo trae el documento. Si eso produce un rechazo del SAT, la
respuesta es honesta: es el documento que enviaste, no uno que reescribimos.

## Request

```bash cURL theme={null}
curl -X POST https://api.ipsofactura.com/cfdi/timbrar-xml \
  -H "x-api: {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y2ZkaTpDb21wcm9iYW50ZSB4bWxuczpjZmRpPSJodHRwOi8vd3d3LnNhdC5nb2IubXgvY2ZkLzQiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3d3dy5zYXQuZ29iLm14L2NmZC80IGh0dHA6Ly93d3cuc2F0LmdvYi5teC9zaXRpb19pbnRlcm5ldC9jZmQvNC9jZmR2NDAueHNkIiBWZXJzaW9uPSI0LjAiIFNlcmllPSJBIiBGb2xpbz0iMSIgRmVjaGE9IjIwMjYtMDgtMTFUMTI6MDA6MDAiIEZvcm1hUGFnbz0iMDMiIFN1YlRvdGFsPSIxMDAuMDAiIE1vbmVkYT0iTVhOIiBUb3RhbD0iMTE2LjAwIiBUaXBvRGVDb21wcm9iYW50ZT0iSSIgRXhwb3J0YWNpb249IjAxIiBNZXRvZG9QYWdvPSJQVUUiIEx1Z2FyRXhwZWRpY2lvbj0iMjYwMTUiPjxjZmRpOkVtaXNvciBSZmM9IkVLVTkwMDMxNzNDOSIgTm9tYnJlPSJFU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YiIFJlZ2ltZW5GaXNjYWw9IjYwMSIvPjxjZmRpOlJlY2VwdG9yIFJmYz0iVVJFMTgwNDI5VE02IiBOb21icmU9IlVOSVZFUlNJREFEIFJPQk9USUNBIEVTUEFOT0xBIiBEb21pY2lsaW9GaXNjYWxSZWNlcHRvcj0iNjUwMDAiIFJlZ2ltZW5GaXNjYWxSZWNlcHRvcj0iNjAxIiBVc29DRkRJPSJHMDMiLz48Y2ZkaTpDb25jZXB0b3M+PGNmZGk6Q29uY2VwdG8gQ2xhdmVQcm9kU2Vydj0iMDEwMTAxMDEiIENhbnRpZGFkPSIxIiBDbGF2ZVVuaWRhZD0iRTQ4IiBEZXNjcmlwY2lvbj0iU2VydmljaW8iIFZhbG9yVW5pdGFyaW89IjEwMC4wMCIgSW1wb3J0ZT0iMTAwLjAwIiBPYmpldG9JbXA9IjAyIj48Y2ZkaTpJbXB1ZXN0b3M+PGNmZGk6VHJhc2xhZG9zPjxjZmRpOlRyYXNsYWRvIEJhc2U9IjEwMC4wMCIgSW1wdWVzdG89IjAwMiIgVGlwb0ZhY3Rvcj0iVGFzYSIgVGFzYU9DdW90YT0iMC4xNjAwMDAiIEltcG9ydGU9IjE2LjAwIi8+PC9jZmRpOlRyYXNsYWRvcz48L2NmZGk6SW1wdWVzdG9zPjwvY2ZkaTpDb25jZXB0bz48L2NmZGk6Q29uY2VwdG9zPjxjZmRpOkltcHVlc3RvcyBUb3RhbEltcHVlc3Rvc1RyYXNsYWRhZG9zPSIxNi4wMCI+PGNmZGk6VHJhc2xhZG9zPjxjZmRpOlRyYXNsYWRvIEJhc2U9IjEwMC4wMCIgSW1wdWVzdG89IjAwMiIgVGlwb0ZhY3Rvcj0iVGFzYSIgVGFzYU9DdW90YT0iMC4xNjAwMDAiIEltcG9ydGU9IjE2LjAwIi8+PC9jZmRpOlRyYXNsYWRvcz48L2NmZGk6SW1wdWVzdG9zPjwvY2ZkaTpDb21wcm9iYW50ZT4=",
    "idempotency_key": "erp-2026-08-11-A-1"
  }'
```

| Campo             | Requerido | Descripción                                                                                                                    |
| ----------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `xml_base64`      | Sí        | El nodo `cfdi:Comprobante` completo, **sin sellar**, codificado en base64 UTF-8. Máximo 2 MB una vez decodificado.             |
| `idempotency_key` | No        | Ver [Idempotencia y recuperación tras timeout](/recuperacion-timeouts). Máximo 100 caracteres ASCII imprimibles, sin espacios. |

<Warning>
  El ejemplo de arriba es real y **se puede timbrar tal cual** en sandbox, pero su `Fecha`
  está fija (`2026-08-11T12:00:00`). El SAT sólo acepta un comprobante fechado dentro de una
  ventana de **72 horas hacia atrás y 5 minutos hacia adelante** respecto al momento en que se
  timbra — así que para usarlo: decodifica el base64, ajusta `Fecha` a un valor dentro de esa
  ventana, y vuelve a codificar a base64 antes de enviarlo.
</Warning>

La respuesta es la misma `TimbrarResponse` de `POST /cfdi/timbrar`, sin importar si el
documento era un ingreso, egreso o complemento de pago.

## Ruteo por tipo de comprobante

Ipsofactura lee `TipoDeComprobante` del propio XML para decidir el pipeline:

* **`I` (ingreso) / `E` (egreso)** → se timbra directo, igual que `POST /cfdi/timbrar`.
* **`P` (pago)** → se rutea al pipeline de complemento de pago: la parcialidad y el saldo
  se recalculan a partir del historial de pagos ya registrados contra los CFDI que el
  documento relaciona, no de lo que el XML declare. Si necesitas declarar tú los saldos
  —porque llevas tu propia contabilidad, o aplicaste un egreso antes del REP— usa
  `POST /cfdi/complemento-pago`, que sí los
  [acepta explícitos](/complemento-pago#parcialidad-y-saldos-derivados-o-explícitos).

La ventana de 72 h / 5 min del SAT se mide sobre la `Fecha` **del documento**, no sobre el
instante en que llega la request — si tu ERP despacha una cola con retraso, re-fecha el XML
antes de reenviarlo.

## Tope de tamaño

El XML decodificado no puede exceder **2 MB** (dos órdenes de magnitud por encima del CFDI
más grande que hemos visto). Por encima del tope, la respuesta es `413 REQUEST_TOO_LARGE`.

## Rechazos con 400

| Código                     | Cuándo                                                                                                                                                                                                                    |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_BASE64`           | `xml_base64` no es base64 válido.                                                                                                                                                                                         |
| `INVALID_XML`              | El XML está mal formado, o la raíz no es un `cfdi:Comprobante` en el namespace de CFDI 4.0.                                                                                                                               |
| `XSD_VALIDATION_ERROR`     | El documento no cumple los esquemas del SAT (CFDI 4.0 / Pagos 2.0). El `message` trae hasta **3** errores `cvc-*`, cada uno con línea y columna.                                                                          |
| `UNSUPPORTED_CFDI_CONTENT` | El documento ya trae `Sello` (debe llegar sin sellar), trae `cfdi:Addenda`, o su `cfdi:Complemento` no es Pagos 2.0 (por ejemplo Carta Porte o Nómina).                                                                   |
| `VALIDATION_ERROR`         | Reglas de negocio del dominio: por ejemplo un ingreso/egreso sin `FormaPago`/`MetodoPago`, o un tipo P que trae `cfdi:CfdiRelacionados` (en Pagos 2.0 los CFDI que se liquidan van dentro de `DoctoRelacionado`, no ahí). |

```json XSD_VALIDATION_ERROR theme={null}
{
  "code": "XSD_VALIDATION_ERROR",
  "message": "The CFDI does not satisfy the SAT schema (CFDI 4.0 / Pagos 2.0): line 1, column 842: cvc-attribute.3: The value '99.9' of attribute 'TasaOCuota' on element 'cfdi:Traslado' is not valid with respect to its type"
}
```

```json UNSUPPORTED_CFDI_CONTENT theme={null}
{
  "code": "UNSUPPORTED_CFDI_CONTENT",
  "message": "The comprobante already carries a 'Sello'. Send it unsigned: ipsofactura seals the CFDI with the emisor's CSD before stamping it, and will not re-sign a document sealed elsewhere"
}
```

## Sandbox

En el [sandbox](/sandbox), este endpoint devuelve un CFDI **completo** — construido con el
mismo conversor que usa producción, con `cfdi:Conceptos`, `cfdi:Impuestos` y su
`TimbreFiscalDigital`, no un stub recortado. La única diferencia es que el material de
sellado es obviamente falso:

* `Sello` y `Certificado` llevan marcadores `SANDBOX_…` (no son una firma real).
* `NoCertificado` reutiliza el serial real del CSD que registraste, así que sigue teniendo
  forma de dato válido.
* La validación criptográfica de ese sello **falla a propósito** — un CFDI de sandbox no
  tiene, ni debe tener, validez fiscal.
* El PDF que acompaña al CFDI de sandbox sigue siendo un placeholder de una página.

Esto te deja probar tu parser contra la forma real de un CFDI timbrado, sin arriesgar que tu
integración sólo funcione contra un documento simplificado que producción nunca envía.

## Ver también

* [Idempotencia y recuperación tras timeout](/recuperacion-timeouts) — cómo reintentar sin
  riesgo de timbrar dos veces.
* [`GET /cfdi/{id}`](/api-reference/cfdi/consultar-cfdi) y los filtros de
  [`GET /cfdi`](/api-reference/cfdi/listar-cfdis) — para recuperar un CFDI por `serie`,
  `folio`, `rfc_emisor` o `folio_fiscal`.
