# Timbrado CFDI 4.0

> Timbra, consulta, cancela y genera el PDF de CFDI 4.0 ante el SAT. Sube tu CSD y factura en minutos

API publicada en idoo.dev por Jonathan Fonseca (categoría: Facturación).
Página: https://idoo.dev/apis/timbrado-cfdi-4-0

## Cómo llamar a la API

- **URL:** `https://idoo.dev/v1/timbrado-cfdi-4-0/{ruta}` — cualquier método HTTP; query, body y `Content-Type` llegan intactos a la API.
- **Autenticación:** header `Authorization: Bearer {api_key}` (o `X-API-KEY: {api_key}`). Una sola key de idoo.dev sirve para todas las APIs a las que estés suscrito.
- **Ambiente según la key:** `idoo_sk_test_...` → sandbox (gratis, no descuenta cuota); `idoo_sk_live_...` → producción.
- **Seguridad:** guarda la key en una variable de entorno (p. ej. `IDOO_API_KEY`) y llama a la API solo desde el servidor; nunca la expongas en el frontend ni la subas al repositorio.
- **Obtener acceso:** inicia sesión en https://idoo.dev/login, suscríbete a la API en https://idoo.dev/apis/timbrado-cfdi-4-0 y crea tu key en https://idoo.dev/panel/apikeys.

## Respuestas

La respuesta de la API se devuelve **tal cual** (status, headers y body del proveedor). Solo los errores del propio gateway usan este sobre JSON:

```json
{"valid": false, "status": 402, "message": "…", "data": null, "timestamp": "2026-01-01 12:00:00"}
```

| Status | Causa | Qué hacer |
|---|---|---|
| 401 | No se envió la key | Agrega el header `Authorization: Bearer {api_key}` |
| 403 | Key inválida o revocada, o sin suscripción activa a la API | Revisa la key y la suscripción en el panel |
| 402 | Cuota mensual agotada (`data.periodo_reinicia` indica cuándo se renueva) | Mejora el plan o compra un paquete; no reintentes |
| 404 | La API no existe o no está publicada | Revisa el slug de la URL |
| 429 | Rate limit por minuto excedido | Espera los segundos del header `Retry-After` y reintenta |
| 502 | La API no respondió o no tiene URL para tu ambiente | Reintenta con backoff; si persiste, contacta al creador |

Headers útiles en cada respuesta: `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-Idoo-Cuota-Restante` (si el plan tiene cuota).
Solo descuentan cuota las llamadas de producción que la API responde con 2xx/3xx; los errores no se cobran.

## Planes

| Plan | Precio (MXN) | Llamadas/mes | Peticiones/min |
|---|---|---|---|
| Prueba Gratis | Gratis | 10 | 60 |
| Basico | $150.00/mes | 30 | 60 |
| Pro | $300.00/mes | 300 | 60 |
| Ultra | $3,000.00/mes | 2,799 | 180 |
| Ultra + | $10,000.00/mes | 12,500 | 360 |

## Documentación

## Timbrado CFDI 4.0

API para timbrar, consultar, cancelar y generar el PDF de Comprobantes Fiscales Digitales por Internet (**CFDI 4.0**) ante el SAT a través de PAC autorizado, así como administrar los emisores (RFC + CSD) asociados a tu cuenta.

