By the end of this tutorial you will have a Python script that pulls Spanish job offers from infojobs.net into structured rows: offer title, company name, city, contract type, working day, remote mode, published salary band, publication date and plain-text description.
InfoJobs Spain (infojobs.net) blocks plain HTTP scrapers quickly. In tests on 8 November 2026, direct HTTP requests from a local machine and from a datacenter server hit a Distil captcha page after four to five pages. InfoJobs Scraper (reventadata/infojobs-scraper) is the Apify Actor I built to handle transport and extraction automatically.
You pass a keyword and optional filters from Python with the Apify API client, and the Actor returns one row per offer. The sample rows shown below are sample rows from a run on 2026-11-10.
⏱️ Time estimate: One sitting: install one package, set one token, run one short Python script.
Prerequisites
You need three things to follow along:
- Python 3.11 or newer (
apify-client3.x requires it). - A free Apify account, which you can create with an email address or GitHub login.
- Your Apify API token, copied from Settings > API & Integrations in Apify Console.
Install the client and export your token:
pip install -U "apify-client~=3.0" export APIFY_API_TOKEN="your_apify_token_here" No browser setup, proxy configuration or HTML parser is needed on your machine.
What you'll achieve
You will run a search on infojobs.net for permanent waiter (camarero) job offers in the province of Madrid (provinceId: "33", contractTypeId: "1"), read the resulting dataset in Python, and print a two-line summary for each offer with its published salary range and working schedule.
You will also see how InfoJobs mixes monthly and annual gross salary bands in the same result set, how promoted listings appear in search results, and how to enable optional detail-page enrichment (fetchDetails) when you need applicant counts, required skills, languages, minimum studies and minimum experience.
Whether you are comparing hospitality pay bands across Spanish provinces, tracking remote tech roles, or feeding a job board analysis table, the same Python script handles the job by changing only the input dictionary.
Step 1: Access InfoJobs Scraper
Open the InfoJobs Scraper page on Apify Store. The Try for free button on the listing links to console.apify.com/actors/<id>?addFromActorId=<id> so you can run it in Apify Console. Because this tutorial runs the Actor from Python through the API, you only need its identifier: reventadata/infojobs-scraper.
Step 2: Configure search parameters and optional filters
The Actor sends your search parameters to infojobs.net through its unblocking service and checks the search object echoed back by the page to confirm every filter was applied:
-
keyword: Job title or search terms (for example"camarero"). Leave empty ("") to list all offers across Spain. -
provinceId: Digits-only InfoJobs province identifier, such as"33"for Madrid,"9"for Barcelona,"49"for Valencia/València or"43"for Sevilla. Non-digit values are rejected before any request is sent. -
categoryId: Digits-only InfoJobs category identifier, such as"160"for Turismo y restauración or"170"for Atención a clientes. -
contractTypeId: One of eight fixed contract IDs ("1"Indefinido,"8"De duración determinada,"9"Fijo discontinuo,"5"Otros contratos,"4"A tiempo parcial,"6"De relevo,"3"Formativo,"10"Autónomo). Any other value is refused before the run starts. -
teleworkingId:"1"(Presencial),"2"(Solo teletrabajo) or"4"(Sin especificar). Any other value is refused before the run starts. -
sinceDate:"ANY"(default),"_24_HOURS","_7_DAYS"or"_15_DAYS". Any other value is refused before the run starts. -
sortBy:"PUBLICATION_DATE"or"RELEVANCE". When omitted, InfoJobs defaults to relevance with a keyword and newest without one. -
fetchDetails: Boolean (falseby default). Whentrue, fetches each offer's detail page sequentially to addapplicants,requiredSkills,requiredLanguages,minimumStudiesandminimumExperience. -
maxItemsandmaxPages: Caps on stored rows (default100) and search pages fetched (default50, maximum2999).
For example, to narrow a search to on-site hospitality offers in Madrid posted within the last 24 hours and sorted by publication date:
{ "keyword": "camarero", "provinceId": "33", "categoryId": "160", "contractTypeId": "1", "teleworkingId": "1", "sinceDate": "_24_HOURS", "sortBy": "PUBLICATION_DATE", "maxItems": 20, "maxPages": 1 } Step 3: Run the Actor from Python
This script initializes ApifyClient, runs the Actor with the example task configuration (keyword: "camarero", provinceId: "33", contractTypeId: "1"), verifies that the run succeeded, and prints two lines per offer:
import os from apify_client import ApifyClient # 1. Initialise the client with your token from the environment client = ApifyClient(os.environ["APIFY_API_TOKEN"]) # 2. Describe the search: permanent waiter offers in Madrid run_input = { "keyword": "camarero", "provinceId": "33", "contractTypeId": "1", "maxItems": 20, "maxPages": 1, } # 3. Start the Actor and wait for it to finish run = client.actor("reventadata/infojobs-scraper").call(run_input=run_input) if run is None or run.status != "SUCCEEDED": status = run.status if run else "unknown" raise SystemExit(f"Run did not succeed (status: {status}).") # 4. Read the rows from the run's default dataset items = client.dataset(run.default_dataset_id).list_items().items # 5. Print two lines per offer, with a salary summary when published for item in items: s_min, s_max, period = item.get("salaryMin"), item.get("salaryMax"), item.get("salaryPeriod") salary = f"{s_min}-{s_max} EUR/{period}" if s_min is not None and s_max is not None else "no salary stated" print(f"{item.get('title')} | {item.get('companyName')} | {item.get('city')} | {salary}") print(f" {item.get('contractType')}, {item.get('workday')}, {item.get('teleworking')}") Behind the scenes, the Actor fetches each search page through its unblocking service, decodes the embedded window.__INITIAL_PROPS__ JSON payload, verifies that InfoJobs applied your filters, strips disallowed tracking query parameters (applicationOrigin) from offer URLs, deduplicates repeated offer IDs across pages, and pushes clean rows to your default dataset.
Step 4: View and download results
Running the print loop on the first four sample rows from a run on 2026-11-10 produces:
Presentador/a de juegos en Italiano | EVOLUTION MADRID SL. | Madrid | 1500-1800 EUR/MONTH Contrato indefinido, Jornada completa, Presencial CONDUCTOR/A VTC · Turno LUNES - MARTES · Contrato indefinido | AURO NEW TRANSPORT CONCEPT SL. | Madrid | 750-900 EUR/MONTH Contrato indefinido, Jornada parcial - noche, Presencial AYUDANTE DE CAMARERO/A | Casino de Juego Gran Madrid | Madrid | 18000-20000 EUR/YEAR Contrato indefinido, Jornada completa, Presencial Camarero/a | InfoJobs Assistance | Móstoles | 1500-1700 EUR/MONTH Contrato indefinido, Jornada completa, Presencial Here is one complete record from those sample rows from a run on 2026-11-10:
{ "id": "6dedd11cef410e9c7af39ef5c0c138", "url": "https://www.infojobs.net/madrid/ayudante-camarero/of-i6dedd11cef410e9c7af39ef5c0c138", "title": "AYUDANTE DE CAMARERO/A", "description": "En Casino Gran Madrid buscamos incorporar a un/a Ayudante de Camarero/a para trabajar a jornada completa en nuestro Casino de Colón.\n•\tAtención al cliente del restaurante sirviendo bebidas, platos, postres, café. \n•\tCobro al cliente.\n•\tAyuda en la preparación de mesas en su rango correspondiente. \n•\tOrden en restaurante al cierre y preparación para el día siguiente.\n\nSe ofrece\n- Contrato Indefinido.\n- Jornada completa, horarios rotativos: 14:00 a 22:00 / 17:00 a 01:00/ 19:00 a 03:00\n- Beneficios sociales: Seguro de vida, Comedor subvencionado: menú diario incluido, parking gratuito para empleados, ayudas sociales, etc.", "city": "Madrid", "contractType": "Contrato indefinido", "workday": "Jornada completa", "teleworking": "Presencial", "salaryMin": 18000, "salaryMax": 20000, "salaryPeriod": "YEAR", "salaryCurrency": "EUR", "salaryType": "GROSS", "publishedAt": "2026-11-10T19:30:05Z", "companyName": "Casino de Juego Gran Madrid", "companyUrl": "https://casinogranmadrid.ofertas-trabajo.infojobs.net", "upsellings": [ "BUMP_WEEKLY" ], "executive": false, "searchUrl": "https://www.infojobs.net/jobsearch/search-results/list.xhtml?keyword=camarero&page=1&provinceIds=33&contractTypeIds=1" } Step 5: Enrich rows with detail pages (optional)
When fetchDetails is set to true, the Actor requests each offer's detail page one at a time and appends five fields to the row. On a detail capture dated 2026-11-08 (of-ib6e209c3f74aec8a552ec465a258a4), those five fields are:
{ "applicants": 355, "requiredSkills": [ "Servicio en sala", "Recogida de mesa", "Manipulación de alimentos" ], "requiredLanguages": [ { "language": "Inglés", "level": "Básico" } ], "minimumStudies": "Sin estudios", "minimumExperience": "No Requerida" } If an individual detail page cannot be read, the Actor keeps the offer row with those five fields set to null and logs a warning.
Understanding your results
Every search-only row contains 19 fields, and enabling fetchDetails adds 5 more (24 fields in total):
-
idandurl: The 30-character hex offer code and the cleanedhttps://www.infojobs.net/...offer URL without tracking query parameters. -
title,description,city: Offer title, plain-text description from the search result, and city name (such as"Madrid","Móstoles"or"Alcobendas"). -
contractType,workday,teleworking: Employer labels for contract ("Contrato indefinido"), schedule ("Jornada completa","Jornada indiferente","Jornada parcial - noche") and work mode (all 10 sample rows from a run on 2026-11-10 say"Presencial"; in a page-2 capture dated 2026-11-08, 21 of 22 offers say"Presencial"and 1 isnull). -
salaryMin,salaryMax,salaryPeriod,salaryCurrency,salaryType: Structured salary bounds (YEARorMONTH,EUR,GROSS) when published by the employer, ornullwhen omitted (in the 10 sample rows from a run on 2026-11-10, 5 useYEAR, 4 useMONTH, and 1 isnull; in a page-2 capture dated 2026-11-08, 15 of 22 offers published a salary). -
publishedAt,companyName,companyUrl,upsellings,executive,searchUrl: ISO publication timestamp, company details, paid promotion flags ("PROMOTED","BUMP_WEEKLY","BUMP"), executive flag and the page-1 search URL.
Pro tips
- Check
salaryPeriodbefore comparing pay: Employers on InfoJobs publish ranges either per month ("MONTH") or per year ("YEAR"). Group or normalize bysalaryPeriodbefore sortingsalaryMinorsalaryMax. - Inspect
descriptionwhen salary fields arenull: One of the 10 sample rows from a run on 2026-11-10 (Camarero/a Tienda de Lujo) hassalaryMin: null, yet itsdescriptiontext statesSALARIO: 643,50€ BRUTO/MES. - Split searches larger than 59,980 offers: InfoJobs serves at most 2,999 pages (20 offers per page, or 59,980 offers) for a single query; from page 3,000 onward the site returns an error object (
hasError: true). The Actor logs a warning on page 1 whentotalElementsexceeds 59,980—split wide searches byprovinceId,categoryId,contractTypeIdorsinceDate. - Keep
fetchDetailsoff unless needed: Each detail page requires a separate sequential request through the unblocking service. LeavefetchDetails: falseunless you need those five fields, and enable it only on smaller, targeted runs. - Filter promoted offers downstream if needed: Promoted offers carry flags like
"PROMOTED"or"BUMP_WEEKLY"inupsellingsand can match a keyword mentioned inside the body text (such as a VTC driver offer welcoming former waiters).
Troubleshooting
- Run succeeded with zero rows: When InfoJobs answers with zero matching offers (
totalElements: 0), the Actor finishesSUCCEEDEDwith an empty dataset and a status message saying nothing matched. Broaden yourkeywordor loosen your filters. - Run failed because a filter was not applied: The Actor compares every sent filter against the
searchobject echoed by InfoJobs. If InfoJobs ignores an unlistedprovinceIdorcategoryId(or answers 404 on page 1), the run stops without retrying and fails when zero rows were stored. - Reading the run log: At the end of a run, the log prints
Skipped N: reasonwhen duplicate IDs across pages were skipped, warns if any detail pages failed, and ends withPushed N row(s).. Read it viaclient.log(run.id).get().
InfoJobs' legal terms restrict automated scraping, so verify that your use complies with their terms and data-protection law.
Next steps
For current pricing and details, see the Store page.
To automate recurring job sweeps, save your input as an Apify task and attach a schedule on a cron expression, or configure a webhook to notify your pipeline when a run succeeds. You can export datasets as JSON, CSV, XML, Excel, HTML table, RSS or JSONL (dataset docs).
What roles or Spanish provinces are you planning to scrape on InfoJobs? Drop a comment! 💬
About the author: reventadata builds and publishes data extraction Actors on the Apify Store.