> ## 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 de nómina

> Cómo emitir un CFDI tipo N con complemento Nómina 1.2: qué armamos por ti, qué envías, cómo deben cuadrar los totales y qué reglas del SAT validamos antes de timbrar.

`POST /cfdi/timbrar-nomina` genera, sella y timbra un **CFDI 4.0 tipo N** (recibo de nómina)
con el complemento **Nómina 1.2** del SAT. Emites un recibo por empleado y por periodo de pago.

Es un endpoint distinto de `POST /cfdi/timbrar` porque el cuerpo es distinto: no envías
`conceptos`, `subtotal` ni `total`. Envías las **percepciones, deducciones y otros pagos** del
periodo y los **datos del empleado**, y Ipsofactura arma el comprobante completo con la
estructura que exige la guía de llenado del SAT.

<Note>
  La referencia campo por campo está en
  [`POST /cfdi/timbrar-nomina`](/api-reference/cfdi/timbrar-cfdi-de-nómina), generada
  directamente del código de la API. Esta guía explica las reglas que la referencia no puede
  expresar.
</Note>

## Qué arma Ipsofactura por ti

Un CFDI de nómina tiene casi todos los atributos del comprobante fijos por norma. No los
envías y no son configurables:

| Atributo del CFDI                | Valor                                                        | Por qué                                                                                         |
| -------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `TipoDeComprobante`              | `N`                                                          | Es el tipo del recibo de nómina.                                                                |
| `Moneda`                         | `MXN`                                                        | La nómina se paga en pesos; no hay `TipoCambio`.                                                |
| `MetodoPago`                     | `PUE`                                                        | Lo exige la guía de llenado. `FormaPago` **no existe** en un tipo N.                            |
| `Exportacion`                    | `01`                                                         | No aplica exportación.                                                                          |
| Impuestos                        | Sin nodo `cfdi:Impuestos`                                    | El ISR retenido viaja en las deducciones del complemento, no como impuesto del comprobante.     |
| `Receptor.RegimenFiscalReceptor` | `605`                                                        | Sueldos y salarios e ingresos asimilados.                                                       |
| `Receptor.UsoCFDI`               | `CN01`                                                       | Nómina.                                                                                         |
| Concepto                         | Uno solo: `84111505` / `ACT`, cantidad `1`, "Pago de nómina" | `ValorUnitario` = percepciones + otros pagos; `Descuento` = deducciones (se omite si son cero). |

Los totales del comprobante se calculan a partir de tus totales del complemento:

```
SubTotal = total_percepciones + total_otros_pagos
Descuento = total_deducciones           (omitido cuando es 0)
Total    = SubTotal - Descuento
```

