Verifica ante el SAT si un RFC está registrado y puede recibir facturas, y coteja nombre y código postal.
Todas las peticiones van al gateway de idoo.dev con tu API key:
API para verificar ante el SAT si un RFC está registrado y es susceptible de recibir facturas, y para cotejar que el nombre o razón social y el código postal coincidan con lo que el SAT tiene registrado. Cada consulta se hace en vivo contra el servicio oficial del SAT: no hay padrones cacheados ni copias locales.
Además, permite consultar si un RFC aparece en las listas públicas del SAT de los artículos 69 y 69-B del Código Fiscal de la Federación: EFOS (empresas que facturan operaciones simuladas), contribuyentes no localizados, créditos fiscales firmes o exigibles, condonaciones, sellos digitales cancelados y las demás. Esas listas se descargan del SAT a diario y se consultan localmente, así que responden en milisegundos.
Todas las peticiones van al gateway de idoo.dev con tu API key:
https://idoo.dev/v1/valida-rfc/{accion}
curl -X POST "https://idoo.dev/v1/valida-rfc/validar" \
-H "Authorization: Bearer {tu_api_key}" \
-H "Content-Type: application/json" \
-d '{"rfc": "FCA130603EX5"}'
También se acepta el header X-API-KEY: {tu_api_key}. Las keys se crean en tu panel de idoo.dev.
El ambiente lo determina tu API key, no un parámetro:
| Key | Ambiente | Comportamiento |
|---|---|---|
idoo_sk_test_... |
Sandbox | Devuelve una respuesta simulada al instante, sin consultar al SAT. Gratis e ilimitado: no consume la cuota de tu plan. La forma de la respuesta es idéntica a producción y trae "simulado": true. |
idoo_sk_live_... |
Producción | Consulta real al SAT. Cada consulta descuenta 1 de tu plan. |
Sandbox siempre responde que el RFC es válido, sin importar cuál mandes: sirve para integrar el contrato de la API, no para probar casos de negocio. Los formatos inválidos (422) sí se validan en sandbox.
En listas y efos, sandbox no consulta ninguna lista: un RFC que empiece por EFO (por ejemplo EFO010101AB1) responde como DEFINITIVO en el 69-B y cualquier otro responde que no aparece, para que pruebes las dos ramas de tu integración.
validar, listas y efos descuentan cuota. listas/estado y salud son libres (aplica únicamente al rate limit de tu plan). El header X-Idoo-Cuota-Restante de cada respuesta cobrable te dice cuántas consultas te quedan en el periodo.
Todas las respuestas (éxito o error) usan el mismo sobre JSON:
{
"valid": true,
"status": 200,
"message": "RFC válido, y susceptible de recibir facturas",
"data": { "...": "..." },
"timestamp": "2026-09-09 07:26:07"
}
En los errores, valid es false y se agrega el campo error con un código estable, pensado para que lo compares en tu código — a diferencia de message, que es texto legible y puede cambiar.
status: 200no significa "RFC válido". Significa que la consulta se realizó con éxito. El veredicto está endata.valido.
Tarda típicamente entre 2 y 4 segundos, y puede llegar a 20 cuando se necesitan varios intentos. Configura el timeout de tu cliente en al menos 30 segundos y haz la llamada en segundo plano si tu interfaz no puede esperar. En sandbox la respuesta es inmediata.
| Código | Significado |
|---|---|
| 200 | Consulta realizada (revisa data.valido para el veredicto). |
| 400 | El cuerpo no es un JSON válido. |
| 401 | No se envió API key, o alguien llamó al backend directo sin pasar por el gateway. |
| 402 | Cuota de consultas de tu plan agotada (solo producción). |
| 403 | API key inválida o sin suscripción activa a esta API. |
| 404 | La ruta no existe. |
| 405 | Método HTTP no permitido. |
| 422 | Los datos enviados no son válidos: falta el RFC, o su formato (o el del CP) es incorrecto. |
| 429 | Excediste el rate limit por minuto de tu plan (Retry-After indica cuándo reintentar). |
| 502 | El SAT no respondió, o devolvió algo que no se pudo interpretar. |
| 503 | No se pudo obtener tras varios intentos, o las listas del SAT no están disponibles. |
| 504 | La consulta al SAT excedió el tiempo máximo. |
| Método | Endpoint | Descripción | ¿Descuenta cuota? |
|---|---|---|---|
| POST | validar |
Valida un RFC ante el SAT; opcionalmente coteja nombre y CP. | Sí |
| POST | listas |
Busca un RFC en todas las listas públicas del SAT (Art. 69, 69-B y 69-B Bis). | Sí |
| POST | efos |
Busca un RFC solo en las listas de EFOS (Art. 69-B y 69-B Bis). | Sí |
| GET | listas/estado |
Cuántos registros tiene cada lista y cuándo se actualizó. | No |
| GET | salud |
Estado del servicio, para monitoreo. | No |
Consulta un RFC ante el SAT.
Body:
{
"rfc": "FON2010129P5",
"nombre": "FONDO DE CULTURA ECONOMICA",
"cp": "14738"
}
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
rfc |
string | Sí | RFC a validar: 12 caracteres para persona moral, 13 para persona física. No distingue mayúsculas. |
nombre |
string | No | Nombre, denominación o razón social a cotejar. |
cp |
string | No | Código postal del domicilio fiscal, 5 dígitos. |
Enviar nombre o cp activa la validación cruzada del SAT: el RFC puede existir y aun así responder que los datos no coinciden. El cotejo del nombre es literal — el SAT compara contra la razón social exacta que tiene registrada, sin régimen de capital (SA DE CV) salvo que así esté inscrita.
Respuesta 200 — solo RFC:
{
"valid": true,
"status": 200,
"message": "RFC válido, y susceptible de recibir facturas",
"data": {
"rfc": "FCA130603EX5",
"nombre": null,
"cp": null,
"resultado": "RFC válido, y susceptible de recibir facturas",
"valido": true,
"simulado": false,
"idoo": { "consumidor_id": 4242, "plan": "Pro", "ambiente": "produccion" }
},
"timestamp": "2026-09-09 07:26:07"
}
Respuesta 200 — con nombre y CP que no coinciden:
{
"data": {
"rfc": "FCA130603EX5",
"nombre": "FONDO DE CULTURA ECONOMICA",
"cp": "14738",
"resultado": "EL Nombre o Razón Social y CP no coinciden con lo registrado en el RFC",
"valido": false,
"simulado": false
},
"message": "EL Nombre o Razón Social y CP no coinciden con lo registrado en el RFC"
}
Campos de data:
| Campo | Tipo | Descripción |
|---|---|---|
rfc |
string | RFC consultado, normalizado a mayúsculas. |
nombre |
string | null | Nombre cotejado, o null si no lo enviaste. |
cp |
string | null | Código postal cotejado, o null si no lo enviaste. |
resultado |
string | Veredicto textual del SAT, tal cual lo devuelve. |
valido |
boolean | true solo si el SAT respondió que el RFC es válido. Si enviaste nombre o cp y no coinciden, es false. |
simulado |
boolean | true cuando la respuesta viene de sandbox y no del SAT. |
idoo |
object | Contexto que el gateway asoció a tu key: consumidor_id, plan y ambiente. Útil para confirmar con qué key estás llamando. |
Errores específicos:
| Código | error |
Cuándo ocurre |
|---|---|---|
| 422 | rfc_requerido |
No enviaste el campo rfc. |
| 422 | rfc_formato_invalido |
El RFC no tiene forma de RFC (12 o 13 caracteres, con la estructura oficial). |
| 422 | cp_formato_invalido |
El código postal no tiene 5 dígitos. |
| 400 | json_invalido |
El cuerpo no es JSON válido. |
| 502 | sat_inalcanzable |
El SAT no respondió. Transitorio: reintenta. |
| 502 | respuesta_inesperada |
El SAT respondió algo que no se pudo interpretar. Transitorio. |
| 503 | no_resuelto |
No hay respuesta del SAT. Transitorio: reintenta. |
| 504 | tiempo_agotado |
La consulta excedió el tiempo máximo. Transitorio. |
Busca un RFC en todas las listas públicas del SAT. No consulta al SAT en vivo: lee una copia local que se descarga del SAT cada día, así que responde en milisegundos y no depende de que el SAT esté arriba.
Body:
{ "rfc": "AAA080808HL8" }
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
rfc |
string | Sí | RFC a buscar: 12 caracteres para persona moral, 13 para persona física. No distingue mayúsculas. |
Respuesta 200 — RFC encontrado en una lista:
{
"valid": true,
"status": 200,
"message": "El RFC aparece en 1 lista(s): 69b",
"data": {
"rfc": "AAA080808HL8",
"encontrado": true,
"razon_social": "ASESORES EN AVALÚOS Y ACTIVOS, S.A. DE C.V.",
"en_listas": ["69b"],
"listas": {
"69b": {
"nombre": "EFOS: operaciones presuntamente inexistentes (Art. 69-B CFF)",
"articulo": "69-B",
"actualizado": "2026-10-23",
"encontrado": true,
"registros": [
{
"rfc": "AAA080808HL8",
"razon_social": "ASESORES EN AVALÚOS Y ACTIVOS, S.A. DE C.V.",
"situacion": "SENTENCIA FAVORABLE",
"oficio_presuncion_sat": "500-05-2018-16632 de fecha 01 de junio de 2018",
"pub_sat_presuntos": "01/06/2018",
"oficio_definitivos_sat": "500-05-2018-27105 de fecha 27 de septiembre de 2018",
"pub_sat_definitivos": "28/09/2018",
"oficio_sentencia_sat": "500-05-2019-7305 de fecha 5 de marzo de 2019",
"pub_sat_sentencia": "05/03/2019"
}
]
},
"69b_bis": { "nombre": "…", "articulo": "69-B Bis", "actualizado": "2026-10-23", "encontrado": false, "registros": [] },
"cancelados": { "nombre": "…", "articulo": "69", "actualizado": "2026-10-23", "encontrado": false, "registros": [] },
"…": "una entrada por cada lista del catálogo"
},
"simulado": false,
"idoo": { "consumidor_id": 4242, "plan": "Pro", "ambiente": "produccion" }
},
"timestamp": "2026-10-23 14:02:11"
}
Campos de data:
| Campo | Tipo | Descripción |
|---|---|---|
rfc |
string | RFC consultado, normalizado a mayúsculas. |
encontrado |
boolean | true si el RFC aparece en al menos una lista. |
razon_social |
string | null | Nombre o razón social que el SAT publica para ese RFC, o null si no aparece. |
en_listas |
string[] | Claves de las listas en las que aparece. Vacío si no aparece en ninguna. |
listas |
object | Una entrada por lista, con la clave como nombre. Siempre vienen todas, aparezca el RFC o no. |
listas.*.nombre |
string | Nombre legible de la lista. |
listas.*.articulo |
string | 69, 69-B o 69-B Bis. |
listas.*.actualizado |
string | null | Fecha de la última descarga exitosa de esa lista. Un encontrado: false solo vale a esa fecha. null si la lista nunca se ha cargado. |
listas.*.encontrado |
boolean | Si el RFC aparece en esa lista. |
listas.*.registros |
array | Los registros tal como los publica el SAT; las columnas dependen de la lista (ver abajo). Un RFC puede traer varios en una misma lista (varios créditos, varias fechas). |
Listas y sus columnas:
| Clave | Artículo | Qué es | Columnas de cada registro |
|---|---|---|---|
69b |
69-B | EFOS: contribuyentes con operaciones presuntamente inexistentes | situacion (PRESUNTO, DESVIRTUADO, DEFINITIVO, SENTENCIA FAVORABLE) y, por fase, oficio_*_sat, pub_sat_*, oficio_*_dof, pub_dof_* |
69b_bis |
69-B Bis | Transmisión indebida de pérdidas fiscales | situacion (DEFINITIVO, SENTENCIA FAVORABLE), oficio_definitivo_*, pub_*_definitivo, oficio_sentencia_*, pub_*_sentencia |
cancelados |
69 | Créditos fiscales cancelados | tipo_persona, supuesto, fecha_cancelacion, monto, fecha_publicacion, entidad_federativa |
exigibles |
69 | Créditos fiscales exigibles | tipo_persona, supuesto, fecha_primera_publicacion, entidad_federativa |
firmes |
69 | Créditos fiscales firmes | igual que exigibles |
no_localizados |
69 | Contribuyentes no localizados | igual que exigibles |
sentencias |
69 | Sentencias condenatorias por delito fiscal | igual que exigibles |
reduccion_multas |
69 | Reducción de multas (Art. 74) | tipo_persona, supuesto, fecha_autorizacion, monto, fecha_publicacion, entidad_federativa |
reduccion_recargos |
69 | Reducción de recargos (Art. 21) | tipo_persona, supuesto, fecha_primera_publicacion, monto, fecha_publicacion_monto, entidad_federativa |
condonados_concurso |
69 | Condonados por concurso mercantil (Art. 146-B) | igual que reduccion_recargos |
condonados_decreto |
69 | Condonados por decreto de 2015 | igual que reduccion_recargos |
retorno_inversiones |
69 | Retorno de inversiones | igual que reduccion_recargos |
csd_sin_efectos |
69 | Certificados de sello digital dejados sin efectos | supuesto_cancelacion, fecha_cancelacion, administracion_responsable, fecha_publicacion |
entes_publicos |
69 | Entes públicos y de gobierno omisos | supuesto, ejercicio, periodo, fecha_primera_publicacion |
condonados_2007_2015 |
69 | Condonados de 2007 a 2015 (histórico) | anio, tipo_persona, monto, motivo, entidad_federativa |
cancelados_146a |
69 | Cancelados Art. 146-A de 2007 a 2015 (histórico) | igual que condonados_2007_2015 |
Todos los registros traen además rfc y razon_social. Las fechas van como YYYY-MM-DD, salvo las de publicación del 69-B (pub_*), que se conservan como las publica el SAT (DD/MM/YYYY). tipo_persona es M (moral) o F (física); en las listas históricas viene como texto (Persona moral).
Errores específicos:
| Código | error |
Cuándo ocurre |
|---|---|---|
| 422 | rfc_requerido |
No enviaste el campo rfc. |
| 422 | rfc_formato_invalido |
El RFC no tiene forma de RFC. |
| 400 | json_invalido |
El cuerpo no es JSON válido. |
| 405 | metodo_no_permitido |
Llamaste con GET en vez de POST. |
| 503 | listas_no_disponibles |
La copia local de las listas no está disponible. Transitorio: reintenta en unos minutos. |
En sandbox no se consulta ninguna lista: cualquier RFC que empiece por EFO (por ejemplo EFO010101AB1) responde como DEFINITIVO en la lista 69b, y cualquier otro responde que no aparece en ninguna. Así puedes probar las dos ramas de tu integración.
Igual que listas, pero solo busca en las listas del artículo 69-B (69b) y 69-B Bis (69b_bis): es la consulta típica antes de aceptar una factura de un proveedor. La respuesta tiene la misma forma, con solo esas dos entradas en listas.
curl -X POST "https://idoo.dev/v1/valida-rfc/efos" \
-H "Authorization: Bearer {tu_api_key}" \
-H "Content-Type: application/json" \
-d '{"rfc": "AAA080808HL8"}'
El message resume el veredicto: El RFC aparece en el artículo 69-B con situación DEFINITIVO o El RFC no aparece en las listas del artículo 69-B. Para decidir en código usa data.encontrado y data.listas["69b"].registros[0].situacion.
encontrado: trueno siempre es malo. Que un RFC aparezca en el 69-B no significa que sea un EFOS confirmado;situaciondice en qué fase está:
situacion |
Qué significa | ¿Afecta las facturas que emitió? |
|---|---|---|
PRESUNTO |
El SAT presume que factura operaciones inexistentes; el contribuyente está a tiempo de desvirtuarlo. | Todavía no, pero es señal de riesgo. |
DESVIRTUADO |
El contribuyente demostró que sus operaciones son reales. | No. |
DEFINITIVO |
Confirmado: sus facturas no tienen efectos fiscales. | Sí. |
SENTENCIA FAVORABLE |
Un tribunal le dio la razón después de haber sido definitivo. | No. |
Si el RFC tiene varios registros en 69b, cada uno es un procedimiento distinto con su propia situación: un mismo RFC puede estar DEFINITIVO por un oficio y con SENTENCIA FAVORABLE por otro.
Dice cuántos registros activos tiene cada lista y cuándo se actualizó por última vez. Sirve para que tu sistema sepa qué tan fresca es la información antes de confiar en un encontrado: false. No descuenta cuota.
curl "https://idoo.dev/v1/valida-rfc/listas/estado" \
-H "Authorization: Bearer {tu_api_key}"
Respuesta 200:
{
"valid": true,
"status": 200,
"message": "Estado de las listas",
"data": {
"listas": {
"69b": {
"nombre": "EFOS: operaciones presuntamente inexistentes (Art. 69-B CFF)",
"articulo": "69-B",
"registros": 13874,
"actualizado": "2026-10-23",
"fecha_corte": "2026-09-30",
"ultima_importacion": "2026-10-23 03:31:08",
"estatus": "OK"
},
"…": "una entrada por lista"
},
"simulado": false,
"idoo": { "consumidor_id": 4242, "plan": "Pro", "ambiente": "produccion" }
},
"timestamp": "2026-10-23 14:02:11"
}
actualizado es la fecha en que se descargó el archivo del SAT; fecha_corte es la fecha que el propio SAT declara dentro del archivo (solo la traen las listas del 69-B) y suele ir unas semanas atrás.
Comprueba que el servicio responde. Útil para monitoreo; no consulta al SAT ni descuenta cuota.
curl "https://idoo.dev/v1/valida-rfc/salud" \
-H "Authorization: Bearer {tu_api_key}"
Respuesta 200:
{
"valid": true,
"status": 200,
"message": "Servicio operativo",
"data": {
"servicio": "valida-rfc",
"idoo": { "consumidor_id": 4242, "plan": "Pro", "ambiente": "produccion" }
},
"timestamp": "2026-09-09 07:25:36"
}
idoo_sk_test_... — es gratis e ilimitado. Ahí armas el manejo del sobre JSON y de los errores 422 sin gastar consultas.422 y no descuenta cuota, pero te cuesta un viaje de red; validarlo en tu formulario es mejor experiencia.validar con el RFC solo, o con nombre y cp si necesitas el cotejo completo (por ejemplo, para dar de alta a un cliente que va a recibir facturas).data.valido, no en el status. Un 200 con "valido": false es una respuesta correcta del SAT diciendo que ese RFC no sirve para facturar.502/503/504 con reintentos acotados y espera entre ellos: el SAT se cae seguido. Recuerda que cada reintento descuenta cuota.Si además usas las listas del SAT:
efos antes de aceptar la factura de un proveedor y decide según situacion. Usa listas cuando necesites el panorama completo, por ejemplo al dar de alta un cliente o en una auditoría: cuesta lo mismo, pero devuelve las 16 listas.actualizado junto con el resultado: un "no aparece" vale hasta esa fecha, y las listas cambian una vez al día.listas/estado en tu monitoreo (no descuenta cuota). Si actualizado se queda atrás varios días, tus "no aparece" van perdiendo valor.Todos corren en sandbox y no consumen cuota. Sustituye {tu_api_key} por tu key idoo_sk_test_....
El caso más simple: ¿este RFC puede recibir facturas?
curl -X POST "https://idoo.dev/v1/valida-rfc/validar" \
-H "Authorization: Bearer {tu_api_key}" \
-H "Content-Type: application/json" \
-d '{"rfc": "FCA130603EX5"}'
En producción, data.valido será true si el SAT lo tiene registrado y activo para facturación.
Al dar de alta un cliente conviene verificar que los tres datos que te dio coinciden entre sí; es lo que evita rechazos al timbrar después.
curl -X POST "https://idoo.dev/v1/valida-rfc/validar" \
-H "Authorization: Bearer {tu_api_key}" \
-H "Content-Type: application/json" \
-d '{
"rfc": "FCA130603EX5",
"nombre": "FONDO DE CULTURA ECONOMICA",
"cp": "14738"
}'
Si el SAT responde que no coinciden, el status sigue siendo 200: la consulta funcionó. El problema está en data.valido: false y en data.resultado, que te dice qué falló.
El SAT no siempre responde. Este es el patrón mínimo de reintento, en PHP:
$intentos = 0;
do {
$respuesta = miCliente()->post('validar', ['rfc' => $rfc]);
$codigo = $respuesta['status'];
if ($codigo < 500) {
break; // 200, 422, 400: no tiene caso reintentar
}
$intentos++;
sleep(2 * $intentos); // espera creciente
} while ($intentos < 3);
Tres intentos son suficientes: si el SAT sigue caído, avísale al usuario y guarda el RFC para reintentar más tarde. No lo pongas en un bucle infinito: cada vuelta descuenta cuota.
Antes de aceptar una factura, confirma que el emisor no es un EFOS definitivo. En sandbox, prueba con EFO010101AB1 (aparece) y con cualquier otro RFC (no aparece).
curl -X POST "https://idoo.dev/v1/valida-rfc/efos" -H "Authorization: Bearer {tu_api_key}" -H "Content-Type: application/json" -d '{"rfc": "EFO010101AB1"}'
En PHP:
$r = miCliente()->post('efos', ['rfc' => $rfcEmisor]);
if (!$r['valid']) {
throw new RuntimeException($r['error'] ?? 'error ' . $r['status']);
}
$situaciones = array_column($r['data']['listas']['69b']['registros'], 'situacion');
if (in_array('DEFINITIVO', $situaciones, true)) {
// Rechazar: sus facturas no tienen efectos fiscales
} elseif (in_array('PRESUNTO', $situaciones, true)) {
// Revisar a mano: está en riesgo
}
efos no va al SAT: responde en milisegundos, así que un timeout de 10 segundos es de sobra.
status como el veredicto. 200 significa "consulté al SAT y esto es lo que dijo". El veredicto está en data.valido.FONDO DE CULTURA ECONOMICA y tú envías FONDO DE CULTURA ECONOMICA SA DE CV, el cotejo falla. Manda el nombre tal como está en la constancia de situación fiscal.422. Ese error es de tus datos, no del SAT: reintentar da exactamente lo mismo.DESVIRTUADO y SENTENCIA FAVORABLE son contribuyentes que ganaron; solo DEFINITIVO invalida sus facturas.actualizado de esa lista.Esta API todavía no tiene reseñas.