> ⚠️ **¿Vas a probar en sandbox? Primero descarga el CSD de pruebas.**
> Tu cuenta **no trae ningún emisor precargado**, ni siquiera en sandbox. Antes de timbrar cualquier cosa tienes que descargar los certificados de prueba del SAT desde nuestro Drive y registrarlos con `emisores/subir_csd`. Sin ese paso, todos los ejercicios de esta guía fallan con `404` (*el emisor no está registrado en tu cuenta*).
>
> 📥 **[Descargar CSD de pruebas (RFC `EKU9003173C9`)](https://drive.google.com/file/d/1QES7XOqySU-aainYwtnCCtNUJ0Z1gSUf/view?usp=sharing)**
>
> Los pasos completos están en **[Antes de empezar: registra el CSD de pruebas](#antes-de-empezar-registra-el-csd-de-pruebas)**.

### URL base y autenticación

Todas las peticiones van al gateway de idoo.dev con tu API key:

```
https://api.idoo.dev/v1/timbrado-cfdi-4-0/{modulo}/{accion}
```

```bash
curl "https://api.idoo.dev/v1/timbrado-cfdi-4-0/emisores/listado" \
  -H "Authorization: Bearer {tu_api_key}"
```

También se acepta el header `X-API-KEY: {tu_api_key}`. Las keys se crean en tu [panel de idoo.dev](/panel/apikeys).

### Ambientes: sandbox vs producción

El ambiente lo determina tu API key, no un parámetro:

| Key | Ambiente | Comportamiento |
|---|---|---|
| `idoo_sk_test_...` | **Sandbox** | Timbra contra el ambiente de pruebas del PAC. Ilimitado y gratis: no consume la cuota de tu plan. **Requiere el CSD de pruebas descargado del Drive.** |
| `idoo_sk_live_...` | **Producción** | Timbra ante el SAT. Cada timbrado exitoso descuenta 1 timbre de tu plan mensual. **Requiere el CSD real de tu empresa.** |

Los emisores (CSD) y las facturas están **separados por ambiente**: un emisor cargado en sandbox no existe en producción, y viceversa. En la práctica eso significa:

- En **sandbox** solo funcionan los certificados de prueba del SAT. Tu CSD real no sirve aquí.
- En **producción** solo funciona tu CSD real. Los certificados de prueba no sirven ahí.
- Cuando pases a producción tendrás que **volver a subir** un CSD, esta vez el tuyo.

Los seis endpoints de timbrado —`facturas/timbrar`, `facturas/timbrar_arreglo`, `facturas/timbrar_complemento`, `facturas/timbrar_complemento_arreglo`, `facturas/timbrar_nomina_arreglo` y `facturas/timbrar_carta_porte_arreglo`— descuentan cuota, y **solo cuando el timbrado es exitoso**: un CFDI rechazado por validación no te cuesta. Ten presente que **un complemento de pago consume un timbre igual que una factura**, así que una venta a crédito pagada en tres parcialidades gasta cuatro timbres en total.

Consultas, PDF, cancelaciones y administración de emisores son libres (aplican únicamente al rate limit de tu plan). El header `X-Idoo-Cuota-Restante` de cada respuesta cobrable te dice cuántos timbres te quedan en el periodo.

### Antes de empezar: registra el CSD de pruebas

Este paso se hace **una sola vez** por cuenta y es **obligatorio** para usar sandbox. Toma un par de minutos.

#### Paso 1. Descarga los certificados

📥 **[Descargar CSD de pruebas del SAT (RFC `EKU9003173C9`)](https://drive.google.com/file/d/1QES7XOqySU-aainYwtnCCtNUJ0Z1gSUf/view?usp=sharing)**

Descomprime el archivo. Necesitas tres cosas de él:

| Qué | Para qué |
|---|---|
| El archivo `.cer` | Certificado público del emisor de pruebas. |
| El archivo `.key` | Llave privada con la que se sellan los comprobantes. |
| La contraseña de la llave | Va en `contrasena_csd`. Para el CSD de pruebas del SAT es `12345678a`. |

> Son certificados **públicos de prueba** que el propio SAT publica para que los desarrolladores integren. No amparan operaciones reales y solo funcionan en sandbox.

#### Paso 2. Conviértelos a Base64

La API recibe el contenido binario de cada archivo en Base64, en una sola línea. Ajusta los nombres de archivo a los que descargaste:

```bash
# Linux
CER=$(base64 -w 0 EKU9003173C9.cer)
KEY=$(base64 -w 0 EKU9003173C9.key)

# macOS
CER=$(base64 -i EKU9003173C9.cer)
KEY=$(base64 -i EKU9003173C9.key)
```

```powershell
# Windows (PowerShell)
$CER = [Convert]::ToBase64String([IO.File]::ReadAllBytes("EKU9003173C9.cer"))
$KEY = [Convert]::ToBase64String([IO.File]::ReadAllBytes("EKU9003173C9.key"))
```

#### Paso 3. Registra el emisor con tu key de sandbox

Usa tu key `idoo_sk_test_...`. Si usas la de producción, el CSD de pruebas se rechaza.

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/emisores/subir_csd" \
  -H "Authorization: Bearer {tu_api_key_de_sandbox}" \
  -H "Content-Type: application/json" \
  -d "{
    \"archivo_cer_base64\": \"$CER\",
    \"archivo_key_base64\": \"$KEY\",
    \"contrasena_csd\": \"12345678a\",
    \"rfc\": \"EKU9003173C9\",
    \"razon_social\": \"ESCUELA KEMPER URGATE\",
    \"regimen_fiscal\": \"601\"
  }"
```

Debes recibir `200` con `"message": "Emisor guardado exitosamente."`.

#### Paso 4. Verifica que quedó registrado

```bash
curl "https://api.idoo.dev/v1/timbrado-cfdi-4-0/emisores/listado" \
  -H "Authorization: Bearer {tu_api_key_de_sandbox}"
```

Si en `data.emisores` aparece `EKU9003173C9` con `estatus: "activo"`, ya puedes correr todos los ejercicios de esta guía. Si la lista sale vacía, revisa que usaste la **misma key de sandbox** en los dos pasos.

### Formato de respuesta

Todas las respuestas (éxito o error) usan el mismo sobre JSON:

```json
{
  "valid": true,
  "status": 200,
  "message": "Factura timbrada exitosamente.",
  "data": { "...": "..." },
  "timestamp": "2026-07-24 12:00:00"
}
```

Cuando algo merece señalarse sin haber impedido la operación, `data` trae además un arreglo `advertencias`.

#### Reintentos: no gastas el timbre dos veces

Si nos vuelves a mandar **el mismo comprobante** —mismos datos y misma `Fecha`, de modo que el XML sellado sea idéntico— no se timbra otra vez: recuperamos el timbrado original del PAC y te devolvemos `200` con el mismo `uuid` y esta advertencia:

```json
{
  "advertencias": ["Este comprobante ya estaba timbrado: se recuperó el timbre original en vez de gastar otro."]
}
```

Eso cubre el caso feo de una petición que se corta después de que el PAC ya timbró. Reintentar es seguro: manda otra vez el mismo body. Si cambias la `Fecha`, el folio o cualquier importe, es un comprobante distinto y sí consume un timbre nuevo.

> **Excepción:** `POST facturas/pdf` devuelve el binario del PDF directamente (`Content-Type: application/pdf`), no el sobre JSON. Trátalo como descarga de archivo.

#### Códigos de estado

| Código | Significado |
|---|---|
| 200 | Operación exitosa. |
| 202 | Cancelación enviada, pendiente de aceptación del receptor. |
| 400 | Petición inválida: falta un campo o el PAC rechazó el timbrado (detalle en `data`). |
| 401 | No se envió API key. |
| 402 | Cuota de timbres del plan agotada (solo producción). |
| 403 | API key inválida o sin suscripción activa a esta API. |
| 404 | El recurso (emisor, factura) no existe o no pertenece a tu cuenta. **En sandbox, casi siempre significa que no has subido el CSD de pruebas.** |
| 405 | Método HTTP no permitido. |
| 409 | Conflicto — p. ej. cancelar una factura ya cancelada. |
| 422 | El CFDI no pasó las validaciones del esquema/reglas del SAT (detalle legible en `data`). |
| 429 | Excediste el rate limit por minuto de tu plan (`Retry-After` indica cuándo reintentar). |
| 500 | Error interno o falla de comunicación con el PAC. |

### Catálogo de endpoints

| Método | Endpoint | Descripción | ¿Descuenta cuota? |
|---|---|---|---|
| GET | `emisores/listado` | Lista los emisores (RFC + CSD) de tu cuenta. | No |
| POST | `emisores/subir_csd` | Registra o actualiza el CSD de un emisor. | No |
| POST | `emisores/eliminar` | Elimina un emisor y sus archivos. | No |
| POST | `facturas/timbrar` | Timbra un CFDI ya construido y sellado (XML en base64). | **Sí** |
| POST | `facturas/timbrar_arreglo` | Construye, sella y timbra un CFDI desde JSON: facturas, **notas de crédito** y demás egresos. | **Sí** |
| POST | `facturas/timbrar_complemento_arreglo` | Construye, sella y timbra un complemento de pago (REP) desde JSON. | **Sí** |
| POST | `facturas/timbrar_complemento` | Timbra un complemento de pago que envías como XML. | **Sí** |
| POST | `facturas/timbrar_nomina_arreglo` | Construye, sella y timbra un recibo de nómina 1.2 desde JSON. | **Sí** |
| POST | `facturas/timbrar_carta_porte_arreglo` | Construye, sella y timbra un CFDI con complemento Carta Porte 3.1 desde JSON. | **Sí** |
| GET | `facturas/consultar` | Estatus de una factura (local + SAT). | No |
| POST | `facturas/pdf` | Representación impresa (PDF) del CFDI. | No |
| POST | `facturas/cancelar` | Cancela un CFDI ante el SAT. | No |

---

### GET emisores/listado

Lista los emisores registrados en tu cuenta, ordenados por vencimiento del certificado. Solo muestra los del ambiente de tu key: con una key de sandbox verás los emisores de prueba; con una de producción, los reales.

**Respuesta 200:**

```json
{
  "data": {
    "total": 1,
    "emisores": [
      {
        "rfc": "EKU9003173C9",
        "razon_social": "ESCUELA KEMPER URGATE",
        "regimen_fiscal": "601",
        "fecha_vencimiento": "2027-05-18",
        "dias_restantes": 318,
        "estatus": "activo"
      }
    ]
  }
}
```

`estatus` es calculado: `vencido` si `dias_restantes <= 0`, `por_vencer` si faltan 30 días o menos, `activo` en cualquier otro caso. Monitorea `por_vencer` para renovar CSDs a tiempo.

### POST emisores/subir_csd

Registra (o actualiza, si el RFC ya existe en tu cuenta) el Certificado de Sello Digital de un emisor. Es **requisito previo** para timbrar con ese RFC. El certificado se valida técnicamente y se registra ante el PAC antes de guardarse.

> **En sandbox** sube el CSD de pruebas que descargas del [Drive](https://drive.google.com/file/d/1QES7XOqySU-aainYwtnCCtNUJ0Z1gSUf/view?usp=sharing) (ver [Antes de empezar](#antes-de-empezar-registra-el-csd-de-pruebas)). **En producción** sube el CSD real de tu empresa, el que tramitaste ante el SAT.

**Body:**

```json
{
  "archivo_cer_base64": "MIIF...",
  "archivo_key_base64": "MIIE...",
  "contrasena_csd": "contraseña-del-csd",
  "rfc": "EKU9003173C9",
  "razon_social": "ESCUELA KEMPER URGATE",
  "regimen_fiscal": "601"
}
```

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `archivo_cer_base64` | string | Sí | Contenido binario del `.cer` en Base64. |
| `archivo_key_base64` | string | Sí | Contenido binario del `.key` en Base64. |
| `contrasena_csd` | string | Sí | Contraseña de la llave privada. |
| `rfc` | string | Sí | RFC del emisor; debe coincidir con el del certificado. |
| `razon_social` | string | No | Razón social a mostrar. |
| `regimen_fiscal` | string | No | Clave de régimen fiscal SAT (ej. `601`). |

**Respuesta 200:**

```json
{
  "data": { "rfc": "EKU9003173C9", "razon_social": "ESCUELA KEMPER URGATE", "vence": "2027-05-18 18:08:08", "tipo": "CSD" },
  "message": "Emisor guardado exitosamente."
}
```

**Errores comunes:** `400` si falta un campo, el RFC no coincide con el certificado, o `.cer`/`.key`/contraseña no embonan; `502` si el PAC no pudo registrar el CSD.

### POST emisores/eliminar

**Body:** `{ "rfc": "EKU9003173C9" }`

**Respuesta 200:** `{ "data": { "rfc": "EKU9003173C9" }, "message": "Emisor y archivos asociados eliminados correctamente" }`

`404` si el RFC no existe en tu cuenta (en el ambiente de tu key).

### POST facturas/timbrar

Timbra un CFDI que **tú ya construiste y sellaste** (trae `Sello` y `NoCertificado`). Si prefieres que el servicio construya y selle por ti, usa `timbrar_arreglo`.

Para un complemento de pago usa `facturas/timbrar_complemento`: acepta el XML sin sellar y revisa que los saldos cuadren antes de gastar el timbre.

**Body:**

```json
{
  "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
  "CondicionPago": "30 días"
}
```

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `xml_base64` | string | Sí | XML del CFDI 4.0 ya sellado, en Base64. |
| `CondicionPago` | string | No | Se inyecta como atributo del nodo `Comprobante` (sobrescribe el existente). |

**Respuesta 200:**

```json
{
  "data": {
    "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
    "rfc_emisor": "EKU9003173C9",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi..."
  },
  "message": "Factura timbrada exitosamente."
}
```

`400` si el Base64 es inválido o el PAC reporta alertas (detalle como arreglo de strings `"[código] mensaje"` en `data`).

### POST facturas/timbrar_arreglo

**La forma recomendada de integrarse.** Construye el CFDI 4.0 desde un JSON, lo sella con el CSD del emisor guardado, lo valida contra los esquemas del SAT, lo timbra y devuelve XML + PDF.

**Body (ejemplo completo):**

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": {
    "Serie": "A",
    "Folio": "1024",
    "FormaPago": "01",
    "MetodoPago": "PUE",
    "Moneda": "MXN",
    "TipoDeComprobante": "I",
    "LugarExpedicion": "20928",
    "Exportacion": "01"
  },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605",
    "UsoCFDI": "S01"
  },
  "conceptos": [
    {
      "ClaveProdServ": "10101502",
      "Cantidad": "1",
      "ClaveUnidad": "KGM",
      "Descripcion": "MAIZ",
      "ValorUnitario": "1000.00",
      "Importe": "1000.00",
      "ObjetoImp": "02",
      "traslados": [
        { "Base": "1000.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "160.00" }
      ],
      "retenciones": []
    }
  ]
}
```

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `rfc_emisor` | string | Sí | RFC del emisor; debe existir en tu cuenta con CSD activo y vigente. |
| `comprobante` | object | Sí | Atributos del nodo `Comprobante` (Serie, Folio, FormaPago, MetodoPago, Moneda, TipoDeComprobante, LugarExpedicion...). Si no envías `Fecha`, se genera con la hora local de tu `LugarExpedicion`. |
| `receptor` | object | Sí | Atributos del nodo `Receptor` (Rfc, Nombre, DomicilioFiscalReceptor, RegimenFiscalReceptor, UsoCFDI). |
| `conceptos` | array | Sí | Conceptos con atributos estándar del nodo `Concepto`, más arreglos opcionales `traslados` y `retenciones`. |
| `informacion_global` | object | No | `{ "Periodicidad": "04", "Meses": "07", "Año": "2026" }` para una factura global. Ver **Factura global** abajo. |
| `relacionados` | object \| array | No | Uno o varios grupos `{ "TipoRelacion": "01", "uuids": ["..."] }`. Ver **Relacionar el CFDI con otros** abajo. |
| `CondicionesDePago` | string | No | Se agrega como atributo del comprobante. |

Los valores deben cumplir los catálogos oficiales del SAT para CFDI 4.0; el servicio solo rellena `Fecha` y `Exportacion` (default `01`) por ti.

> **Sobre la `Fecha`:** el SAT la exige en la hora local del lugar de expedición y no admite comprobantes fechados en el futuro. Si la omites, se genera con el huso que corresponde a tu `LugarExpedicion` —Sinaloa, Sonora, BCS, Baja California, Nayarit, Quintana Roo y Ciudad Juárez no van con la hora del centro—. Si prefieres mandarla tú, mándala en la hora local de ese código postal.

> Si emites con `MetodoPago: "PPD"`, recuerda que la `FormaPago` debe ser `99` y que tendrás que emitir un complemento de pago con `facturas/timbrar_complemento_arreglo` por cada cobro que recibas.

#### Factura global (ventas al público en general)

Cuando amparas en un solo comprobante las ventas de un periodo a clientes que no pidieron factura, agrega el nodo `InformacionGlobal` con el periodo que cubre:

```json
{
  "comprobante": {
    "Serie": "G", "Folio": "501",
    "FormaPago": "01", "MetodoPago": "PUE",
    "Moneda": "MXN", "TipoDeComprobante": "I", "LugarExpedicion": "80290",
    "InformacionGlobal": { "Periodicidad": "04", "Meses": "07", "Año": "2026" }
  }
}
```

| Campo | Descripción |
|---|---|
| `Periodicidad` | `01` diario, `02` semanal, `03` quincenal, `04` mensual, `05` bimestral |
| `Meses` | `01`–`12` para un mes; `13`–`18` para un bimestre (`13` Enero-Febrero … `18` Noviembre-Diciembre) |
| `Año` | Cuatro dígitos. También se acepta escribirlo `Anio` |

**El receptor no lo mandas**: manda `"receptor": {}` (u omítelo) y se completa con lo que el SAT fija para una global —RFC `XAXX010101000`, nombre `PUBLICO EN GENERAL`, régimen `616` y uso `S01`—, con el `DomicilioFiscalReceptor` igual a tu `LugarExpedicion`. Si envías alguno de esos campos con otro valor, se rechaza con `422` antes de gastar el timbre; también se rechaza una periodicidad o un mes fuera de catálogo, un bimestre declarado como mes (o al revés) y un año que todavía no ocurre.

El nodo se acepta dentro de `comprobante` o en la raíz del body como `informacion_global`. El PDF imprime el periodo en un bloque propio ("Mensual · Julio 2026").

#### Relacionar el CFDI con otros

Lo necesitas en dos casos habituales: una **nota de crédito** (`TipoDeComprobante: "E"`), que se relaciona con la factura que corrige (`TipoRelacion` `01` o `03`), y una **sustitución** (`TipoRelacion: "04"`), obligatoria después de cancelar un CFDI con motivo `01`.

```json
{
  "relacionados": { "TipoRelacion": "01", "uuids": ["81B3033F-6F7B-5D24-AE32-1F1DE4ABE354"] }
}
```

El SAT admite más de un tipo de relación por comprobante; para eso manda un arreglo de grupos:

```json
{
  "relacionados": [
    { "TipoRelacion": "01", "uuids": ["81B3033F-6F7B-5D24-AE32-1F1DE4ABE354"] },
    { "TipoRelacion": "04", "uuids": ["8179B395-B827-5F92-92FF-BC38DD268C64"] }
  ]
}
```

El campo se acepta en la raíz del body o dentro de `comprobante` como `CfdiRelacionados`, que es donde vive el nodo en el XML del SAT. Los folios pueden ir bajo `uuids` o `CfdiRelacionado`, y como `["UUID"]` o `[{ "UUID": "..." }]`. Si repites el mismo `TipoRelacion` en dos grupos se fusionan en un solo nodo, porque el SAT no admite el nodo duplicado.

Valores de `TipoRelacion` (catálogo `c_TipoRelacion`):

| Clave | Significado |
|---|---|
| `01` | Nota de crédito de los documentos relacionados |
| `02` | Nota de débito de los documentos relacionados |
| `03` | Devolución de mercancía sobre facturas o traslados previos |
| `04` | Sustitución de los CFDI previos |
| `05` | Traslados de mercancías facturados previamente |
| `06` | Factura generada por los traslados previos |
| `07` | CFDI por aplicación de anticipo |

**Reglas que se aplican antes de llamar al PAC**, todas con `422` y sin gastar timbre:

- Un `TipoDeComprobante: "E"` **debe** traer `relacionados`. Una nota de crédito que no dice qué corrige es lo primero que observa el SAT en una revisión.
- `TipoRelacion` tiene que estar en el catálogo y los folios tener forma de UUID.
- El folio relacionado no puede ser de otro emisor de tu cuenta.
- Un CFDI **cancelado** solo se puede relacionar con `TipoRelacion: "04"` (sustitución); con cualquier otro tipo se rechaza.

Si el folio no aparece en tu historial de timbrado —porque la factura original se emitió con otro sistema— **no se bloquea**: se timbra y la respuesta trae un arreglo `advertencias` diciéndolo. El estatus que se consulta es el que tenemos registrado; si cancelaste el original por fuera y nadie ha llamado a `facturas/consultar`, aquí seguirá como vigente.

**Respuesta 200:**

```json
{
  "data": {
    "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
    "rfc_emisor": "EKU9003173C9",
    "total": "1160.00",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "Factura timbrada exitosamente."
}
```

`data.advertencias` aparece solo cuando hay algo que señalar sin haber impedido el timbrado, por ejemplo un folio relacionado que no está en tu historial.

#### Lo que se revisa antes de gastar el timbre

- El `Importe` de cada concepto debe corresponder a `Cantidad × ValorUnitario`, con la misma tolerancia que aplica el SAT: una unidad de la última posición decimal de la moneda (`0.01` en pesos).
- Los grupos de `relacionados`: `TipoRelacion` dentro del catálogo y folios con forma de UUID.
- Que en `comprobante`, `receptor` y cada concepto no viaje una lista u objeto donde el XML espera un atributo de texto (salvo `traslados` y `retenciones`, que sí son nodos hijos).

Todo esto devuelve `422` con el campo señalado, sin llamar al PAC.

**Errores:**

- `400` — faltan campos, el emisor no existe/está inactivo, o su CSD está vencido.
- `422` — el CFDI no pasó las validaciones del SAT o las previas; `data` trae mensajes legibles por campo:

```json
[
  "[CFDI40101] Campo 'FormaPago': el valor '99a' no es válido. Valores permitidos: 01, 02, 03, ...",
  "Concepto 1 ('MAIZ'): el Importe declarado (100.50) no corresponde a Cantidad × ValorUnitario (3 × 33.33 = 99.99). La diferencia máxima permitida en MXN es de 0.01."
]
```

- `402` — cuota de timbres agotada (producción). Mejora tu plan para desbloquear al instante.
- `500` — el PAC rechazó el timbrado u otro error interno.

### POST facturas/timbrar_complemento_arreglo

Emite un **CFDI de tipo P: el complemento de recepción de pagos 2.0 (REP)**, el comprobante que acredita que cobraste una factura emitida a crédito.

Lo necesitas cuando facturaste con `MetodoPago: "PPD"` (pago en parcialidades o diferido). El SAT te obliga a emitir un REP por cada cobro que recibas, **a más tardar el quinto día natural del mes siguiente** al que recibiste el pago. Si tu venta fue de contado (`PUE`) no hace falta ninguno.

Igual que `timbrar_arreglo`, este endpoint construye el XML, lo sella con el CSD guardado del emisor, lo valida y lo timbra. Solo tienes que enviarle lo que únicamente tú sabes: **cuándo, cómo y cuánto te pagaron, y contra qué facturas se aplica.**

**Body (ejemplo completo):**

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": {
    "Serie": "PAG",
    "Folio": "310",
    "LugarExpedicion": "80290"
  },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "pagos": [
    {
      "FechaPago": "2026-08-01T10:00:00",
      "FormaDePagoP": "03",
      "MonedaP": "MXN",
      "NumOperacion": "REF-88451203",
      "RfcEmisorCtaOrd": "BBA830831LJ2",
      "CtaOrdenante": "0123456789",
      "RfcEmisorCtaBen": "BMN930209927",
      "CtaBeneficiario": "9876543210",
      "documentos": [
        {
          "IdDocumento": "C1855BEE-324E-55EB-9154-91D890F31469",
          "Serie": "FAC",
          "Folio": "20458",
          "NumParcialidad": "1",
          "ImpSaldoAnt": "11600.00",
          "ImpPagado": "5800.00",
          "traslados": [
            { "BaseDR": "5000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "800.00" }
          ]
        }
      ]
    }
  ]
}
```

#### Campos

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `rfc_emisor` | string | Sí | RFC del emisor; debe existir en tu cuenta con CSD activo y vigente. |
| `comprobante` | object | Sí | Serie, Folio y `LugarExpedicion`. Si no envías `Fecha`, se genera con la hora local de tu `LugarExpedicion`. |
| `receptor` | object | Sí | Rfc, Nombre, `DomicilioFiscalReceptor` y `RegimenFiscalReceptor` del cliente que pagó. |
| `pagos` | array | Sí | Uno o más pagos recibidos. Cada uno puede aplicarse a varias facturas. |
| `relacionados` | object \| array | No | `{ "TipoRelacion": "04", "uuids": ["..."] }` — úsalo cuando este REP sustituye a otro cancelado. Acepta las mismas formas que en `timbrar_arreglo`; si omites `TipoRelacion` se asume `04`. |

**Cada elemento de `pagos`:**

| Campo | Obligatorio | Descripción |
|---|---|---|
| `FechaPago` | Sí | `AAAA-MM-DDThh:mm:ss`. No puede ser posterior a la fecha del comprobante. |
| `FormaDePagoP` | Sí | Clave del catálogo `c_FormaPago` (`03` transferencia, `01` efectivo, `04` tarjeta de crédito...). **No se admite `99`**: aquí ya sabes cómo te pagaron. |
| `MonedaP` | Sí | Moneda en que recibiste el dinero (`MXN`, `USD`...). |
| `TipoCambioP` | Condicional | Obligatorio si `MonedaP` no es `MXN`. Si es `MXN` se pone `1` solo. |
| `Monto` | No | Importe recibido. Si lo omites se calcula sumando lo aplicado a cada factura. |
| `NumOperacion` | Recomendado | Referencia bancaria, folio de la transferencia o autorización de la tarjeta. |
| `RfcEmisorCtaOrd`, `CtaOrdenante`, `NomBancoOrdExt` | No | Banco y cuenta de quien pagó. |
| `RfcEmisorCtaBen`, `CtaBeneficiario` | No | Banco y cuenta donde recibiste. |
| `documentos` | Sí | Facturas que se liquidan con este pago (al menos una). |

**Cada elemento de `documentos`:**

| Campo | Obligatorio | Descripción |
|---|---|---|
| `IdDocumento` | Sí | UUID de la factura que estás cobrando. |
| `Serie`, `Folio` | No | Serie y folio de esa factura, para que se lean en el PDF. |
| `NumParcialidad` | Sí | Número de pago sobre esa factura: `1` el primero, `2` el segundo... Si lo omites se asume `1`. |
| `ImpSaldoAnt` | Sí | Saldo pendiente **antes** de este pago. En la primera parcialidad es el total de la factura. |
| `ImpPagado` | Sí | Cuánto se aplica de este pago a esa factura. |
| `ImpSaldoInsoluto` | No | Saldo que queda. Se calcula como `ImpSaldoAnt − ImpPagado` si no lo envías. |
| `MonedaDR` | No | Moneda de la factura. Se asume la del pago. |
| `EquivalenciaDR` | Condicional | Tipo de cambio entre la moneda de la factura y la del pago. Obligatorio si difieren; si coinciden se pone `1`. |
| `ObjetoImpDR` | No | `02` si la factura causa impuestos, `01` si no. Se deduce de si mandaste impuestos. |
| `traslados` | Condicional | IVA/IEPS trasladados, **en proporción a lo pagado**, no al total de la factura. |
| `retenciones` | No | ISR/IVA retenidos, también en proporción. |

Cada traslado lleva `BaseDR`, `ImpuestoDR`, `TipoFactorDR`, `TasaOCuotaDR` e `ImporteDR`; los exentos (`TipoFactorDR: "Exento"`) omiten los dos últimos. Las retenciones llevan los cinco siempre.

#### Lo que **no** tienes que enviar

Se arma solo, y mandarlo es error: `TipoDeComprobante` (`P`), `Moneda` (`XXX`), `SubTotal` y `Total` (cero — el importe real vive en el complemento), `Exportacion`, `UsoCFDI` (`CP01`), el concepto único obligatorio (clave `84111506`, "Pago"), el nodo `Totales` del complemento con sus totales por tasa, y los impuestos agregados del pago.

Un CFDI de pago **no admite** `FormaPago`, `MetodoPago`, `CondicionesDePago`, `Descuento` ni `TipoCambio` en el comprobante; si los envías recibes un `422`. La forma de pago va dentro de cada pago, como `FormaDePagoP`.

#### Los saldos los llevas tú

El servicio **no lleva el estado de cuenta de tus facturas**: `NumParcialidad`, `ImpSaldoAnt` e `ImpSaldoInsoluto` los envías tú, porque la cuenta por cobrar vive en tu sistema. Lo que sí hacemos es revisar que la aritmética cuadre **antes** de mandar nada al PAC, para que un descuadre te salga gratis en lugar de costarte un timbre:

- `ImpSaldoInsoluto` debe ser exactamente `ImpSaldoAnt − ImpPagado`.
- `ImpPagado` no puede exceder el saldo anterior, y debe ser mayor que cero.
- `ImporteDR` de cada traslado debe ser `BaseDR × TasaOCuotaDR`.
- `FechaPago` no puede ser posterior a la fecha del comprobante.
- El `IdDocumento` debe tener forma de UUID.

**Respuesta 200:**

```json
{
  "data": {
    "uuid": "DA025970-C9AB-5666-A090-D469BB6AB19D",
    "rfc_emisor": "EKU9003173C9",
    "monto_total_pagos": "5800.00",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "Complemento de pago timbrado exitosamente."
}
```

`monto_total_pagos` es la suma de lo cobrado (el `Total` del CFDI siempre es cero en un comprobante de pago).

**Errores:**

- `400` — faltan `rfc_emisor`, `receptor` o `pagos`; el emisor no existe o su CSD está vencido; el PAC rechazó el timbrado.
- `422` — el complemento no cuadra o incluye campos prohibidos. `data` trae un mensaje por problema, señalando la posición exacta:

```json
[
  "pagos[0].documentos[1].ImpSaldoInsoluto (1000.00) no cuadra: ImpSaldoAnt − ImpPagado = 0.00.",
  "pagos[0].FormaDePagoP: el complemento de pagos no admite la clave 99 (Por definir)."
]
```

- `402` — cuota de timbres agotada (producción).
- `404` — el emisor no está registrado en tu cuenta.

### POST facturas/timbrar_complemento

La misma emisión de REP, pero **tú generas el XML**. Úsalo si ya tienes un generador de CFDI y solo quieres el timbrado; si no, `timbrar_complemento_arreglo` es bastante más cómodo.

Acepta el XML **con o sin sello**:

- **Sin sellar** — lo sellamos con el CSD que tienes registrado. Es el caso normal cuando subiste tu CSD con `emisores/subir_csd`. Además validamos el comprobante contra los esquemas del SAT antes de timbrarlo.
- **Ya sellado** — lo mandamos tal cual al PAC, sin tocar nada (cualquier reescritura invalidaría tu sello). Requiere que conserves tu llave privada de tu lado.

En ambos casos verificamos antes de gastar el timbre que sea de tipo `P`, que traiga complemento de pagos 2.0, que el RFC emisor esté registrado en tu cuenta y que los saldos cuadren.

**Body:**

```json
{ "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi..." }
```

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `xml_base64` | string | Sí | XML del CFDI de tipo `P` con complemento Pagos 2.0, en Base64. Sellado u opcionalmente sin sellar. |

**Respuesta 200:**

```json
{
  "data": {
    "uuid": "2AAD6C0D-2440-5E71-9344-BDBDF75DA0C5",
    "rfc_emisor": "EKU9003173C9",
    "monto_total_pagos": "5800.00",
    "sellado_por_api": true,
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi..."
  },
  "message": "Complemento de pago timbrado exitosamente."
}
```

`sellado_por_api` te dice si el XML llegó sin sello y lo sellamos nosotros. Este endpoint no devuelve PDF; obtenlo después con `facturas/pdf` usando el `uuid`.

**Errores:**

- `400` — Base64 o XML inválido; el comprobante no es de tipo `P`; no trae complemento de pagos; el PAC lo rechazó.
- `404` — el RFC emisor del XML no está registrado en tu cuenta.
- `422` — los saldos no cuadran, o el XML sin sellar no pasó las validaciones del SAT.

### POST facturas/timbrar_nomina_arreglo

Emite un **CFDI de tipo N: el recibo de nómina 1.2**, el comprobante de lo que le pagas a un trabajador en un periodo.

Como en el resto de endpoints `_arreglo`, solo mandas lo que únicamente tú sabes —**el periodo, el empleado, y lo que le pagas y le retienes**—. Los totales del complemento, los del comprobante y el concepto único se calculan aquí.

**Body (ejemplo completo):**

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "NOM", "Folio": "4471", "LugarExpedicion": "80290" },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "nomina": {
    "TipoNomina": "O",
    "FechaPago": "2026-08-13",
    "FechaInicialPago": "2026-07-30",
    "FechaFinalPago": "2026-08-13",
    "NumDiasPagados": "15",
    "emisor": { "RegistroPatronal": "B5510768108" },
    "receptor": {
      "Curp": "MASO451221MSLRGL05",
      "NumSeguridadSocial": "04099312345",
      "FechaInicioRelLaboral": "2019-03-01",
      "Antigüedad": "P356W",
      "TipoContrato": "01",
      "Sindicalizado": "No",
      "TipoJornada": "01",
      "TipoRegimen": "02",
      "NumEmpleado": "EMP-0042",
      "Departamento": "Administración",
      "Puesto": "Coordinadora de operaciones",
      "RiesgoPuesto": "1",
      "PeriodicidadPago": "04",
      "SalarioBaseCotApor": "1233.33",
      "SalarioDiarioIntegrado": "1290.00",
      "ClaveEntFed": "SIN"
    },
    "percepciones": [
      { "TipoPercepcion": "001", "Clave": "P001", "Concepto": "Sueldos, salarios y rayas", "ImporteGravado": "18500.00" },
      { "TipoPercepcion": "021", "Clave": "P021", "Concepto": "Prima vacacional", "ImporteGravado": "700.00", "ImporteExento": "500.00" }
    ],
    "deducciones": [
      { "TipoDeduccion": "002", "Clave": "D002", "Concepto": "ISR", "Importe": "3120.45" },
      { "TipoDeduccion": "001", "Clave": "D001", "Concepto": "Seguridad social", "Importe": "640.18" }
    ],
    "incapacidades": [
      { "DiasIncapacidad": "2", "TipoIncapacidad": "02", "ImporteMonetario": "0.00" }
    ]
  }
}
```

#### Campos

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `rfc_emisor` | string | Sí | RFC del patrón; debe existir en tu cuenta con CSD activo y vigente. |
| `comprobante` | object | Sí | Serie, Folio y `LugarExpedicion`. Si no envías `Fecha`, se genera con la hora local de tu `LugarExpedicion`. |
| `receptor` | object | Sí | Datos fiscales del trabajador: Rfc, Nombre, `DomicilioFiscalReceptor` y `RegimenFiscalReceptor` (normalmente `605`). |
| `nomina` | object | Sí | El recibo en sí (tabla siguiente). |
| `relacionados` | object \| array | No | Para sustituir un recibo cancelado; si omites `TipoRelacion` se asume `04`. |

**Dentro de `nomina`:**

| Campo | Obligatorio | Descripción |
|---|---|---|
| `TipoNomina` | Sí | `O` ordinaria, `E` extraordinaria. |
| `FechaPago`, `FechaInicialPago`, `FechaFinalPago` | Sí | `AAAA-MM-DD` (solo la fecha, sin hora). El periodo no puede ir al revés. |
| `NumDiasPagados` | Sí | Días que ampara el recibo; mayor que cero. |
| `emisor` | Recomendado | `RegistroPatronal`, y `EntidadSNCF` si aplica. |
| `receptor` | Sí | Datos laborales del trabajador: `Curp`, `NumSeguridadSocial`, `TipoContrato`, `TipoRegimen`, `PeriodicidadPago`, `SalarioBaseCotApor`, `ClaveEntFed`… |
| `percepciones` | Condicional | Lista de `{TipoPercepcion, Clave, Concepto, ImporteGravado, ImporteExento}`. El importe que omitas se toma como `0.00`. Admite `horas_extra` y `acciones_o_titulos` por percepción. |
| `deducciones` | No | Lista de `{TipoDeduccion, Clave, Concepto, Importe}`. |
| `otros_pagos` | Condicional | Lista de `{TipoOtroPago, Clave, Concepto, Importe}`. El subsidio para el empleo (`002`) lleva además `"subsidio": { "SubsidioCausado": "..." }`. |
| `incapacidades` | No | Lista de `{DiasIncapacidad, TipoIncapacidad, ImporteMonetario}`. |
| `separacion_indemnizacion`, `jubilacion_pension_retiro` | Condicional | Obligatorios si pagas percepciones de esos tipos (ver abajo). |

Necesitas al menos una percepción o un otro pago.

#### Lo que **no** tienes que enviar

Se arma solo, y mandarlo es error: `TipoDeComprobante` (`N`), `Moneda` (`MXN`), `FormaPago` (`99`), `MetodoPago` (`PUE`), `Exportacion`, `UsoCFDI` (`CN01`), el concepto único obligatorio (clave `84111505`, "Pago de nómina"), el `SubTotal`, el `Descuento` y el `Total` del comprobante, y **todos los totales del complemento**:

- `TotalPercepciones`, `TotalDeducciones` y `TotalOtrosPagos` del nodo `Nomina`.
- `TotalGravado` y `TotalExento`, y el total por tipo de pago: `TotalSueldos`, `TotalSeparacionIndemnizacion` y `TotalJubilacionPensionRetiro`, según la clave de cada percepción.
- `TotalImpuestosRetenidos` (el ISR, clave de deducción `002`) y `TotalOtrasDeducciones` (todo lo demás).

Puedes enviar `TotalPercepciones`, `TotalDeducciones` o `TotalOtrosPagos` si quieres que verifiquemos tu cálculo: si no cuadran con la suma de los conceptos, recibes `422` sin gastar el timbre.

#### Lo que se revisa antes de gastar el timbre

- El periodo: fechas en formato `AAAA-MM-DD` y `FechaInicialPago` no posterior a `FechaFinalPago`.
- Que cada percepción, deducción, otro pago e incapacidad traiga su clave, su concepto y un importe no negativo.
- Que los totales que declaraste cuadren con la suma de los conceptos.
- Que las deducciones no superen a lo pagado: el total de un recibo no puede ser negativo.
- Que las percepciones de **separación o indemnización** (`022`, `023`, `025`) traigan `separacion_indemnizacion`, y las de **jubilación o pensión** (`039`, `044`) traigan `jubilacion_pension_retiro`.
- Que el **subsidio para el empleo** (`TipoOtroPago: "002"`) traiga su `SubsidioCausado`.

**Respuesta 200:**

```json
{
  "data": {
    "uuid": "9F0C1A44-6C34-5E52-9A2D-27C10E5F0B31",
    "rfc_emisor": "EKU9003173C9",
    "total_percepciones": "19700.00",
    "total_deducciones": "3760.63",
    "total_otros_pagos": "0.00",
    "neto_pagado": "15939.37",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "Recibo de nómina timbrado exitosamente."
}
```

El PDF trae el desglose de percepciones, deducciones, otros pagos e incapacidades, y etiqueta el total como "Neto a pagar".

**Errores:**

- `400` — faltan `rfc_emisor`, `receptor` o `nomina`; el emisor no existe o su CSD está vencido; el PAC rechazó el timbrado.
- `422` — el recibo no cuadra o incluye campos que se calculan solos; `data` trae un mensaje por problema señalando la posición exacta (`nomina.percepciones[0].Concepto: es obligatorio.`).
- `402` — cuota de timbres agotada (producción).
- `404` — el emisor no está registrado en tu cuenta.

### POST facturas/timbrar_carta_porte_arreglo

Emite un CFDI con **complemento Carta Porte 3.1**, el que ampara el traslado de mercancía por carretera. Cubre los dos casos reales, y el tipo de comprobante es lo único que los distingue:

| Caso | `TipoDeComprobante` | Quién lo emite |
|---|---|---|
| **Traslado** | `T` | El dueño de la mercancía, que la mueve con sus propios medios. No hay cobro: moneda `XXX` y totales en cero. |
| **Ingreso** | `I` | El transportista que **factura el flete**. Los conceptos y sus impuestos son los del servicio. |

> Esta versión cubre **autotransporte federal**. Marítimo, aéreo y ferroviario devuelven `422` diciéndolo, en vez de dejar que el PAC rechace el comprobante a mitad de la operación.

**Body (traslado de mercancía propia):**

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "TipoDeComprobante": "T", "Serie": "CP", "Folio": "120", "LugarExpedicion": "80290" },
  "receptor": { "DomicilioFiscalReceptor": "26670" },
  "carta_porte": {
    "TranspInternac": "No",
    "ubicaciones": [
      {
        "TipoUbicacion": "Origen",
        "RFCRemitenteDestinatario": "EKU9003173C9",
        "FechaHoraSalidaLlegada": "2026-08-14T08:00:00",
        "domicilio": { "Calle": "Carretera a Navolato km 4", "Municipio": "006", "Estado": "SIN", "Pais": "MEX", "CodigoPostal": "80290" }
      },
      {
        "TipoUbicacion": "Destino",
        "RFCRemitenteDestinatario": "MASO451221PM4",
        "FechaHoraSalidaLlegada": "2026-08-14T18:00:00",
        "DistanciaRecorrida": "842.5",
        "domicilio": { "Calle": "Avenida Vallarta 1200", "Municipio": "039", "Estado": "JAL", "Pais": "MEX", "CodigoPostal": "44100" }
      }
    ],
    "mercancias": [
      {
        "BienesTransp": "11121900", "Descripcion": "Maíz blanco a granel",
        "Cantidad": "20", "ClaveUnidad": "TNE", "PesoEnKg": "20000.000",
        "MaterialPeligroso": "No", "ValorMercancia": "180000.00", "Moneda": "MXN"
      }
    ],
    "autotransporte": {
      "PermSCT": "TPAF01", "NumPermisoSCT": "A2C3D4E5F6",
      "identificacion_vehicular": { "ConfigVehicular": "C2R2", "PesoBrutoVehicular": "12.5", "PlacaVM": "AB123CD", "AnioModeloVM": "2021" },
      "seguros": { "AseguraRespCivil": "Quálitas Compañía de Seguros", "PolizaRespCivil": "POL-99887766" },
      "remolques": [ { "SubTipoRem": "CTR004", "Placa": "XY9876Z" } ]
    },
    "figuras": [
      { "TipoFigura": "01", "NombreFigura": "JUAN PEREZ RAMIREZ", "RFCFigura": "MASO451221PM4", "NumLicencia": "B4523187" }
    ]
  }
}
```

Para facturar el flete (`TipoDeComprobante: "I"`) el body es el mismo, más `FormaPago`, `MetodoPago`, `Moneda`, el `receptor` de tu cliente y los `conceptos` del servicio de transporte, exactamente como en `timbrar_arreglo`.

#### Campos

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `rfc_emisor` | string | Sí | RFC del emisor con CSD activo. |
| `comprobante` | object | Sí | `TipoDeComprobante` (`T` por defecto), Serie, Folio y `LugarExpedicion`. |
| `receptor` | object | Condicional | En un **traslado** se deriva del emisor; solo manda `DomicilioFiscalReceptor` si tu domicilio fiscal no coincide con el lugar de expedición. En un **ingreso** es tu cliente. |
| `conceptos` | array | Solo en `I` | El servicio de transporte que cobras. En un traslado se generan solos, uno por mercancía y sin valor. |
| `carta_porte` | object | Sí | El traslado en sí (tablas siguientes). |
| `relacionados` | object \| array | No | Para sustituir un traslado cancelado. |

**Dentro de `carta_porte`:**

| Campo | Obligatorio | Descripción |
|---|---|---|
| `TranspInternac` | Sí | `"No"` o `"Sí"`. Con `"Sí"` se vuelven obligatorios `EntradaSalidaMerc` (`Entrada`/`Salida`), `PaisOrigenDestino`, `ViaEntradaSalida` y `regimenes_aduaneros`. |
| `ubicaciones` | Sí | Al menos un `Origen` y un `Destino`, cada uno con `RFCRemitenteDestinatario`, `FechaHoraSalidaLlegada` y `domicilio` (mínimo `Estado`, `Pais`, `CodigoPostal`). Cada destino lleva su `DistanciaRecorrida` en kilómetros. |
| `mercancias` | Sí | Cada una con `BienesTransp` (clave `c_ClaveProdServCP`), `Descripcion`, `Cantidad`, `ClaveUnidad` y `PesoEnKg`. Admite `documentacion_aduanera`, `guias_identificacion`, `cantidad_transporta` y `detalle_mercancia`. |
| `autotransporte` | Sí | `PermSCT`, `NumPermisoSCT`, `identificacion_vehicular` (configuración, peso bruto, placa y año), `seguros` (responsabilidad civil obligatoria) y `remolques` opcionales. |
| `figuras` | Sí | Quienes intervienen en el traslado. Debe ir al menos el operador (`TipoFigura: "01"`) con su `NumLicencia`. |
| `UnidadPeso` | No | Unidad del peso total; se asume `KGM`. |

#### Lo que **no** tienes que enviar

Se calcula solo, y mandarlo mal es un `422`:

- **`IdCCP`**, el folio de 36 caracteres que identifica la carta porte ante el SAT. Se genera aquí y se te devuelve en `data.id_ccp`. Solo mándalo si estás re-timbrando un traslado con su folio original.
- **`TotalDistRec`**: la suma de las distancias de los destinos.
- **`PesoBrutoTotal`** y **`NumTotalMercancias`**: salen de las mercancías.
- **`IDUbicacion`**: se numeran solas (`OR000001`, `DE000001`…).
- En un traslado: `TipoDeComprobante` `T`, `Moneda` `XXX`, los totales en cero, los conceptos y el receptor.

Si envías `TotalDistRec`, `PesoBrutoTotal` o `NumTotalMercancias`, se contrastan con lo calculado y el descuadre se rechaza antes de gastar el timbre.

#### Lo que se revisa antes de gastar el timbre

Origen y destino presentes; distancias y pesos mayores que cero; fechas coherentes (la salida no puede ser posterior a la llegada); domicilios con estado, país y código postal; el operador con licencia; los catálogos chicos (`c_TipoPermiso`, `c_ConfigAutotransporte`, `c_SubTipoRem`, `c_ClaveUnidadPeso`, `c_TipoEmbalaje`, `c_RegimenAduanero`, `c_FiguraTransporte`); el material peligroso con su clave y embalaje; y el traslado internacional con sus datos aduaneros.

**Lo que sí valida el PAC y no nosotros** —porque son catálogos de decenas de miles de entradas—: la clave `BienesTransp`, la clave de material peligroso y las colonias y municipios. Dos reglas de esos catálogos sorprenden seguido:

- Hay claves de `BienesTransp` marcadas como "material peligroso condicional": en ellas hay que declarar `MaterialPeligroso` explícitamente (`"Sí"` o `"No"`).
- No toda configuración vehicular admite remolque: si mandas `remolques`, la `ConfigVehicular` debe ser una que los acepte (por ejemplo `C2R2`, no `C2`).

**Respuesta 200:**

```json
{
  "data": {
    "uuid": "A741FCDD-FC43-54AB-8673-AEA651D0F174",
    "id_ccp": "CCC7e8d7-169b-483d-8383-e139638e1b97",
    "rfc_emisor": "EKU9003173C9",
    "tipo_comprobante": "I",
    "total": "33060.00",
    "peso_bruto_total": "20350.500",
    "num_total_mercancias": "2",
    "total_distancia_km": "842.50",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "CFDI con carta porte timbrado exitosamente."
}
```

El PDF incluye el bloque de carta porte que exige el SAT en la representación impresa: IdCCP, ubicaciones con domicilios y distancias, mercancías, datos del vehículo y del seguro, remolques y figuras del transporte.

**Errores:**

- `400` — faltan `rfc_emisor` o `carta_porte`; el emisor no existe o su CSD está vencido; el PAC rechazó el timbrado.
- `422` — la carta porte no cuadra o un catálogo no coincide; `data` señala la posición exacta (`carta_porte.ubicaciones[1].DistanciaRecorrida: …`).
- `402` — cuota de timbres agotada (producción).

### GET facturas/consultar

Consulta el estatus de una factura timbrada. Si no está cancelada localmente, verifica además el estatus real ante el SAT y actualiza el registro si cambió.

```
GET facturas/consultar?uuid=E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB
```

**Respuesta 200:**

```json
{
  "data": {
    "factura": {
      "emisor_rfc": "EKU9003173C9",
      "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
      "rfc_receptor": "MASO451221PM4",
      "total": "1160.000000",
      "tipo_comprobante": "I",
      "estatus_sat": "vigente"
    },
    "sat_check": { "...": "estatus devuelto por el SAT, o null si no se pudo consultar" }
  },
  "message": "Consulta exitosa."
}
```

### POST facturas/pdf

Genera la representación impresa de un CFDI timbrado.

**Body:** `{ "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB" }`

**Respuesta 200 — importante:** es el **binario del PDF** (`Content-Type: application/pdf`, `Content-Disposition: inline`), no JSON. Guárdalo como archivo (`response.blob()` en JS, `--output` en curl). Los errores (`400`, `404`, `500`) sí usan el sobre JSON.

### POST facturas/cancelar

Cancela un CFDI ante el SAT a través del PAC.

**Body:**

```json
{
  "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
  "motivo": "02",
  "uuid_reemplaza": ""
}
```

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `uuid` | string | Sí | UUID del CFDI a cancelar. |
| `motivo` | string | Sí | Motivo SAT (tabla siguiente). |
| `uuid_reemplaza` | string | Solo si `motivo = "01"` | UUID del CFDI que sustituye al cancelado. |

**Motivos de cancelación:**

| Clave | Significado |
|---|---|
| `01` | Emitido con errores **con** relación (requiere `uuid_reemplaza`). |
| `02` | Emitido con errores sin relación. |
| `03` | No se llevó a cabo la operación. |
| `04` | Operación nominativa relacionada con una factura global. |

**Respuestas:**

- `200` — cancelado (o ya estaba cancelado ante el SAT):

```json
{ "data": { "uuid": "...", "estatus": "cancelado", "estatus_documento": "201", "motivo": "02" } }
```

- `202` — el receptor debe aceptar o rechazar la cancelación (según reglas del SAT).
- `400` — motivo inválido, falta `uuid_reemplaza`, o el PAC la rechazó (detalle en `data`).
- `409` — ya estaba marcada como cancelada.

---

### Flujo recomendado de integración

1. **Crea tu key de sandbox** (`idoo_sk_test_...`) en tu [panel de idoo.dev](/panel/apikeys). Sandbox es gratis e ilimitado.
2. **Descarga el CSD de pruebas del SAT** (RFC `EKU9003173C9`) desde el **[Drive de idoo](https://drive.google.com/file/d/1QES7XOqySU-aainYwtnCCtNUJ0Z1gSUf/view?usp=sharing)**. Este paso no es opcional: sin estos archivos no puedes timbrar nada en sandbox.
3. **Regístralo** con `emisores/subir_csd` usando tu key de sandbox, y confirma con `emisores/listado` que aparece como `activo`. Los pasos exactos están en [Antes de empezar](#antes-de-empezar-registra-el-csd-de-pruebas).
4. Timbra con `facturas/timbrar_arreglo` (recomendado) o `facturas/timbrar` si generas tu propio XML sellado.
5. Si facturas a crédito (`PPD`), emite un `facturas/timbrar_complemento_arreglo` por cada cobro que recibas. Guarda el saldo de cada factura en tu sistema: la API no lo lleva.
6. Guarda el `uuid`: con él consultas, descargas el PDF, cancelas y **relacionas correcciones**. Para corregir a la baja (devoluciones, descuentos, bonificaciones) emite una nota de crédito con `TipoDeComprobante: "E"` y `relacionados`; para errores que no se corrigen con eso, cancela con motivo `01` y timbra la sustituta con `TipoRelacion: "04"`.
7. Maneja `422`/`400` mostrando los mensajes de `data` a tu usuario — ya vienen en lenguaje natural.
8. Cuando estés listo, crea tu key de producción (`idoo_sk_live_...`), **sube el CSD real de tu empresa** con esa key (el de pruebas no sirve en producción) y elige el plan según tu volumen de timbres.

#### ¿PUE o PPD? La decisión que determina si necesitas complementos

| Situación | `MetodoPago` | `FormaPago` | ¿Necesitas REP? |
|---|---|---|---|
| Te pagan al momento de facturar | `PUE` | La forma real (`01`, `03`, `04`...) | No |
| Facturas ahora y te pagan después | `PPD` | **`99`** (Por definir) | Sí, uno por cada cobro |
| Te pagan en parcialidades | `PPD` | **`99`** | Sí, uno por cada parcialidad |

En una factura `PPD` la forma de pago **tiene que ser `99`**: todavía no sabes cómo te van a pagar. La forma real se declara después, en el `FormaDePagoP` de cada complemento.

---

### Ejercicios guiados

Todos corren en sandbox y no consumen cuota. Sustituye `{tu_api_key}` por tu key `idoo_sk_test_...`.

> ⚠️ **Requisito para todos los ejercicios:** haber registrado el emisor de pruebas `EKU9003173C9`. Si todavía no lo haces, empieza por el **Ejercicio 0**. Si te saltas este paso, cada ejercicio responderá `404` (emisor no registrado).

#### Ejercicio 0 — Registra el emisor de pruebas (una sola vez)

**Paso 1.** Descarga el CSD de pruebas desde el **[Drive de idoo](https://drive.google.com/file/d/1QES7XOqySU-aainYwtnCCtNUJ0Z1gSUf/view?usp=sharing)** y descomprímelo. Ubica el `.cer` y el `.key`.

**Paso 2.** Conviértelos a Base64 y súbelos (en macOS cambia `base64 -w 0` por `base64 -i`; en Windows usa los comandos de PowerShell de [Antes de empezar](#antes-de-empezar-registra-el-csd-de-pruebas)):

```bash
CER=$(base64 -w 0 EKU9003173C9.cer)
KEY=$(base64 -w 0 EKU9003173C9.key)

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/emisores/subir_csd" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d "{
    \"archivo_cer_base64\": \"$CER\",
    \"archivo_key_base64\": \"$KEY\",
    \"contrasena_csd\": \"12345678a\",
    \"rfc\": \"EKU9003173C9\",
    \"razon_social\": \"ESCUELA KEMPER URGATE\",
    \"regimen_fiscal\": \"601\"
  }"
```

**Paso 3.** Comprueba que quedó activo:

```bash
curl "https://api.idoo.dev/v1/timbrado-cfdi-4-0/emisores/listado" \
  -H "Authorization: Bearer {tu_api_key}"
```

Cuando veas `EKU9003173C9` con `"estatus": "activo"`, sigue con el Ejercicio 1. No necesitas repetir este paso: el emisor queda guardado en tu cuenta de sandbox.

#### Ejercicio 1 — Tu primer CFDI de contado

Factura de $1,000 más IVA, pagada en efectivo al momento.

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "Serie": "A", "Folio": "1001",
      "FormaPago": "01", "MetodoPago": "PUE",
      "Moneda": "MXN", "TipoDeComprobante": "I",
      "LugarExpedicion": "80290"
    },
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605",
      "UsoCFDI": "S01"
    },
    "conceptos": [{
      "ClaveProdServ": "10101502", "ClaveUnidad": "KGM", "Cantidad": "1",
      "Descripcion": "MAIZ", "ValorUnitario": "1000.00", "Importe": "1000.00",
      "ObjetoImp": "02",
      "traslados": [
        { "Base": "1000.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "160.00" }
      ]
    }]
  }'
```

Guarda el `uuid` de la respuesta y descarga el PDF:

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/pdf" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"uuid":"EL-UUID-QUE-TE-DEVOLVIO"}' \
  --output factura.pdf
```

Al ser `PUE`, aquí terminas. No hay complemento que emitir.

#### Ejercicio 2 — Venta a crédito y su complemento de pago

**Paso 1.** Emite la factura a crédito. Fíjate en `MetodoPago: "PPD"` y `FormaPago: "99"`:

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "Serie": "FAC", "Folio": "20458",
      "FormaPago": "99", "MetodoPago": "PPD",
      "Moneda": "MXN", "TipoDeComprobante": "I",
      "LugarExpedicion": "80290"
    },
    "CondicionesDePago": "Crédito a 30 días",
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605",
      "UsoCFDI": "G03"
    },
    "conceptos": [{
      "ClaveProdServ": "43211508", "ClaveUnidad": "H87", "Cantidad": "1",
      "Descripcion": "Servidor rack 2U", "ValorUnitario": "10000.00", "Importe": "10000.00",
      "ObjetoImp": "02",
      "traslados": [
        { "Base": "10000.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "1600.00" }
      ]
    }]
  }'
```

Total de la factura: **$11,600.00**. Anota su `uuid` — lo vas a necesitar.

**Paso 2.** Un mes después el cliente te transfiere los $11,600. Emite el complemento:

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_complemento_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": { "Serie": "PAG", "Folio": "1", "LugarExpedicion": "80290" },
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605"
    },
    "pagos": [{
      "FechaPago": "2026-09-01T10:00:00",
      "FormaDePagoP": "03",
      "MonedaP": "MXN",
      "NumOperacion": "REF-88451203",
      "documentos": [{
        "IdDocumento": "UUID-DE-LA-FACTURA-DEL-PASO-1",
        "Serie": "FAC", "Folio": "20458",
        "NumParcialidad": "1",
        "ImpSaldoAnt": "11600.00",
        "ImpPagado": "11600.00",
        "traslados": [
          { "BaseDR": "10000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "1600.00" }
        ]
      }]
    }]
  }'
```

No enviaste `ImpSaldoInsoluto` (se calcula: `11600 − 11600 = 0.00`), ni `TipoCambioP`, ni `MonedaDR`, ni `EquivalenciaDR`, ni `ObjetoImpDR`, ni el `Monto` del pago. Todo eso se deriva.

#### Ejercicio 3 — Cobro en tres parcialidades

La misma factura de $11,600, pagada en tres exhibiciones. **Un complemento por cada cobro**, y en cada uno el saldo avanza:

| Parcialidad | `NumParcialidad` | `ImpSaldoAnt` | `ImpPagado` | `ImpSaldoInsoluto` | `BaseDR` | `ImporteDR` |
|---|---|---|---|---|---|---|
| 1ª | `1` | `11600.00` | `5800.00` | `5800.00` | `5000.00` | `800.00` |
| 2ª | `2` | `5800.00` | `3480.00` | `2320.00` | `3000.00` | `480.00` |
| 3ª | `3` | `2320.00` | `2320.00` | `0.00` | `2000.00` | `320.00` |

Dos reglas que se rompen seguido:

- **El `ImpSaldoAnt` de cada parcialidad es el `ImpSaldoInsoluto` de la anterior.** Guárdalo en tu base de datos al timbrar cada complemento.
- **La base del impuesto es proporcional a lo cobrado, no el total de la factura.** Si cobras la mitad, la base es la mitad. Un `ImporteDR` que no sea `BaseDR × TasaOCuotaDR` se rechaza con `422` antes de gastar el timbre.

Cuerpo de la segunda parcialidad:

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "PAG", "Folio": "2", "LugarExpedicion": "80290" },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "pagos": [{
    "FechaPago": "2026-10-01T10:00:00",
    "FormaDePagoP": "03",
    "MonedaP": "MXN",
    "NumOperacion": "REF-88451999",
    "documentos": [{
      "IdDocumento": "UUID-DE-LA-FACTURA",
      "Serie": "FAC", "Folio": "20458",
      "NumParcialidad": "2",
      "ImpSaldoAnt": "5800.00",
      "ImpPagado": "3480.00",
      "traslados": [
        { "BaseDR": "3000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "480.00" }
      ]
    }]
  }]
}
```

#### Ejercicio 4 — Un pago que liquida dos facturas, con retenciones

Tu cliente te hace **una sola transferencia** que cubre dos facturas: una parcialidad de mercancía y unos honorarios con retención de ISR e IVA. Va **un solo complemento** con dos documentos dentro del mismo pago.

Los honorarios: base $10,000, IVA $1,600, retención de ISR 10% ($1,000) y de IVA 10.6667% ($1,066.67) → te depositan $9,533.33. Más los $5,800 de la otra factura, la transferencia es de **$15,333.33**.

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "PAG", "Folio": "3", "LugarExpedicion": "80290" },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "pagos": [{
    "FechaPago": "2026-08-01T10:00:00",
    "FormaDePagoP": "03",
    "MonedaP": "MXN",
    "NumOperacion": "REF-88451203",
    "RfcEmisorCtaOrd": "BBA830831LJ2",
    "CtaOrdenante": "0123456789",
    "RfcEmisorCtaBen": "BMN930209927",
    "CtaBeneficiario": "9876543210",
    "documentos": [
      {
        "IdDocumento": "C1855BEE-324E-55EB-9154-91D890F31469",
        "Serie": "FAC", "Folio": "20458",
        "NumParcialidad": "2",
        "ImpSaldoAnt": "11600.00",
        "ImpPagado": "5800.00",
        "traslados": [
          { "BaseDR": "5000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "800.00" }
        ]
      },
      {
        "IdDocumento": "8179B395-B827-5F92-92FF-BC38DD268C64",
        "Serie": "HON", "Folio": "118",
        "NumParcialidad": "1",
        "ImpSaldoAnt": "9533.33",
        "ImpPagado": "9533.33",
        "traslados": [
          { "BaseDR": "10000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "1600.00" }
        ],
        "retenciones": [
          { "BaseDR": "10000.00", "ImpuestoDR": "001", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.100000", "ImporteDR": "1000.00" },
          { "BaseDR": "10000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.106667", "ImporteDR": "1066.67" }
        ]
      }
    ]
  }]
}
```

Un solo timbre cubre los dos documentos. `monto_total_pagos` te regresa `15333.33`, y el PDF lista ambas facturas con sus impuestos y el desglose de retenciones.

#### Ejercicio 5 — Emitiste un complemento con datos equivocados

Un CFDI timbrado no se edita: se cancela y se sustituye.

**Paso 1.** Cancela el complemento incorrecto con motivo `02` (emitido con errores sin relación):

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/cancelar" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"uuid":"UUID-DEL-COMPLEMENTO-MALO","motivo":"02"}'
```

**Paso 2.** Emite el complemento correcto relacionándolo con el cancelado:

```json
{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "PAG", "Folio": "4", "LugarExpedicion": "80290" },
  "relacionados": { "TipoRelacion": "04", "uuids": ["UUID-DEL-COMPLEMENTO-MALO"] },
  "receptor": { "...": "..." },
  "pagos": [ "..." ]
}
```

La cancelación del complemento **no cancela la factura original**: son comprobantes distintos. Si además hay que corregir la factura, cancélala aparte con motivo `01` y su `uuid_reemplaza`.

#### Ejercicio 6 — Te devolvieron parte de la mercancía (nota de crédito)

Facturaste 10 costales de maíz a 250.00 (`FAC`, total 2,900.00 con IVA) y el cliente devuelve 2. **No se cancela la factura**: se emite una **nota de crédito** por lo devuelto, relacionada con ella. Es el mismo endpoint de siempre, `facturas/timbrar_arreglo`, con dos diferencias: `TipoDeComprobante: "E"` y el bloque `relacionados`.

**Paso 1.** Emite la factura original —el Ejercicio 1 sirve tal cual— y anota su `uuid`.

**Paso 2.** Timbra la nota de crédito por los 2 costales devueltos (500.00 + IVA):

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "TipoDeComprobante": "E",
      "Serie": "NC", "Folio": "18",
      "FormaPago": "03", "MetodoPago": "PUE",
      "Moneda": "MXN", "LugarExpedicion": "80290"
    },
    "relacionados": { "TipoRelacion": "03", "uuids": ["UUID-DE-LA-FACTURA-DEL-PASO-1"] },
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605",
      "UsoCFDI": "S01"
    },
    "conceptos": [{
      "ClaveProdServ": "10101502", "ClaveUnidad": "KGM", "Cantidad": "2",
      "Descripcion": "Devolución de 2 costales de maíz de 50 kg",
      "ValorUnitario": "250.00", "Importe": "500.00", "ObjetoImp": "02",
      "traslados": [
        { "Base": "500.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "80.00" }
      ]
    }]
  }'