Del receptor sólo envías `rfc`, `nombre` y `domicilio_fiscal` (código postal). El régimen y el
uso de CFDI los fija el endpoint. `serie`, `folio`, `fecha` y `lugar_expedicion` siguen los
mismos [defaults que `POST /cfdi/timbrar`](/guia-timbrado#defaults-al-timbrar).

## Ejemplo: nómina ordinaria de sueldos

Una quincena de un empleado bajo régimen de sueldos (`tipo_regimen` `02`), con ISR retenido y
subsidio para el empleo:

```json Request theme={null}
{
  "rfc_emisor": "EKU9003173C9",
  "serie": "NOM",
  "idempotency_key": "nomina-EMP-042-2026-09-2",
  "receptor": {
    "rfc": "VECJ880326XXX",
    "nombre": "JUAN VECTOR",
    "domicilio_fiscal": "64000"
  },
  "nomina": {
    "tipo_nomina": "O",
    "fecha_pago": "2026-09-15",
    "fecha_inicial_pago": "2026-09-01",
    "fecha_final_pago": "2026-09-15",
    "num_dias_pagados": 15,
    "total_percepciones": 12000.00,
    "total_deducciones": 1800.00,
    "total_otros_pagos": 100.00,
    "empleado": {
      "curp": "VECJ880326HDFXXX01",
      "num_empleado": "EMP-042",
      "num_seguridad_social": "12345678901",
      "fecha_inicio_rel_laboral": "2020-03-01",
      "antiguedad": "P6Y6M14D",
      "tipo_contrato": "01",
      "tipo_jornada": "01",
      "tipo_regimen": "02",
      "periodicidad_pago": "04",
      "departamento": "Tecnología",
      "puesto": "Desarrollador",
      "riesgo_puesto": "1",
      "salario_base_cot_apor": 500.00,
      "salario_diario_integrado": 550.00,
      "banco": "002",
      "cuenta_bancaria": "12345678901",
      "clave_ent_fed": "NLE"
    },
    "empleador": {
      "registro_patronal": "A1234567890"
    },
    "percepciones": [
      {
        "tipo_percepcion": "001",
        "clave": "001",
        "concepto": "Sueldo",
        "importe_gravado": 10000.00,
        "importe_exento": 2000.00
      }
    ],
    "deducciones": [
      {
        "tipo_deduccion": "002",
        "clave": "ISR",
        "concepto": "ISR",
        "importe": 1800.00
      }
    ],
    "otros_pagos": [
      {
        "tipo_otro_pago": "002",
        "clave": "SUB",
        "concepto": "Subsidio para el empleo",
        "importe": 100.00,
        "subsidio_al_empleo": { "subsidio_causado": 100.00 }
      }
    ]
  }
}
```

```json Response 201 theme={null}
{
  "id": "9a4f07ff-3abc-435f-b7a6-88c2bc65b4c1",
  "uuid": "6128396f-c09b-4ec6-8699-43da5a244971",
  "serie": "NOM",
  "folio": "1",
  "stamped_at": "2026-09-15T10:00:00",
  "numero_certificado_sat": "00001000000500000000",
  "sello_cfdi": "...",
  "sello_sat": "...",
  "cadena_original_sat": "||1.1|...||",
  "idempotency_key": "nomina-EMP-042-2026-09-2"
}
```

La respuesta es la misma que la de cualquier timbrado. El XML timbrado, con el nodo
`nomina12:Nomina` completo, se descarga con `GET /cfdi/{id}/xml`.

## Los totales deben cuadrar con el detalle

Los tres totales son obligatorios, incluso en cero, y cada uno debe ser **exactamente** la suma
de su detalle. Si no coinciden, la respuesta es `400 NOMINA_TOTALS_MISMATCH` y no se consume
timbre:

| Total                | Debe ser igual a                                             |
| -------------------- | ------------------------------------------------------------ |
| `total_percepciones` | Σ (`importe_gravado` + `importe_exento`) de `percepciones[]` |
| `total_deducciones`  | Σ `importe` de `deducciones[]`, o `0` si no hay deducciones  |
| `total_otros_pagos`  | Σ `importe` de `otros_pagos[]`, o `0` si no hay otros pagos  |

```json 400 NOMINA_TOTALS_MISMATCH theme={null}
{
  "code": "NOMINA_TOTALS_MISMATCH",
  "message": "total_percepciones (12000.00) no coincide con la suma del detalle (11000.00)"
}
```

Te pedimos los totales en vez de calcularlos porque son los que tu sistema de nómina ya
determinó: si no cuadran, lo más probable es que el detalle que nos mandas no sea el que
pagaste, y eso conviene detectarlo antes de que exista un CFDI.

## Qué validamos antes de llamar al PAC

Todo lo que sigue responde `400 VALIDATION_ERROR` con el nombre del campo en `message`, sin
consumir timbre. Son las reglas de la guía de llenado y de la matriz de errores de nómina del
SAT que dependen sólo del payload.

### Periodo y días

* `fecha_inicial_pago` ≤ `fecha_final_pago`, y `fecha_pago` ≥ `fecha_inicial_pago`.
* `num_dias_pagados` > 0. Acepta fracción: `7.5` se timbra como `7.500` (el SAT exige entero
  o exactamente tres decimales; nosotros hacemos la conversión).

### Empleado

| Campo                                                                | Regla                                                                                                                                                                            |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `curp`                                                               | 18 caracteres con el formato oficial (4 letras, 6 dígitos, 6 letras, 2 dígitos).                                                                                                 |
| `num_seguridad_social`                                               | Si lo envías, exactamente 11 dígitos.                                                                                                                                            |
| `antiguedad`                                                         | Formato de duración del SAT: semanas completas (`P6W`) o años/meses/días **terminando siempre en días** (`P6Y0D`, `P6Y6M14D`). `P6Y` es inválido.                                |
| `riesgo_puesto`, `salario_base_cot_apor`, `salario_diario_integrado` | **Requeridos** cuando `tipo_regimen` es `02`, `03` o `04` (sueldos).                                                                                                             |
| `cuenta_bancaria`                                                    | Si tiene 18 dígitos es una CLABE: se verifica su dígito de control y **no debe** venir `banco`. Con cualquier otra longitud (10, 11, 15 o 16 dígitos), `banco` es **requerido**. |

**El bloque del registro patronal va completo o no va.** Si envías
`empleador.registro_patronal`, son obligatorios `num_seguridad_social`,
`fecha_inicio_rel_laboral`, `antiguedad` (si la omites la calculamos, ver abajo), `riesgo_puesto`
y `salario_diario_integrado`. Si no lo envías, ninguno de esos cinco debe aparecer. Es una regla literal de la guía de llenado; en la
práctica una nómina de sueldos (contrato `01` a `08`) siempre lleva registro patronal y el
bloque completo, y una de asimilados a salarios (`tipo_regimen` `09`, contrato `99`) no lleva
ninguno.

**El tipo de contrato decide el régimen y si hay registro patronal.** Con `tipo_contrato` `01` a
`08` (una relación laboral), `tipo_regimen` debe ser `02`, `03` o `04` y
`empleador.registro_patronal` es obligatorio. Con `09`, `10` o `99`, `tipo_regimen` va de `05` a
`99` y el registro patronal no debe venir.

**La periodicidad sigue al tipo de nómina, no al régimen.** Una nómina ordinaria (`O`) no admite
`periodicidad_pago` `99`; una extraordinaria (`E`) la exige.

**`antiguedad` la calculamos si la omites.** Cuando hay registro patronal y no envías
`antiguedad`, la calculamos como el lapso en años, meses y días entre `fecha_inicio_rel_laboral`
y `fecha_final_pago`: del `2020-03-01` al `2026-09-15`, `P6Y6M14D`. Si la envías, la respetamos
tal cual, y el SAT la rechaza con `NOM55` si no es ese lapso. `fecha_inicio_rel_laboral` no puede
ser posterior a `fecha_final_pago`.

### Catálogos

Cualquier clave fuera del catálogo del SAT responde `400` con la lista de valores aceptados en
`message`. No la degradamos ni la enviamos al PAC.

| Campo                                     | Catálogo del SAT                                                                                                                         |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `tipo_nomina`                             | `O` ordinaria, `E` extraordinaria                                                                                                        |
| `empleado.tipo_contrato`                  | `c_TipoContrato` (`01`–`10`, `99`)                                                                                                       |
| `empleado.tipo_regimen`                   | `c_TipoRegimen` (`02`–`13`, `99`)                                                                                                        |
| `empleado.periodicidad_pago`              | `c_PeriodicidadPago` (`01`–`10`, `99`)                                                                                                   |
| `empleado.tipo_jornada`                   | `c_TipoJornada` (`01`–`08`, `99`)                                                                                                        |
| `empleado.riesgo_puesto`                  | `c_RiesgoPuesto` (`1`–`5`, `99`)                                                                                                         |
| `empleado.clave_ent_fed`                  | `c_Estado`: la entidad donde el empleado prestó el servicio (`CMX`, `NLE`, `JAL`, …). Incluye estados de EE. UU. y provincias de Canadá. |
| `percepciones[].tipo_percepcion`          | `c_TipoPercepcion`                                                                                                                       |
| `deducciones[].tipo_deduccion`            | `c_TipoDeduccion`                                                                                                                        |
| `otros_pagos[].tipo_otro_pago`            | `c_TipoOtroPago`                                                                                                                         |
| `percepciones[].horas_extra[].tipo_horas` | `c_TipoHoras` (`01` dobles, `02` triples, `03` simples)                                                                                  |
| `incapacidades[].tipo_incapacidad`        | `c_TipoIncapacidad` (`01`–`04`)                                                                                                          |
| `empleado.banco`                          | `c_Banco`, 3 dígitos                                                                                                                     |

La `clave` de cada percepción, deducción y otro pago es tu clave interna. El SAT la exige de
**3 a 15 caracteres**: una clave interna de uno o dos caracteres (`1`, `P1`) se rechaza con
`400`. Si tu catálogo interno usa claves cortas, rellénalas (`001`, `P01`).

### Horas extra

`horas_extra` cuelga de la **percepción que las paga**, y el SAT sólo lo admite bajo
`tipo_percepcion` `019`. Enviarlo en cualquier otra percepción es `400`.

```json Percepción 019 con horas extra theme={null}
{
  "tipo_percepcion": "019",
  "clave": "019",
  "concepto": "Horas extra",
  "importe_gravado": 600.00,
  "importe_exento": 400.00,
  "horas_extra": [
    { "dias": 2, "tipo_horas": "02", "horas_extra": 4, "importe_pagado": 1000.00 }
  ]
}
```

### Otros pagos y el subsidio para el empleo

Es la regla que más nóminas rechaza, así que va primero: **en una nómina de sueldos
(`tipo_regimen` `02`) el SAT exige un otro pago con `tipo_otro_pago` `002` (subsidio para el
empleo)**, aunque el subsidio sea cero, salvo que la nómina traiga un `007` (ISR ajustado por
subsidio) o un `008` (subsidio entregado que no correspondía). Sin esa línea el SAT responde
`NOM105`. Validamos esto antes de timbrar.

| Regla                                                                                                                                                                                                                   | Respuesta si no se cumple |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `tipo_regimen` `02` requiere un otro pago `002`, o bien uno `007`/`008` (no ambos).                                                                                                                                     | `400`                     |
| Con `tipo_regimen` distinto de `02`, no puede haber `002`, `007` ni `008`.                                                                                                                                              | `400`                     |
| Un otro pago `002` requiere `subsidio_al_empleo.subsidio_causado`.                                                                                                                                                      | `400`                     |
| En el `002`, `importe` (lo entregado) ≤ `subsidio_causado` (lo que marca la tabla del Anexo 8). Es el único otro pago que admite `importe` en `0`.                                                                      | `400`                     |
| Cualquier otro tipo de otro pago lleva `importe` > 0.                                                                                                                                                                   | `400`                     |
| Un otro pago `004` requiere `compensacion_saldos_a_favor` con `saldo_a_favor` ≥ `remanente_sal_fav`, y `anio` igual al año anterior a `fecha_pago` (o al mismo año sólo cuando el periodo pagado termina en diciembre). | `400`                     |

```json Subsidio causado pero no entregado (importe en cero) theme={null}
{
  "tipo_otro_pago": "002",
  "clave": "SUB",
  "concepto": "Subsidio para el empleo",
  "importe": 0.00,
  "subsidio_al_empleo": { "subsidio_causado": 120.00 }
}
```

`subsidio_causado` **no** es lo mismo que `importe`: `importe` es lo que le entregaste al
empleado y `subsidio_causado` lo que le corresponde según la tabla del subsidio vigente. El SAT
compara ambos. Los topes de esa tabla (`NOM101`, `NOM108`) cambian cada año con la Resolución
Miscelánea, así que **no** los validamos nosotros: los aplica el SAT al timbrar.

### Finiquitos, indemnizaciones y jubilación

Un finiquito o una liquidación se timbra como cualquier otro recibo: sus percepciones van en
`percepciones[]` junto con el sueldo del último periodo. Normalmente es una nómina
**extraordinaria** (`tipo_nomina` `E`, `periodicidad_pago` `99`). Lo que cambia es que ciertas
claves de percepción exigen un bloque de datos adicional:

| Percepciones                                                                  | Bloque requerido                   |
| ----------------------------------------------------------------------------- | ---------------------------------- |
| `022` prima por antigüedad, `023` pagos por separación, `025` indemnizaciones | `nomina.separacion_indemnizacion`  |
| `039` jubilación en una exhibición, `044` jubilación en parcialidades         | `nomina.jubilacion_pension_retiro` |

Cada bloque va **una vez por recibo**, dentro de `nomina`, no dentro de cada percepción: describe
el total de esas percepciones aunque las separes en varias líneas.

Como toda nómina extraordinaria, un finiquito lleva `periodicidad_pago` `99`.

Los subtotales de percepciones los calculamos nosotros por clave: el sueldo por un lado, la
separación por otro y la jubilación por otro. Tú sólo mandas `total_percepciones`, que sigue
siendo la suma de **todas** las percepciones.

```json Finiquito: último sueldo, prima de antigüedad e indemnización theme={null}
{
  "tipo_nomina": "E",
  "total_percepciones": 30000.00,
  "percepciones": [
    { "tipo_percepcion": "001", "clave": "001", "concepto": "Sueldo",
      "importe_gravado": 7000.00, "importe_exento": 0.00 },
    { "tipo_percepcion": "022", "clave": "022", "concepto": "Prima por antigüedad",
      "importe_gravado": 1000.00, "importe_exento": 2000.00 },
    { "tipo_percepcion": "025", "clave": "025", "concepto": "Indemnización",
      "importe_gravado": 5000.00, "importe_exento": 15000.00 }
  ],
  "separacion_indemnizacion": {
    "total_pagado": 23000.00,
    "num_anios_servicio": 6,
    "ultimo_sueldo_mens_ord": 15000.00,
    "ingreso_acumulable": 6000.00,
    "ingreso_no_acumulable": 0.00
  }
}
```

En `separacion_indemnizacion` los cinco campos son obligatorios y `num_anios_servicio` va de
`0` a `99`. `ingreso_acumulable` e `ingreso_no_acumulable` los determina tu sistema de nómina
conforme a la guía de llenado del SAT; nosotros los transportamos sin recalcularlos.

Una jubilación se paga **en una exhibición o en parcialidades, nunca de las dos formas**:

| Percepción | `jubilacion_pension_retiro` lleva    | No debe llevar                      |
| ---------- | ------------------------------------ | ----------------------------------- |
| `039`      | `total_una_exhibicion`               | `total_parcialidad`, `monto_diario` |
| `044`      | `total_parcialidad` y `monto_diario` | `total_una_exhibicion`              |

En los dos casos, `ingreso_acumulable` e `ingreso_no_acumulable` son obligatorios.

```json Pensión en parcialidades (044) theme={null}
"jubilacion_pension_retiro": {
  "total_parcialidad": 9000.00,
  "monto_diario": 300.00,
  "ingreso_acumulable": 3000.00,
  "ingreso_no_acumulable": 0.00
}
```

Todas estas reglas responden `400` antes de timbrar:

| Regla                                                                      | Respuesta si no se cumple |
| -------------------------------------------------------------------------- | ------------------------- |
| Una percepción `022`, `023` o `025` requiere `separacion_indemnizacion`.   | `400`                     |
| Una percepción `039` o `044` requiere `jubilacion_pension_retiro`.         | `400`                     |
| `jubilacion_pension_retiro` sin ninguna percepción `039` ni `044`.         | `400`                     |
| `039` y `044` en el mismo recibo.                                          | `400`                     |
| Con `039` o `044`, los montos que no corresponden según la tabla anterior. | `400`                     |

Para un jubilado usa `tipo_regimen` `03`: no lleva subsidio para el empleo, que es sólo para
`02` (ver la sección anterior).

## Lo que valida el SAT y nosotros no

Estas reglas dependen de datos que no están en tu payload (el padrón del SAT, la relación
laboral, las tablas fiscales). Llegan como `400 VALIDATION_ERROR` **después** de llamar al PAC,
sin consumir timbre, con el código del SAT (`NOMxx`) en `sat_code`:

```json Rechazo del SAT en nómina theme={null}
{
  "code": "VALIDATION_ERROR",
  "message": "NOM55 - El campo Antigüedad no corresponde al periodo entre FechaInicioRelLaboral y FechaFinalPago.",
  "sat_code": "NOM55"
}
```

Las que más aparecen al integrar:

| Código                    | Regla                                                                                                                  | Cómo evitarla                                                                                                    |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `NOM55`                   | `antiguedad` debe ser el lapso **exacto** en años, meses y días entre `fecha_inicio_rel_laboral` y `fecha_final_pago`. | Omítela y la calculamos nosotros. Si la envías, calcúlala en cada recibo, no la copies de la ficha del empleado. |
| `CFDI40143` a `CFDI40147` | `nombre` y `domicilio_fiscal` del empleado deben coincidir **exactamente** con su constancia de situación fiscal.      | Toma ambos de la constancia, no del expediente de RH. Es la misma regla que en cualquier CFDI 4.0.               |

## Cobertura actual

Timbra hoy, verificado contra el ambiente de pruebas del PAC:

* Nómina **ordinaria** y **extraordinaria** (`tipo_nomina` `O` / `E`).
* Régimen de **sueldos** (`02`) con subsidio para el empleo, y **asimilados a salarios** (`09`).
* Percepciones con **horas extra** (`019`), **incapacidades**, otros pagos con
  **subsidio al empleo** (`002`) y **compensación de saldos a favor** (`004`).
* Nómina **sin deducciones**, y días pagados **fraccionarios**.
* **Finiquitos**: separación e indemnización (`022`, `023`, `025`), y **jubilación, pensión o
  retiro** en una exhibición (`039`) o en parcialidades (`044`). Ver la sección
  *Finiquitos, indemnizaciones y jubilación*.

Todavía **no** soportamos:

* Percepción `045` (**acciones o títulos**) y **subcontratación** (`SubContratacion` en el
  receptor).
* `GET /cfdi/{id}` devuelve el comprobante (un concepto "Pago de nómina") pero **no** el detalle
  del complemento. Para leer percepciones y deducciones de un recibo ya timbrado descarga el
  XML con `GET /cfdi/{id}/xml`.

`GET /cfdi/{id}/pdf` de un tipo N devuelve un **recibo de nómina**: patrón y empleado, periodo y
días pagados, percepciones, deducciones, otros pagos e incapacidades, el neto a pagar y el timbre
con su QR. El PDF se genera una vez y se conserva, así que un recibo cuyo PDF pediste antes de
que existiera esta plantilla sigue entregando el documento original.

## Idempotencia, reintentos y cancelación

Se comportan igual que en `POST /cfdi/timbrar`:

* Reenviar el mismo `idempotency_key` con el mismo payload devuelve el primer resultado;
  la misma `serie` + `folio` funciona igual. Ver
  [Recuperación tras timeout](/recuperacion-timeouts).
* Una clave estable natural es `nomina-<num_empleado>-<periodo>`.
* La nómina se timbra únicamente por el PAC que acepta el complemento Nómina 1.2, así que no
  tiene el respaldo de un segundo PAC: si ese proveedor no está disponible recibes `503` y
  debes reintentar con la misma clave.
* Un recibo se cancela con `POST /cfdi/cancelar` como cualquier CFDI. Ver
  [Cancelación](/cancelacion).
* El webhook `cfdi.timbrado` se dispara igual que para cualquier comprobante. Ver
  [Webhooks](/webhooks).

## En el sandbox

El [sandbox](/sandbox) responde `201` con un UUID simulado y aplica **todas** las validaciones
de esta guía que dependen del payload: totales, catálogos, periodo, CURP, bloque del registro
patronal y reglas de otros pagos. Úsalo para probar el contrato de la API y tu manejo de errores.

Lo que no puedes verificar ahí:

* Las reglas del SAT de la sección anterior (`NOM55`, `NOM34`, `NOM42`, identidad del receptor
  contra el padrón): el sandbox no las aplica.

El XML que devuelve el sandbox para un tipo N sí incluye el complemento `nomina12:Nomina`
completo, con la misma estructura que el de producción, así que puedes validar tu parser y la
estructura del complemento contra el XSD del SAT desde el sandbox. El sello y el timbre son
simulados y no tienen validez fiscal.

<Warning>
  Un CFDI de nómina timbrado en producción es un hecho fiscal real: entra al prellenado de la
  declaración anual del empleado y al expediente del patrón. Para tu primera nómina real, timbra
  **un solo recibo** y valídalo antes de correr la nómina completa.
</Warning>
