Emitir CFDI

Nómina

Dar de alta empleados y timbrar recibos de nómina con el complemento 1.2.

Un recibo de nómina es un CFDI de tipo nómina: un comprobante con el complemento de Nómina 1.2 del SAT. Se emite por empleado y por periodo de pago.

Scopes: invoices.create para dar de alta empleados y timbrar; invoices.read para consultarlos.

Paso 1 — da de alta al empleado

POST /api/authenticated/{org}/empleados
CampoRequeridoReglas
rfc13 caracteres (persona física).
curp18 caracteres.
nombreSolo el nombre de pila.
primerApellido
segundoApellidono
cp5 dígitos.
tipoContratoClave de c_TipoContrato.
tipoRegimenClave de c_TipoRegimen (02 sueldos y salarios es lo común).
salarioDiarioIntegradoSDI.
periodicidadPagorecomendadoClave de c_PeriodicidadPago.
entidadFederativarecomendadoClave de c_Estado.
riesgoPuestorecomendadoClave de c_RiesgoPuesto.
salarioBaseCotizacionrecomendadoSBC.
nssno11 dígitos.
clabeno18 dígitos.
banco, numEmpleado, departamento, puesto, numContrato, tipoJornadano
curl -s -X POST "$FACTY_HOST/api/authenticated/$FACTY_ORG/empleados" \
  -H "X-API-Key: $FACTY_KEY" -H "Content-Type: application/json" \
  -d '{
    "rfc": "PEGJ850101HDF",
    "curp": "PEGJ850101HDFRRN04",
    "nombre": "JUAN",
    "primerApellido": "PEREZ",
    "segundoApellido": "GARCIA",
    "cp": "06600",
    "tipoContrato": "01",
    "tipoRegimen": "02",
    "periodicidadPago": "04",
    "entidadFederativa": "DIF",
    "riesgoPuesto": "1",
    "salarioDiarioIntegrado": 850.50,
    "salarioBaseCotizacion": 820.00,
    "nss": "12345678901",
    "numEmpleado": "E-0042",
    "puesto": "Desarrollador"
  }' | jq

Consultar y buscar (?q= cruza RFC, nombre, apellidos y número de empleado):

GET /api/authenticated/{org}/empleados?q=perez

Paso 2 — timbra el recibo

POST /api/authenticated/{org}/invoices
{
  "type": "nomina",
  "empleadoId": "clxem1…",
  "tipoNomina": "O",
  "fechaInicioPeriodo": "2026-08-01",
  "fechaFinPeriodo": "2026-08-15",
  "fechaPago": "2026-08-15",
  "diasPagados": 15,
  "idempotencyKey": "rh:prod:nomina:2026-08-q1:E-0042",
  "percepciones": [
    {
      "tipoPercepcion": "001",
      "descripcion": "Sueldos, salarios rayas y jornales",
      "importeGravado": 12750.00,
      "importeExento": 0
    }
  ],
  "deducciones": [
    { "tipoDeduccion": "002", "descripcion": "ISR", "importe": 1840.25 },
    { "tipoDeduccion": "001", "descripcion": "Seguridad social", "importe": 412.10 }
  ]
}

Campos

CampoRequeridoDescripción
type"nomina"
empleadoIdcuid del empleado.
tipoNomina"O" ordinaria, "E" extraordinaria (finiquito, aguinaldo, PTU).
fechaInicioPeriodo, fechaFinPeriodo, fechaPago"YYYY-MM-DD".
diasPagadosMayor a 0, máximo 366.
percepcionesAl menos una.
deduccionesPuede ir vacío ([]), pero el campo debe existir.
idempotencyKeyDeriva de periodo + empleado.
otrosPagosnoSubsidio al empleo, reembolsos.
incapacidadesno
serienoSobrescribe la serie de nómina.
formaPagonoDefault "03" (transferencia).
nss, clabe, banco, numContrato, tipoJornada, anoAntiguedad, fechaInicioRel, sindicatonoSobrescriben lo registrado en el empleado.
periodicidadPago, entidadFederativa, riesgoPuesto, salarioBaseCotizacion, registroPatronalnoIgual: si el empleado ya los trae, no hace falta repetirlos.

Percepciones

{
  "tipoPercepcion": "001",
  "descripcion": "Sueldos, salarios rayas y jornales",
  "importeGravado": 12750.00,
  "importeExento": 0
}

tipoPercepcion es una clave de c_TipoPercepcion. La separación gravado/exento la determina la ley (aguinaldo exento hasta 30 UMA, prima vacacional hasta 15 UMA, etc.); es un cálculo de nómina, no algo que la API deduzca por ti.

Deducciones

{ "tipoDeduccion": "002", "descripcion": "ISR", "importe": 1840.25 }

Clave de c_TipoDeduccion. Las más usadas: 001 seguridad social, 002 ISR, 004 otros, 006 descuento por incapacidad.

Otros pagos

{
  "tipoOtroPago": "002",
  "descripcion": "Subsidio para el empleo",
  "importe": 0,
  "subsidioCausado": 0
}

Cuando tipoOtroPago es "002" (subsidio para el empleo), subsidioCausado es obligatorio para el SAT — incluso valiendo cero.

Incapacidades

{ "tipoIncapacidad": "02", "diasIncapacidad": 3, "importeMonetario": 1275.00 }

Respuesta

Igual que cualquier otro timbrado:

{
  "id": "clxn8…",
  "uuid": "C3D4E5F6-…",
  "serie": "N",
  "folio": 341,
  "status": "stamped",
  "total": 10497.65,
  "moneda": "MXN",
  "stampedAt": "2026-08-15T22:10:04.000Z",
  "timbresRemaining": 1180
}

Timbrar un periodo completo

Un recibo por empleado, un timbre por recibo. Recorre tu nómina en serie o con poca concurrencia — recuerda el límite de 60 escrituras por minuto por llave.

for emp in empleados_activos:
    key = f"rh:prod:nomina:{periodo}:{emp.num_empleado}"
    post("/invoices", body={
        "type": "nomina",
        "empleadoId": emp.facty_id,
        "tipoNomina": "O",
        "fechaInicioPeriodo": periodo.inicio,
        "fechaFinPeriodo": periodo.fin,
        "fechaPago": periodo.pago,
        "diasPagados": periodo.dias,
        "idempotencyKey": key,
        "percepciones": emp.percepciones,
        "deducciones": emp.deducciones,
    })

Con la llave derivada de periodo + empleado, volver a correr el job tras una interrupción no timbra a nadie dos veces. Ver Idempotencia.

Consultar recibos emitidos

GET /api/authenticated/{org}/invoices?type=nomina&dateFrom=2026-08-01&dateTo=2026-08-31