Tu sistema tiene 40 empresas registradas. Alguien llama a POST /cfdi/timbrar con el empresa_id equivocado. El SAT lo timbra, el folio queda grabado y ahora tienes un CFDI válido con el RFC incorrecto. Cancelarlo requiere la aceptación del receptor — si la da.
Ese es el escenario que hace que los sistemas multi-empresa fallen, y no por lógica de timbrado sino por un problema más simple: no había forma de buscar la empresa correcta antes de usarla.
El endpoint GET /empresas ahora acepta un parámetro ?q= para filtrar. Esta guía explica cómo funciona y cómo construir un flujo de selección de emisor que no dependa de UUIDs memorizados.
Antes de empezar
Necesitas:
- Una API key de Ipsofactura (sandbox o producción)
- Al menos dos empresas registradas en tu cuenta — si no, puedes crearlas con
POST /empresas - Python 3.8+ o curl si quieres correr los ejemplos directamente
Si estás empezando con la API, lee primero la guía de autenticación — los ejemplos aquí asumen que ya tienes el header x-api configurado.
El problema con los UUIDs
Cuando registras una empresa en Ipsofactura, la API devuelve un id en formato UUID:
0bacab57-4dc0-4f15-a026-53e7deaf1df8Ese id es lo que necesitas para timbrar. El problema es que un UUID no le dice nada a nadie. Si tu sistema almacena los empresa_id en una base de datos y los asocia por RFC, funciona. Pero si necesitas buscar en runtime — porque el usuario eligió un cliente, porque vas a timbrar en nombre de un tercero o porque simplemente necesitas validar que el emisor existe y está activo — no había un endpoint para eso.
GET /empresas?q= resuelve exactamente eso.
Cómo funciona el filtro
El parámetro q combina cuatro criterios de búsqueda. Los cuatro criterios se evalúan en paralelo, y la respuesta puede incluir cualquier empresa que coincida con uno o más criterios:
| Modo | Campo | Tipo de match |
|---|---|---|
| RFC | rfc | Parcial, sin importar mayúsculas |
| Nombre fiscal | nombre_fiscal | Parcial, sin importar mayúsculas |
| Régimen fiscal | regimen_fiscal | Exacto (clave de tres dígitos) |
| ID | id | Exacto (UUID completo) |
Una sola llamada con ?q=EKU9 devuelve todas las empresas cuyo RFC contenga esa cadena. Lo mismo con ?q=ESCUELA — busca por nombre fiscal. Si pasas ?q=601, devuelve todas las del régimen general de personas morales. Y si pasas un UUID completo, devuelve la empresa exacta o lista vacía.
El endpoint nunca devuelve empresas de otro partner aunque el RFC coincida. La búsqueda está aislada por API key.
Ejemplos de uso
Buscar por RFC parcial
curl https://api.ipsofactura.com/empresas?q=PAL20 \
-H "x-api: tu_api_key"import requests
resp = requests.get(
"https://api.ipsofactura.com/empresas",
params={"q": "PAL20"},
headers={"x-api": "tu_api_key"}
)
empresas = resp.json()["empresas"]Respuesta:
{
"empresas": [
{
"id": "0bacab57-4dc0-4f15-a026-53e7deaf1df8",
"rfc": "PAL200101AB1",
"nombre_empresa": "Palenca Tecnología",
"nombre_fiscal": "PALENCA TECNOLOGIA SA DE CV",
"regimen_fiscal": "601",
"codigo_postal": "06600",
"es_activo": true
}
]
}Buscar por nombre fiscal
curl "https://api.ipsofactura.com/empresas?q=tecnologia" \
-H "x-api: tu_api_key"Funciona con cualquier fragmento del nombre fiscal. El match es case-insensitive — tecnologia, TECNOLOGIA y Tecnología devuelven el mismo resultado.
Buscar por régimen fiscal
curl "https://api.ipsofactura.com/empresas?q=612" \
-H "x-api: tu_api_key"Útil si tu sistema segmenta operaciones por tipo de persona. El régimen 612 agrupa a personas físicas con actividades empresariales y profesionales; 601 a personas morales en régimen general. El match de regimen_fiscal es exacto, no parcial. Aun así, ?q=6 puede devolver empresas por coincidencias parciales de rfc o nombre_fiscal.
Resolver un UUID
Si ya tienes el empresa_id pero quieres confirmar que sigue activo antes de timbrar:
empresa_id = "0bacab57-4dc0-4f15-a026-53e7deaf1df8"
resp = requests.get(
"https://api.ipsofactura.com/empresas",
params={"q": empresa_id},
headers={"x-api": "tu_api_key"}
)
empresas = resp.json()["empresas"]
if not empresas:
raise ValueError(f"Empresa {empresa_id} no encontrada o no pertenece a este partner")
empresa = empresas[0]
if not empresa["es_activo"]:
raise ValueError(f"Empresa {empresa['rfc']} está inactiva")El flujo completo: buscar → validar → timbrar
Si estás construyendo una interfaz donde el operador elige el emisor antes de facturar, el flujo natural es este:
import requests
API_KEY = "tu_api_key"
BASE = "https://api.ipsofactura.com"
HEADERS = {"x-api": API_KEY, "Content-Type": "application/json"}
def buscar_empresa(q: str) -> list[dict]:
resp = requests.get(f"{BASE}/empresas", params={"q": q}, headers=HEADERS)
resp.raise_for_status()
return resp.json()["empresas"]
def timbrar(empresa_id: str, payload: dict) -> dict:
payload["empresa_id"] = empresa_id
resp = requests.post(f"{BASE}/cfdi/timbrar", json=payload, headers=HEADERS)
resp.raise_for_status()
return resp.json()
# El operador escribe "Palenca" en el buscador
resultados = buscar_empresa("Palenca")
if len(resultados) == 0:
raise ValueError("No se encontraron empresas con ese criterio")
if len(resultados) > 1:
# Mostrar lista al operador para que seleccione
for r in resultados:
print(f"{r['rfc']} — {r['nombre_fiscal']}")
empresa_id = resultados[0]["id"] # simplificado
else:
empresa_id = resultados[0]["id"]
cfdi = timbrar(empresa_id, {
"tipo_comprobante": "I",
"metodo_pago": "PUE",
"forma_pago": "03",
"moneda": "MXN",
"lugar_expedicion": "06600",
"receptor": {
"rfc": "XAXX010101000",
"nombre": "PUBLICO EN GENERAL",
"uso_cfdi": "S01",
"domicilio_fiscal_receptor": "06600",
"regimen_fiscal_receptor": "616"
},
"conceptos": [{
"clave_prod_serv": "81111806",
"cantidad": 1,
"clave_unidad": "E48",
"descripcion": "Servicio de consultoría",
"valor_unitario": 1000.00,
"impuestos": {
"traslados": [{
"base": 1000.00, "impuesto": "002",
"tipo_factor": "Tasa", "tasa_o_cuota": 0.16, "importe": 160.00
}]
}
}]
})
print(f"Timbrado: {cfdi['uuid']}")La ventaja de pasar por el buscador antes de timbrar es que el empresa_id no sale de tu base de datos: sale del estado actual de la API. Si la empresa fue desactivada, el buscador devuelve es_activo: false antes de que el request de timbrado llegue al SAT.
Incluir los certificados en la búsqueda
Si necesitas verificar también que el CSD esté vigente, agrega include_certificados=true:
curl "https://api.ipsofactura.com/empresas?q=PAL20&include_certificados=true" \
-H "x-api: tu_api_key"La respuesta incluye un campo certificados por empresa con el estado del CSD, la fecha de vencimiento y el número de certificado. Útil para alertar al administrador antes de que el CSD expire en producción — no cuando ya falló un timbrado.
Límites a tener en cuenta
El parámetro q acepta hasta 100 caracteres. Queries más largas devuelven 400 VALIDATION_ERROR. No es un límite que vayas a alcanzar con búsquedas normales por RFC o nombre, pero sí si construyes un buscador que pasa input del usuario directamente al query param — valida en tu frontend antes de hacer el request.
El endpoint no pagina los resultados.