```

Respuesta:

```json
{
  "valid": true,
  "status": 200,
  "message": "Factura timbrada exitosamente.",
  "data": {
    "uuid": "62C13FEA-C0FB-5DF1-9CE4-BEA6F41857B2",
    "rfc_emisor": "EKU9003173C9",
    "total": "580.00",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  }
}
```

El XML sale con el nodo relacionado y el PDF rotulado **"Nota de crédito"**:

```xml
<cfdi:CfdiRelacionados TipoRelacion="03">
  <cfdi:CfdiRelacionado UUID="UUID-DE-LA-FACTURA-DEL-PASO-1"/>
</cfdi:CfdiRelacionados>
```

**Paso 3.** El `pdf_base64` ya viene en la respuesta; si lo quieres después, con el `uuid`:

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/pdf" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"uuid":"UUID-DE-LA-NOTA-DE-CREDITO"}' \
  --output nota_credito.pdf
```

Cuatro cosas que suelen costar un timbre:

- **Los importes van en positivo.** El `TipoDeComprobante: "E"` ya indica que resta; no mandes cantidades negativas.
- **`TipoRelacion`**: `03` si hubo devolución física de mercancía, `01` si es un descuento o bonificación posterior sin devolución, `07` si estás aplicando un anticipo.
- **`FormaPago` y `MetodoPago` son obligatorios** en tipo `E`, aunque no haya movimiento de dinero. Lo natural es repetir los de la factura original.
- **`UsoCFDI`**: `G02` (devoluciones, descuentos o bonificaciones) es el uso propio de una nota de crédito, pero exige un régimen empresarial en el receptor. Con un receptor de régimen `605` (sueldos y salarios) el PAC lo rechaza y va `S01`.

