Cómo extraer listados de Google Jobs con Python
Empresa, ubicación y enlaces para aplicar de Google Jobs en JSON, y la forma de respuesta que rompe el parsing ingenuo con un HTTP 200 normal. Probado en agosto de 2026.

Quieres listados de empleo para un puesto y un lugar, nombre de la empresa, ubicación, un enlace para aplicar, a una escala que va más allá de ir haciendo clic en las tarjetas de empleo de Google una a una: un portal de empleo de nicho, un feed de inteligencia de mercado, o un script que sigue quién está contratando para un puesto durante las semanas siguientes.
La solicitud
Una llamada a /v1/jobs recibe una consulta de búsqueda tal como la escribirías en Google, más los parámetros opcionales country, language y domain si necesitas resultados acotados a un mercado concreto.
La forma de respuesta que te rompe el código si te la saltas
Google no devuelve un módulo de empleo para todas las consultas, y cuando no lo hace, /v1/jobs igualmente responde con un HTTP 200. El cuerpo es {"jobs_results": ["There are no jobs for this query"]}, un listado que contiene una sola cadena en vez de objetos de empleo. Reprodujimos esto el 27 de agosto de 2026: la consulta “product manager” devolvió ese mensaje en todos los intentos, mientras que “product manager jobs” devolvió diez listados reales para lo que en la práctica es la misma búsqueda. Un bucle que asume que jobs_results es un listado de dicts, for job in results: print(job["title"]), lanza un TypeError en cuanto llega a la cadena, porque no puedes indexar una cadena con otra cadena. La solución es comprobar que cada entrada es un dict con un title antes de tratarla como un empleo, no solo que el listado no esté vacío:
import requests
API_KEY = "faf_live_..." # tu clave de FastAF
resp = requests.get(
"https://api.fastaf.co/v1/jobs",
params={"query": "product manager jobs"},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
resp.raise_for_status()
raw_results = resp.json()["jobs_results"]
# Google no siempre tiene un módulo de empleo para una consulta. Cuando no lo tiene,
# jobs_results es ["There are no jobs for this query"], una cadena, no un empleo.
listings = [r for r in raw_results if isinstance(r, dict) and "title" in r]
if not listings:
print("No jobs module for this query.")
for job in listings:
apply_options = job.get("apply_options", [])
apply_via = apply_options[0]["title"] if apply_options else None
print(f'{job["title"]} @ {job["company_name"]} ({job["location"]})')
print(f' apply via {apply_via}, share link: {job["share_link"]}')
Ejecutamos esto contra una búsqueda real de “data engineer” mientras escribíamos el post (agosto de 2026): las diez entradas de jobs_results eran objetos de empleo correctos, así que listings salió con la misma longitud que el listado bruto. La comprobación no cuesta nada cuando el módulo está presente; solo evita el fallo cuando no lo está.
Los enlaces para aplicar están en apply_options, no en el empleo mismo
Cada listado de nuestra muestra traía un listado apply_options, pares de {title, link} como {"title": "LinkedIn", "link": "/goto?..."}, una entrada por cada sitio que publica la oferta: la propia página de empleo de la empresa, y luego los que correspondan de Indeed, LinkedIn, ZipRecruiter, Glassdoor y portales parecidos que también la recogieron. El link de cada uno es una ruta relativa, no una URL lista para abrir, así que guárdalo junto a su title en vez de asumir que funciona como href independiente. Cada listado también traía un share_link aparte, una URL absoluta completa de vuelta a esa misma oferta en la propia página de búsqueda de empleo de Google. Ese es el único campo del payload que es una URL completa por sí sola, lo que lo convierte en la opción natural para un enlace de “ver online” o una clave estable para deduplicar un empleo entre dos ejecuciones de búsqueda.
Qué más trae un listado
Más allá de título, empresa y ubicación, cada entrada trae via (el sitio al que Google atribuye el listado), extensions (etiquetas cortas como “Full-time” o “20 days ago”), una description completa, y job_highlights, un listado de viñetas agrupadas bajo encabezados como “Qualifications”, “Responsibilities” y “Benefits” cuando el parser de Google los encontró en la oferta. No todos los listados tienen todos los grupos: en nuestra muestra, la mitad de las ofertas se saltaban “Benefits” por completo y una solo tenía “Responsibilities”, así que trata job_highlights como un listado en el que buscar por encabezado, no como un conjunto fijo de campos con nombre.
Cuánto cuesta
/v1/jobs cuesta 5 créditos por llamada, tanto si devuelve diez listados como si devuelve la cadena de sin-resultados. Los 500 créditos del plan gratuito cubren 100 llamadas. En el pack Scale ($99 por 300.000 créditos) son 60.000 llamadas, $1,65 por cada 1.000.
En resumen
Mete la comprobación de isinstance y title en tu parsing desde el principio, porque la respuesta de sin-empleos es un 200 normal y bien formado, no un error que tu try/except vaya a ver nunca. A partir de ahí, apply_options te lleva a la oferta y share_link te da una URL estable para el listado en sí. Consigue 500 créditos gratis, sin tarjeta y prueba tus propias combinaciones de puesto y ubicación.