c_ClaveProdServ: el catálogo del SAT que rompe más CFDIs de lo que crees
Más de 80,000 claves de productos y servicios, publicadas en Excel, actualizadas sin aviso. Te explico por qué poner 01010101 en todo es un error que el SAT ya está detectando — y cómo manejarlo correctamente.
Cada partida de un CFDI 4.0 requiere dos campos de catálogo: c_ClaveProdServ (qué es lo que vendiste) y c_ClaveUnidad (en qué unidad lo mediste). Son campos obligatorios, de catálogo cerrado, con más de 80,000 valores posibles. La mayoría de los proyectos los implementa mal desde el principio — y el problema no aparece hasta que el SAT empieza a auditar.
Qué son estos catálogos y de dónde vienen
El SAT publica un archivo Excel llamado catCFDI.xls con todos los catálogos del complemento de facturación. El catálogo c_ClaveProdServ tiene actualmente más de 82,000 entradas organizadas en capítulos y partidas, siguiendo la nomenclatura del Sistema Armonizado de la OMA (la misma base que usan las aduanas a nivel mundial). El catálogo c_ClaveUnidad está basado en la norma UN/ECE Rec. 20 e incluye unidades de medida para todo: kilogramos, litros, horas de servicio, piezas, licencias de software.
El SAT actualiza este archivo sin calendario fijo. Hay versiones nuevas que agregan o modifican entradas varias veces al año. Si tu aplicación carga los catálogos una sola vez en el setup inicial, puede estar usando datos desactualizados.
Por qué poner 01010101 en todo es un error
La clave 01010101 existe en el catálogo: su descripción es "No existe en el catálogo". El SAT la incluyó expresamente como válvula de escape temporal durante la transición a CFDI 4.0. El problema: muchos sistemas la adoptaron como valor permanente para todo.
El SAT cruzó sus datos y puede detectar exactamente qué RFC está usando 01010101 en el 100% (o cerca) de sus facturas. Ese patrón es una señal de alerta para auditorías automáticas. No es ilegal por sí solo — el campo técnicamente es válido — pero combinado con otros indicadores (volumen alto, conceptos genéricos, receptores concentrados) pone a la empresa en una categoría de mayor escrutinio.
Más allá del riesgo de auditoría: muchos PACs ya tienen reglas propias que rechazan ciertos conceptos con 01010101. Si tu sistema usa esa clave de forma indiscriminada y el PAC la rechaza, el error llega en producción al momento de timbrar.
El segundo error: consultar el catálogo del SAT en vivo
Algunos equipos resuelven el problema de los catálogos de la peor manera posible: haciendo una petición HTTP al SAT o a algún servicio de terceros en el momento de generar cada factura para validar o resolver la clave. Esto tiene tres problemas graves:
- 1. Latencia en producción. Cada factura que genera tu sistema agrega una o más peticiones HTTP externas al camino crítico. Si el servicio externo tiene latencia o falla, tu facturación falla.
- 2. El SAT no tiene una API oficial de catálogos. Los servicios que ofrecen esta consulta son terceros no oficiales. No hay SLA, no hay garantía de disponibilidad, y los datos pueden estar desactualizados.
- 3. Dependencia externa innecesaria. El catálogo completo ocupa menos de 15 MB en base de datos. No hay razón para que sea una consulta remota — es un dato que debería vivir en tu infraestructura.
Los tres patrones de código — y cuál usar
❌ Patrón 1: valor hardcodeado
// Hardcodeado para "que funcione" — señal de auditoría
$concepto = [
'ClaveProdServ' => '01010101', // ← "No existe en catálogo"
'ClaveUnidad' => 'ACT', // ← "Actividad" (genérico)
'Descripcion' => 'Servicios de desarrollo',
'Cantidad' => '1',
'ValorUnitario' => '5000.00',
];
❌ Patrón 2: consulta en vivo al momento de facturar
// Consulta HTTP externa en cada factura — frágil y lento
public function buscarClave(string $descripcion): string
{
$response = $this->httpClient->request('GET',
'https://tercero-no-oficial.mx/catalogo/buscar',
['query' => ['q' => $descripcion]]
);
// Si este servicio falla → tu facturación falla
return $response->toArray()['clave'];
}
✅ Patrón 3: caché local + ForgeStack API
// Catálogo local: < 15 MB en base de datos, consulta en < 1ms
public function buscarClaveProdServ(string $descripcion): SatCatalogResult
{
return $this->catalogoRepository->buscarPorDescripcion(
$descripcion,
CatalogoTipo::PROD_SERV
);
}
// Actualización programada — no en el camino de la factura
#[AsScheduledTask('0 3 * * 1')] // cada lunes a las 3am
public function actualizarCatalogos(): void
{
$this->forgeStackCfdi->catalogos()->sincronizar();
// descarga el XLS del SAT, importa cambios incrementales
}
$claveProdServ = $this->catalogo
->buscarClaveProdServ('Desarrollo de software a medida');
$concepto = $this->forgeStackCfdi
->concepto()
->descripcion('Servicios de desarrollo')
->claveProdServ($claveProdServ->clave()) // '81112101'
->claveUnidad('E48') // 'Unidad de servicio'
->cantidad(1)
->valorUnitario(5000.00)
->build();
Cómo encontrar la clave correcta para tu producto o servicio
El catálogo usa la taxonomía del Sistema Armonizado. Las claves para servicios digitales y software están en los capítulos 43 (TI) y 81 (servicios profesionales):
| c_ClaveProdServ | Descripción en catálogo | Cuándo usarla |
|---|---|---|
| 43232408 | Plataformas de facturación en línea | SaaS de facturación |
| 43231507 | Paquetes de software de contabilidad | Software de contabilidad |
| 81112101 | Servicios de desarrollo de software | Desarrollo a medida |
| 81111501 | Consultoría en tecnologías de la información | Consultoría TI |
| 43232000 | Software de empresa como servicio (SaaS) | Suscripciones SaaS |
| 80141601 | Servicios de publicidad en internet | Digital ads / marketing |
Nota: siempre verifica en el catálogo oficial vigente. El SAT puede mover o actualizar descripciones en versiones nuevas del Excel.
El catálogo que se olvida: c_ClaveUnidad
La mayoría de los artículos hablan de c_ClaveProdServ y dejan de lado c_ClaveUnidad. Pero el error más frecuente que veo en proyectos PHP es usar ACT (Actividad) para absolutamente todo, incluyendo productos físicos donde correspondería KGM (kilogramo) o H87 (pieza).
| c_ClaveUnidad | Descripción | Cuándo usarla |
|---|---|---|
| E48 | Unidad de servicio | Servicios digitales, SaaS |
| ACT | Actividad | Honorarios, consultoría por proyecto |
| HUR | Hora | Servicios por hora (desarrollo, soporte) |
| H87 | Pieza | Productos unitarios |
| KGM | Kilogramo | Productos por peso |
| LTR | Litro | Productos líquidos |
| A9 | Licencia | Licencias de software |
| MO | Mes | Suscripciones mensuales |
Cómo manejar las actualizaciones del catálogo
El SAT actualiza el Excel sin publicar un diff. Si tu sistema tiene los catálogos cargados en base de datos, necesitas una estrategia de sincronización que no rompa facturas en producción cuando hay cambios:
#[AsCommand('forgestack:catalogos:sincronizar')]
class SincronizarCatalogosCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$resultado = $this->forgeStackCfdi
->catalogos()
->sincronizar([
'solo_cambios' => true, // importación incremental
'marcar_obsoletos' => true, // no borra — marca inactivos
'notificar_cambios' => true, // log auditable de qué cambió
]);
$output->writeln(sprintf(
'Catálogo actualizado: %d nuevas, %d modificadas, %d obsoletas',
$resultado->nuevas(),
$resultado->modificadas(),
$resultado->obsoletas()
));
return Command::SUCCESS;
}
}
Resumen: qué hacer y qué no hacer
No hacer
- ▸Hardcodear
01010101como clave universal - ▸Consultar el catálogo vía HTTP en el path de la factura
- ▸Usar
ACTpara productos físicos - ▸Cargar el catálogo una sola vez y nunca actualizarlo
- ▸Confiar en un tercero no oficial para resolución de claves
Hacer
- ▸Cargar el catálogo completo en base de datos local
- ▸Actualización incremental programada (no en el path de la factura)
- ▸Marcar obsoletos en lugar de borrar (no rompe facturas históricas)
- ▸Mapeo por categoría de producto, no búsqueda de texto libre
- ▸Auditar qué claves usa tu sistema en producción periódicamente
¿Conciliar pagos SPEI a mano cada mes?
Estamos construyendo una herramienta para automatizar la conciliación de pagos SPEI en despachos y negocios mexicanos. Antes de construirla, queremos entender bien el problema — 4 preguntas, 2 minutos.
Sin compromiso, sin dejar tus datos.
Responder la encuesta