Si la factura corregida era **PPD** y ya le emitiste complementos de pago, la nota de crédito reduce el saldo por cobrar: el siguiente REP arranca con el `ImpSaldoAnt` ya disminuido. El servicio no lleva ese saldo por ti.

#### Ejercicio 7 — Cancelar una factura y sustituirla

Cuando el error no se arregla con una nota de crédito (RFC equivocado, conceptos mal, importe de más), se cancela con motivo `01` y se emite la sustituta. El orden importa: **primero timbras la sustituta, luego cancelas**, porque el motivo `01` exige el UUID del comprobante que reemplaza.

**Paso 1.** Timbra la factura correcta relacionándola con la que vas a cancelar:

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "TipoDeComprobante": "I", "Serie": "A", "Folio": "1002",
      "FormaPago": "01", "MetodoPago": "PUE",
      "Moneda": "MXN", "LugarExpedicion": "80290"
    },
    "relacionados": { "TipoRelacion": "04", "uuids": ["UUID-DE-LA-FACTURA-MALA"] },
    "receptor": { "...": "los datos ya corregidos" },
    "conceptos": [ "..." ]
  }'
```

**Paso 2.** Cancela la anterior apuntando a la nueva:

```bash
curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/cancelar" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "uuid": "UUID-DE-LA-FACTURA-MALA",
    "motivo": "01",
    "uuid_reemplaza": "UUID-DE-LA-FACTURA-DEL-PASO-1"
  }'
```

Si ya cancelaste antes de timbrar la sustituta, no pasa nada: relacionar un CFDI cancelado se permite justamente con `TipoRelacion: "04"`. Con cualquier otro tipo de relación un folio cancelado se rechaza con `422`.

---

### Errores frecuentes al empezar en sandbox

| Síntoma | Causa | Solución |
|---|---|---|
| `404` "el emisor no está registrado en tu cuenta" al timbrar | No subiste el CSD de pruebas | Descárgalo del [Drive](https://drive.google.com/file/d/1QES7XOqySU-aainYwtnCCtNUJ0Z1gSUf/view?usp=sharing) y haz el [Ejercicio 0](#ejercicio-0--registra-el-emisor-de-pruebas-una-sola-vez) |
| `emisores/listado` regresa `total: 0` | Subiste el CSD con otra key o en otro ambiente | Usa la **misma** key `idoo_sk_test_...` para subir el CSD y para timbrar |
| `400` "el RFC no coincide con el certificado" | Mandaste un `rfc` distinto al del `.cer` | Con el CSD de pruebas el RFC es `EKU9003173C9` |
| `400` ".cer/.key/contraseña no embonan" | Contraseña equivocada, o mezclaste un `.cer` y un `.key` de juegos distintos | Usa el `.cer` y el `.key` del mismo archivo descargado y la contraseña `12345678a` |
| `400` Base64 inválido | El Base64 trae saltos de línea o subiste la ruta del archivo en vez de su contenido | Genera el Base64 en una sola línea (`base64 -w 0` en Linux, `base64 -i` en macOS) |
| El CSD de pruebas se rechaza en producción | Los certificados de prueba solo existen en sandbox | En producción sube el CSD real de tu empresa con tu key `idoo_sk_live_...` |
| Tu CSD real se rechaza en sandbox | El PAC de pruebas solo reconoce certificados de prueba | En sandbox usa el CSD de pruebas del Drive |

### Errores frecuentes al emitir notas de crédito

| Síntoma | Causa | Solución |
|---|---|---|
| `422` "Un CFDI de egreso debe indicar qué comprobante corrige" | Mandaste `TipoDeComprobante: "E"` sin `relacionados` | Agrega el bloque con el UUID de la factura y su `TipoRelacion` |
| `422` "El 'TipoRelacion' no es válido" | Clave fuera del catálogo `c_TipoRelacion` | Usa `01`, `03` o `07` según el caso |
| `422` "no tiene el formato de un UUID del SAT" | Pusiste la serie-folio en vez del folio fiscal | Va el `uuid` que devolvió el timbrado, no `FAC-20458` |
| `422` "Los campos 'TipoRelacion', 'uuids' van dentro de 'relacionados'" | Mandaste los campos sueltos en la raíz | Envuélvelos: `"relacionados": { "TipoRelacion": "...", "uuids": [...] }` |
| `422` "está cancelado. Solo el TipoRelacion '04'…" | Relacionaste con `01` una factura ya cancelada | Una factura cancelada no se corrige con nota de crédito: se sustituye con `04` |
| `422` "lo emitió el RFC …, no …" | El folio relacionado es de otro emisor de tu cuenta | Timbra la nota con el mismo `rfc_emisor` de la factura original |
| `422` "el Importe declarado no corresponde a Cantidad × ValorUnitario" | Redondeo propio en el concepto | `Importe = Cantidad × ValorUnitario`, con 0.01 de tolerancia en pesos |
| El PAC rechaza el `UsoCFDI` | Usaste `G02` con un receptor de régimen `605` | `G02` exige régimen empresarial; con `605` va `S01` |
| `400` "Fecha y hora de generación fuera de rango" | Mandaste `Fecha` con la hora de tu servidor y expides al oeste del centro | Omítela (la ponemos con el huso de tu `LugarExpedicion`) o mándala en hora local de ese CP |

### Errores frecuentes al emitir complementos

| Síntoma | Causa | Solución |
|---|---|---|
| `422` "ImpSaldoInsoluto no cuadra" | Arrastraste mal el saldo entre parcialidades | `ImpSaldoAnt` de esta parcialidad = `ImpSaldoInsoluto` de la anterior |
| `422` "ImporteDR no cuadra: BaseDR × TasaOCuotaDR" | Usaste la base del total de la factura en un pago parcial | La base es proporcional a lo cobrado |
| `422` "no admite la clave 99" | Copiaste el `FormaPago` de la factura PPD | En el complemento va la forma real del cobro |
| `422` "FechaPago no puede ser posterior" | Fecha del pago mayor que la del comprobante | Revisa la zona horaria; `FechaPago` es cuando entró el dinero |
| `422` "Un CFDI de pago no admite estos campos" | Mandaste `FormaPago` o `MetodoPago` en `comprobante` | Quítalos; van dentro de `pagos` |
| `422` "no tiene forma de folio fiscal (UUID)" | Pusiste la serie-folio en `IdDocumento` | Va el `uuid` que te devolvió `timbrar_arreglo`, no `FAC-20458` |
| `422` "ImpPagado no puede exceder el saldo anterior" | Aplicaste a una factura más de lo que debía | Reparte el pago entre varios `documentos` |
| Emites REP de una factura `PUE` | Confusión de método de pago | Las `PUE` no llevan complemento |

### Necesitas ayuda
Ingresa a https://idoo.mx/ayuda o escribenos a ayuda@idoo.mx
