Indeed API alternative: Indeed jobs by keyword or search URL — title, company, location, salary, posted date and apply link on every row, exact-country results (UK means UK). From $1.50 per 1,000 job listings. A row cap and a deadline stop the run exactly there. Never pay for an undelivered job.
The whole "Maximum cost per run" you set is now spent on jobs. Before this build the run held back about a tenth of your cost cap before it started — a cushion for platform running costs which, on this actor's pricing, your cap does not pay for at all: your cap buys jobs, and the running costs are ours. So the cushion was simply cap you had asked to spend and could not get. On a run capped at $0.10 with jobs at $0.0015 the plan asked for 60 jobs where the cap paid for 66 — 6 jobs of your own cap, unspendable by construction. From this build the plan uses your cap in full, so a run asks for as many jobs as your cap can really pay for. Nothing you are charged changes, and nothing can be charged above the cap you typed: every charge is still checked against what is left of your cap at the moment it is posted, and a run that reaches your cap still stops cleanly, ships what it had already fetched, and tells you what is left. No price, input field or output column changes.
1.0.65 — 2026-09-19
The first line of this page now says what you get and what it costs, instead of what we do not charge for. It opened on "never pay for a job we didn't deliver" and the price sat below the AI-agent block; a buyer arriving from store search read a guarantee before reading that this returns Indeed jobs as rows with no Indeed API key to obtain. The opening line now says that first, with the price — from $1.50 per 1,000 listings — and the row-cap promise kept immediately under it, word for word. This build changes nothing about the run: no input field, output column, charged event or price moved.
1.0.64 — 2026-09-18
The Google Jobs search fee this page mentions is live now, so the page states it plainly instead of announcing it. The job-board shelf section told you that Google Jobs would begin adding a $0.004 search fee on a coming date. That fee took effect, so the sentence is now in the present tense: Google Jobs adds one search fee of $0.004 per search that returns listings, and a search that returns nothing pays nothing. The shelf line naming the three actors that carry a small delivery-conditional fee reads the same way. This build changes nothing about this actor: no input field, output column, charged event or price of its own moved.
1.0.63 — 2026-09-14
The page's first screen now says that a second charge exists before you meet it. Turning on Fetch the full description for every job fetches each job's description as well, charged as a second event per job and only when the description actually arrives. The page said so, but further down — so the first thing a buyer read was the per-listing price on its own, and the extra charge arrived after the decision. It now sits directly under the price, with the fact that the option is off by default (so a run started without it is the per-listing price and nothing else) and a pointer to the Pricing tab, where the per-description price is shown as the Full description charge. Nothing about the run changed: no input field, output column, charged event or price moved.
1.0.62 — 2026-09-14
If your API token cannot open key-value stores, the run now says so — and says which permission to grant. The check that stops you paying twice for a listing you already have lives in a key-value store in your own account, and it is also what stops a second Full description fetch and a second description charge. A run started from the API with a token whose permissions are limited to this Actor's scope cannot open that store, so the check silently did not run and listings your account already had were charged again — while the run page said only "repeat check unavailable" and named no cause and no fix. The run page now names both: the token cannot open key-value stores, repeats may have been charged again, and the fix is key-value store Read, Write and Create (or Actor runs set to Full access) under Settings → API & Integrations, or running from the console. The same sentence is now the first row of your dataset, uncharged, so a script or an AI agent that reads rows and never the run page sees it too, and the run log says it as well. A store that is merely unreadable for any other reason is unchanged and still says what it always said.
The run page no longer loses the tail of a long status line. Counts that are also in OUTPUT and on the rows now give way first when the line would run past the 500 characters the run page shows, and the line says they did, so the invoice, the uncharged items, the binding limit and the contact line always survive.
If your API token can read but not write key-value stores, the run now says so before you are charged twice over. The memory that stops you paying again for a listing you already have has to be written to, not only read. A run started from the API with a token that may read key-value stores but not write them (or may write but not create the store on your first run) opened the memory, read your existing listings correctly, and then quietly recorded nothing — so the run looked perfect and your next run was charged for the whole delivery all over again. On this actor that is twice per listing, because a listing you already have also skips the Full description fetch and its charge. The run page now says it as soon as the first page of results is delivered: the token cannot write key-value stores, the listings delivered now will be charged again next run, and the log says what to grant — key-value store Write (and Create) under Settings → API & Integrations, or Actor runs set to Full access. The same sentence lands once as an uncharged row in your dataset, so a script or an AI agent that never reads the run page sees it too. A run whose writes land is unchanged in every respect, and nothing about what this run charges has moved.
One wording across our actors for the older “cannot open key-value stores” line, so running two of them shows you one sentence and not two. And when a very long status line has to give way, the count of listings you already had now gives way with the other counts — it is still in OUTPUT and on every row — so the cause, the binding limit and the contact line always survive.
Nothing else moved: no input field, output column, event or price changed, and what a run charges is unchanged.
1.0.61 — 2026-09-13
The first paragraph of the page now says what one row holds. The opening said what the run will not charge you for and how the caps work, and left the buyer to scroll for what actually comes back. It now names the columns on every posting — title, company, location, salary, posted date and the apply link — in the first line under the heading. No price, input, output column or charged event changed.
1.0.60 — 2026-09-13
The first line of the page is the listing's own name again. The store title changed on 2026-09-11 and the page's heading still carried the old one, so the card and the page called this actor two different things. No price, no input, no output column and no charged event changed.
The input form's own description now says how to call this actor and what a listing costs. An AI agent shopping for actors never opens this page — it reads the input schema, and ours said little more than what the actor does. It now opens with the call that works ({"queries": ["software engineer"], "countries": ["US"]}), says which field is required and that every other one can be left out, gives the price of one delivered listing with the arithmetic for a 200-listing run, says that asking for full descriptions adds its own charge, lists what is never charged, and names the run option that caps the bill. The search terms field now names what is never charged and where a company's own careers page belongs, before the console sample note. Nothing else moved: no input field, output column, event or price changed.
1.0.59 — 2026-09-12
An aborted or migrated run now leaves its receipt from the first moment of the run, instead of only after start-up finishes. The record that says how a run ended was only put in place once the run had finished starting up — reading your input, opening its stores, working out what a previous attempt had already delivered and charged. A run stopped inside that window ended with nothing written about it at all, which mattered most on a re-started run, where the thing not written about was the earlier attempt's work. It is now in place from the run's first moment. Nothing else moved: no input field, output column, event or price changed, and a run that reaches its work behaves exactly as before.
1.0.58 — 2026-09-12
Asking for more than this actor can do no longer stops the run before it starts. A row cap above 10,000, a run time outside 30–3,600 seconds, a radius above 160 km or "Posted within (days)" above 30 used to be refused by Apify itself: no run, no rows, no explanation — just an error, which is what an API call or an AI agent guessing a round number got. Those ceilings are unchanged and still hard, but they now live in the actor instead of in the form: the run starts, continues at the nearest limit, and writes one extra uncharged row saying what you asked for and what bound it. The radius and the posting window are Indeed's own ceilings, and the note row names them as such. Nothing about a run inside the limits is different, and the note row is never charged.
The links to our other scrapers on this page name them correctly again. Several of those actors were retitled on the store, and this page still used their old names. The links always pointed at the right actors; only the words were out of date.
Nothing else moved: no input field, output column, event or price changed.
1.0.57 — 2026-09-11
The one-link agent pin now sits at the top of this page. An AI agent that reads this listing sees only its opening, and the pin — along with the actor id, the single input field a run needs, and the run option that caps what a run can spend — used to sit thousands of characters below it. All four are now in the first few lines, so an agent can pin this actor and start a capped run without reading further. The links to our other job actors on this page also carry the names those actors actually go by on the store. Nothing about the actor, its input or its output changed.
A maintenance change besides that: nothing about your runs changes. The private run-report this actor writes for our own support — counts only, never anything you typed — now also records the per-event prices the run was charged under, so a question about a bill can be answered from the run's own record rather than from a price table read afterwards. It records the prices that already applied; it does not set them.
Nothing else moved: no price, event or output column changed, and only delivered results are charged.
1.0.56 — 2026-09-10
A maintenance build: nothing about your runs changes. The private run-report this actor writes for our own support — counts only, never anything you typed — gained room for four figures it does not fill in yet: how many targets a run was given, how long it waited on a blocked source, how many of those waits recovered, and a reason code for an item our own size limit refused. The build carries the shared contract so a later one can report them; on this actor every one of them is left blank, and nothing a run does or costs is different.
Nothing else moved: no price, event or output column changed, and only delivered results are charged.
1.0.55 — 2026-09-09
Two runs started at the same time no longer erase each other's memory. The account's repeat memory is now merged on every write, so an item one run delivered stays remembered, and a later re-run hands it back instead of charging it again.
Nothing else moved: no price, event or output column changed, and only delivered results are charged.
1.0.54 — 2026-09-09
A run your own filters emptied no longer ends by asking whether something went wrong. The closing line on the run page asks you to open an issue when a run ends badly. It was deciding that from the row count alone, so a search that ran, listed jobs and had every one of them removed by a filter you set still pointed you at the Issues tab. It now says so only when nothing that happened explains the empty result — your own filter, and listings already in your account, are clean endings.
The same correction applies to the record the run writes back to us, which reads that closing line: a clean run is no longer recorded as a problem run.
A wait for a full job description is no longer taken when the time left cannot hold the retry it buys. When Indeed would not return a description, this actor can pause and try once more. It checked for 120 seconds of run time before pausing — one request's worth — but a description fetch retries once after a pause of its own, so a whole one can take 255 seconds. The pause was therefore being taken to buy an attempt the run clock then cut off, spending the minutes the run needed to report. It now waits only when a whole retry really fits.
Nothing else moved: no price, event or output column changed, and only delivered results are charged.
1.0.53 — 2026-09-06
A block of links pasted into one row is now read as the list you meant, whatever separates them. "Search terms" takes one search per row, and a pasted Indeed search link is a search of its own. A block of search links pasted into a single row was read as ONE search whose keyword was the other links, which could only come back empty. A pasted block is now split back into its individual links whether they are separated by spaces, line breaks, tabs, commas, semicolons, pipes or nothing at all, so a column copied straight out of a spreadsheet works. Each one is then read, deduplicated and charged on its own, exactly as if you had pasted them one per row.
A link that carries another link inside it is still one link. An address holding a second address in its query or its path is left whole, not broken in two, and a single link pasted on its own is never rewritten.
Text pasted around a link no longer breaks it. A number, a bullet or a note sitting beside a link is ignored and the link itself is used. A row holding no link at all is still answered as one row, not one row per word.
Nothing else moved: no price, event, output column or charge changed, and only delivered results are charged.
1.0.52 — 2026-09-06
The run's own record now counts the searches your limits left unrun. When your row cap filled, your maximum cost per run was reached, or the run's clock ended the walk, the searches still ahead of it were simply absent from the report this run writes back to us: a run of two searches that delivered from one closed its books with the other named by nothing at all. Each is now counted under the limit that stopped it, so a run that stops short of your searches is visible to us without you having to report it.
A run that stopped at your row cap now also records whether that cap really filled. It is one true-or-false fact about the run, never the number you typed, and it lets us see a run that claimed your cap stopped it while it had in fact delivered fewer rows than you allowed.
Nothing you can see changed. Your rows, your columns, your receipt row, the searches it lists as not reached and your bill are exactly as they were — only delivered rows are charged.
1.0.51 — 2026-09-06
Listings on a results page already fetched when the run's time limit lands are now delivered, never dropped. Fetching an Indeed results page is the expensive step; writing its listings out costs nothing more. When the clock ran out on that fetch, the run used to stop part-way down the page and hand you a short list from a page it had read in full. Every listing on a page this run fetched is now delivered, charged once each exactly as before.
Full descriptions are unchanged, and still bounded. A description is its own fetch, so the time limit still ends those: a listing whose description the run had no time to fetch ships with the same honest note naming the run timeout, and no description charge applies.
What a time limit means has not changed. It still ends the collecting — no further page and no further search is fetched after it — and the summary still names maxRunSeconds and lists the searches that were not reached. Your row cap and your maximum cost per run still cut the delivery exactly where you set them, and the run keeps back enough time to write the receipt.
1.0.50 — 2026-09-05
A full description Indeed never answers is now waited out inside your own time budget, instead of being handed straight back for a re-run. With "Fetch the full description for every job" turned on, each listing's description is fetched on its own. Until now a request that came back with no answer at all — a dropped connection, a timeout, a momentary outage — ended there: the job row shipped with an honest note saying re-running may deliver the description, while most of the run's time went unused on a problem that usually clears in minutes. Now, while the run still holds real time, it waits a minute and a half to three and asks once more. A description that arrives after the wait is delivered exactly as before and charged exactly once; one that still does not arrive ships the same honest note and is not charged.
What never waits: an answer. If Indeed answers the request with a sign-in page, or with a different job's panel, that is an answer — asking the same question again returns the same one, so the run does not ask twice. Neither does a run that has reached its own description allowance, its time limit or its row cap: those are your settings doing what you set them to.
The waiting is bounded across the whole run, not per listing. At most one wait per listing, at most a few minutes of waiting in total, never more than half the time the run had left when the first request went unanswered, and always keeping room to finish and report. A short run makes one attempt exactly as before. The run's summary row now says how many descriptions went unanswered, how long the run waited and how many arrived after it; the run log says the same in one line.
Charges are unchanged: waiting is not charged, and a description that is not delivered is not charged.
1.0.49 — 2026-09-05
A search that turns up nothing, and a listing your own settings remove, are now counted against what the run asked for. Every run of this actor keeps its own arithmetic of what was asked and what came back. That arithmetic used the row limit as the ask, so a run with the default limit of 100 that found 15 listings recorded itself as owing eighty-five rows nobody had ever asked for — and a listing your "Remote only" setting removed, or one already in your account from an earlier run, was recorded as nothing at all. The run now asks for exactly what its searches listed, and names every listing it did not deliver. Nothing about your rows or your bill changed: the same listings are delivered, the same ones are charged, and a search that found nothing is still an uncharged row that says so.
The receipt row now says how many searches you asked for, not how many survived. If a country or a sort order you typed could not be read, the searches for it never ran — and the receipt counted only the ones that did, so a run where every search was stopped said "across 0 searches". It now says how many you asked for, and the rows above it still name each search that was stopped and which setting stopped it.
A job row whose description could not be fetched is no longer counted twice. It was always a delivered, charged job row carrying an honest note about its description, and it still is; only our own bookkeeping changed.
1.0.48 — 2026-09-05
A run started with no input at all now has room to wait out a block. Press Start on the untouched form and this actor runs a small real sample — "software engineer" on Indeed US, up to 5 listings, charged like any run. Indeed sometimes turns every connection away for a few minutes at a time, and one full round of attempts can take three minutes on its own — longer than the sample's own three-minute limit. So the pause-and-try-again this actor gained in the last build could never happen on the most common first run of all: it reported "blocked, please re-run" while the block was still lifting. The sample's time limit is now ten and a half minutes, sized to hold one full round of attempts, one pause, one more full round on fresh network routes, and enough time left over to stop cleanly and report. A sample Indeed answers still finishes in well under a minute; only a blocked one uses the extra time, and nothing is charged unless listings are delivered. This replaces the line in 1.0.47 that said the no-input sample keeps a single pass.
How much of its remaining time one blocked search may spend waiting now follows the size of what you asked for. A run with other searches still to make keeps exactly the limits it had — no single search may spend more than half the time left on waiting, and a fresh round has to fit inside a third of what is left after the longest pause — so a wide block still leaves the later searches their share of the run. A run with a single search to make — the no-input sample, or one job title in one country — may spend what is left on its one pause and one more round, because nothing else is waiting for that time. Every other bound is unchanged: at most three rounds per search, never into the time kept back for reporting, and never when the run could not afford another round.
A no-input sample is now always a real, fresh search. It no longer checks, or adds to, the record of listings this account has already been given. That record is what stops you being charged twice for the same listing on a real search, and it works exactly as before on any run where you set what to look for. On the no-input sample it was making the shop window hand back listings from an earlier press of Start instead of fetching new ones, so pressing Start twice showed rows that cost nothing but also proved nothing. Every no-input sample now fetches live and charges for the few listings it delivers.
1.0.47 — 2026-09-05
A search Indeed walls is now waited out inside your own time budget, instead of being handed straight back for a re-run. Until now a search refused on every route ended after one pass — two to three minutes of trying — and shipped its uncharged "please re-run" row while most of a long run's time went unused, on a wall that usually lifts within minutes. Now, while the run still holds real time, it waits a minute and a half to three, starts over on fresh network routes and walks the whole search again — up to three passes per search, with the waiting capped at a few minutes in total and at half of the time the run had left, and always keeping room to finish and report. A search that delivers after a wait delivers exactly as before, charged once per listing. A search still refused after the waits ships the same honest uncharged row, which now says what was tried: how long the run waited, how many passes and connections it made, and how many minutes were left. The run's summary row carries the same facts, and the run log says what the waits bought.
What never waits: a real "no jobs matched" answer, a "not found" answer, and the run's own data or cost limits — those are answers, and a wait cannot change them. A short run makes one pass exactly as before, because a wait and a fresh walk have to fit inside a third of the time left: the default 15-minute run has room for one wait and one fresh walk on a walled search, the 3-minute no-input sample and a 60-second run keep their single pass.
Charges are unchanged: waiting is uncharged, a refused search is uncharged, and only delivered listings are charged.
1.0.46 — 2026-09-05
A search that is blocked behind a rendered page now gets a second look before it gives up. When the ordinary search rungs are walled, this actor falls back to a rendered fetch of the search page — and that page can itself come back as a sign-in wall. A single walled rendered fetch used to end the search with an uncharged "please re-run" row, while the search budget's own second attempt went unspent. It now takes one more look on a fresh network route, so a search that was reachable one route later delivers its listings instead of a re-run request. Nothing changes on a search the ordinary rungs answer, and a search walled on both looks still ships the same honest, uncharged row.
1.0.45 — 2026-09-04
The run log no longer opens by saying nothing was set on a run where something was. On a filter-only sample run the first line of your run log still read "Nothing was set — running the default sample"; it now says your settings were kept, names them, and says a limit larger than the sample's own is capped at it. The dataset rows and the status line already said this; the log line was the last one that did not.
1.0.44 — 2026-09-04
Narrowing one setting no longer costs you the sample run. Run this actor with no input at all and it searches "software engineer" on Indeed US — 5 listings, charged like any run — so you see real rows. Set one thing first, though — a country, a sort order, "Remote jobs only", a row cap — and leave Search terms empty, and you used to get a single row asking for search terms instead of any output at all: the caller who set nothing was served better than the caller who narrowed a field. Now your settings are kept and the same sample runs under them, charged like any run. Everything you did not set takes the sample's own value, and one extra uncharged row names which settings were yours and how to run your own search.
A limit you set can only shrink that sample, never grow it. "Max job listings for the whole run" and "Max run seconds" are ceilings: ask for at most 10,000 listings and you get the sample's own 5; ask for at most 2 and you get 2.
The paid description leg stays off unless you turn it on. "Fetch the full description for every job" is the one setting that adds a charge, so the sample leaves it off — turn it on with no search terms and it applies to the sample's five listings and no more, and the note row names it back to you.
Two things still answer with guidance instead of rows, unchanged: a field name this actor does not recognise, and a search-term list you set and left empty. A country with no Indeed site, or a sort order this actor does not offer, is still refused before anything is fetched.
No row on such a run opens by saying nothing was set — it says which half was missing.
Every run now reports what you asked for in the same unit it reports what it delivered. The run's own record counted your ask in searches while it counted the result in listings, so a run that returned 73 listings from one search recorded an ask of 1 — an ask smaller than its own delivery. Your ask is now your row cap. The summary row still counts searches, and says "searches".
1.0.43 — 2026-09-04
A country or sort order this actor cannot run no longer swallows your whole search list. Send five search terms with one country Indeed has no site for and you used to get a dataset holding a single guidance row. Every term you sent now comes back with its own uncharged row saying it was not run and which input field stopped it, so nothing you asked for goes unaccounted for.
A refused country now says which of three things went wrong. A country that really exists but has no Indeed site says so and names the nearest country sites this actor does cover — it is not a spelling mistake, so you are not sent to fix one. A region or a whole-world value (Europe, worldwide) says Indeed is searched one country site at a time and points you at single countries. A value that names no country at all says there is no honest way to guess and lists what is accepted.
Every input the run refuses now names the input field it is about — queries, countries, sortBy or resumeFromDatasetId — on the row itself and in the run's own record, so a run that delivered nothing can say which field to fix rather than "some input was wrong".
The run's record now reports what you asked for, not what survived. A run whose every search was refused used to report the number of refusals as the ask; it now reports the searches you asked for, counted before anything was refused.
Wording: past entries that described a row or an attempt as "free" now use the register the rest of this changelog uses — every run spends platform time you pay for, so only "uncharged" is true. No facts, numbers or build headings changed.
1.0.42 — 2026-09-04
A listing already delivered to your account is never charged a second time. Every run now remembers what it delivered for your account, in a key-value store called indeed-watch-account in your own Apify account. Re-run the same searches and those listings are skipped before they take a row slot — no row, no charge, no bite out of your row cap — and the status line says how many. It also means no second Full description fetch and no second description charge for a listing you already own the text of. Delete that store to forget everything; entries older than 90 days count as new again.
New option: "Include listings you already have" (includeSeen). Off by default. Turn it on and rows your account already has come back in every run's dataset anyway, marked repeat: true with firstSeenAt and firstSeenRunId naming the run that first delivered them — and still not charged. A handed-back row carries no full description: that text came with the earlier delivery and is not bought again, and the row says so.
A memory that cannot be read never stops a run. The run delivers and charges as usual and the status line says the repeat check was unavailable, so you know a repeat may have been charged that once.
A mistyped "Continue from an earlier run" ID no longer creates an empty dataset in your account. The ID is now checked and looked up read-only before anything else happens, so a typo, a pasted link or pasted rows come straight back as one honest uncharged row naming what the value looked like — instead of quietly making a storage under that name and leaving you to find it. A run stopped that way now says so in one short sentence on the run page and keeps the full explanation on its row, where nothing is cut.
A search whose every listing you already have is reported as a real search that ran, not as an empty one, and no longer asks you to open an issue.
1.0.41 — 2026-09-04
"Sort by" reads the value you actually sent.Date posted — the console's own name for the option — plus newest, most recent, latest and date_posted used to count as unrecognised values, and an unrecognised sort stops every keyword search in the run: one guidance row, and nothing for any term you typed. They all land on the order they name now. A value that names no order is still refused rather than answered in a different order.
A country written the ordinary way now works in "Countries".USA, United States, uk, United Kingdom, Germany and Canada all resolve to the Indeed country site they name. A country Indeed runs no site for is still skipped by name and never answered with a neighbour's listings.
1.0.40 — 2026-09-04
A run that stops before it starts now reports its outcome too — a run refused for its memory setting now reaches us the same way every other run does: counts and reason codes only, never your input or your rows.
1.0.39 — 2026-09-04
Every run now reports its own outcome to us — counts and reason codes only, never your input or your rows — so a run that goes wrong reaches us even when nobody shares it.
Vendor credit caps now carry an absolute per-run ceiling on top of the ask-derived figure.
Stop guidance names the run-wide row cap first.
1.0.38 — 2026-09-04
Run status lines fit the run page again; sample-run wording shortened.
1.0.37 — 2026-09-04
Full descriptions left behind by the row cap you set no longer count as something that went wrong. A description we did try and could not fetch still does.
1.0.36 — 2026-09-04
A run that filled the row cap you set is no longer treated as a run that went wrong. The Issues-tab line now appears only when something actually did.
1.0.35 — 2026-09-04
A run that ends with a problem now says where to reach us. A miss, an input error, an early stop or a failed run closes by pointing at the Issues tab and naming the reply time; a fully delivered run is left alone.
One support promise across this page — issues are answered in a couple of hours, always within a day.
1.0.34 — the screenshots load from Apify's own storage
The two console screenshots on this page — the input form and the dataset table a real run delivers — are now served from Apify's own storage instead of an outside host, so every picture and every link on this page stays on Apify. Same pictures, same page. What the actor does and what it charges are unchanged.
1.0.33 — the suite blocks name the sibling fees that start 16 September 2026
The suite blocks at the bottom of this page now state the three delivery-conditional fees that start 16 September 2026 on sibling actors — keyword volume's fresh lookup, profile posts' profile lookup, and Google Jobs' search fee. This page also now says "nothing charged for starting a run" wherever it used to say "no start fee" — the same promise, in the words the whole shelf uses. Nothing changes on this actor: same price, same events, same rows.
1.0.32 — real screenshots of the input form and the delivered rows
Two pictures from the Apify console are now on this page: the input form you fill in, and the dataset table a real run delivers. The suite links at the bottom were re-checked against the live store listings, and one FAQ answer that still described the retired placeholder sample was corrected to match the rest of the page. What the actor does and what it charges are unchanged.
1.0.31 — the default sample delivers even when Indeed blocks the first attempt
Follow-up to 1.0.30: when Indeed blocked the sample's plain search, the run ended with one uncharged row explaining it and no listings. The sample now takes the same fallback route a real search takes (with a small fixed allowance on our side), so a bare Start ends with listings whenever Indeed can be read at all. Nothing else changed.
1.0.30 — run it with no input and get a real sample, not placeholder rows
Running the actor with an empty input ({} from the API, an AI agent, or a saved task that was never filled in) used to return uncharged placeholder rows and no listings. It now runs a small real search — "software engineer" on Indeed US, up to 5 listings, inside three minutes — charged like any run, so the first thing you see is actual output. The status line and the summary row say it was the sample and how to run your own search. If Indeed does not answer at that moment, you still get one uncharged row explaining it, never an empty result. Real searches are unchanged.
1.0.29 — run it from an AI agent, and where feedback goes
Two additions to this page, nothing else: a one-line setup for running this actor from an AI agent over MCP (pin it by URL, no store search needed), and a note on where issues and feedback go. What the actor does and what it charges are unchanged.
1.0.28 — continue an earlier run without paying twice
A run that stopped at a cap can now be continued, and nothing you already have is charged
again. Until this build the summary row's advice after a cap was to raise the cap and re-run —
and the re-run, starting from an empty dataset, fetched and charged the whole head of the list a
second time. There is now a "Continue from an earlier run" input (resumeFromDatasetId): give it
the earlier run's dataset ID and every listing already in that dataset is skipped — never fetched,
never charged — so a higher cap collects only what is new. The summary row now carries the run's
own datasetId (what you paste next time) and, on a continued run, carriedOver: how many rows
were skipped because you already had them; the search rows say the same per search.
The advice is honest again. A capped run's summary now tells you exactly how to collect the
rest without paying for it twice, instead of telling you to re-run.
A dataset that cannot be read stops the run before it fetches anything, uncharged. Continuing
blind could have billed you twice, so the run refuses, says why on an uncharged row, and asks you
to check the ID.
1.0.27 — a Sort-by value we do not offer stops the search instead of quietly re-ordering it
An unrecognised Sort-by value is now refused, not run on relevance. Until this build, asking
for a sort order that is not relevance or date ran your search in relevance order and said so
on an uncharged row. Saying it does not make it right: listings ordered by relevance look exactly
like listings ordered by date, so you could not see that you had been answered a different
question — and you were charged for the answer. Those searches are no longer run. You get one
uncharged row naming the two values that work, nothing is fetched for them and nothing is charged.
Nothing else about Sort-by changes.relevance and date behave exactly as before, and
leaving the field out — or sending null — still means relevance. A pasted Indeed search URL that
carries its own &sort= never took its order from this field, so it is unaffected and still runs
in the same input.
1.0.26 — a bracketed note after the state no longer costs you the country
"Austin, TX (Hybrid)" now reads as Austin, TX, US. The shared location parser this
actor uses picked up one more rule: a note in brackets at the end of a location line is an
office note, not part of the place, so the state and country underneath it are read
normally instead of the whole tail being treated as one unknown fragment. Indeed's own
lines rarely carry these, so most rows are unaffected — this keeps every actor in the
family reading locations identically.
1.0.25 — the Sort-by dropdown accepts null too, so a template can null every field
Every optional field now accepts null — the Sort-by dropdown was the last one that did
not. A dropdown with a fixed list of choices is checked separately by the platform, and
that check refuses null however the field is declared, so a caller that fills a template
and sends null for every unset field still had one field that failed the whole run before
it started. Sort-by is still the same dropdown with the same two choices; the list is now
checked by the actor instead of by the platform.
A Sort-by value we do not recognise is reported, never silently swapped. If you ask for
a sort order that is not "relevance" or "date", the run uses relevance — as it always did —
but now also returns one uncharged row naming what was not recognised and what it used. A
quietly swapped sort order returns listings that look exactly like the ones you asked for,
which is the kind of wrong you cannot see.
1.0.24 — the country on a listing is a country, and an optional field can be sent as null
location.country is now a real two-letter country code, or empty — never a state and
never a ZIP. Indeed prints one location line per listing, and until now we split it on
its commas and called the last piece the country: a listing in "Kemmerer, WY 83101" arrived
with a country of "WY 83101", and "Los Angeles, CA" arrived as "CA" — which is Canada's
country code, not California's, and a filter on it could not tell. Both now read country
"US", with the state in location.region and the ZIP in the new location.postcode. UK
listings read the same way: "London E14 5JP" is city London, postcode E14 5JP, country
"GB". When a line names no country we can prove, the field is empty rather than a guess,
and Indeed's untouched line is always in location.raw.
Optional fields now accept null. Templated callers — n8n, agent frameworks, anything
that fills a request from a form — send null for every field left unset, and the platform
used to refuse those runs before they started. Every optional field except the Sort-by
dropdown now accepts null and reads it as "use the default" (the dropdown follows in
1.0.25). queries is still required.
1.0.23 — a search Indeed blocks now keeps trying, and usually gets through
A search that hits Indeed's access check no longer gives up in seconds. It used to try
three times in a row, from the same kind of connection, inside about ten seconds — three
attempts that amounted to one look. A blocked search now waits properly between attempts,
moves to a different connection and then to a different device profile, and only then
reports back. Nothing about this is charged: it either delivers listings or it delivers the
same uncharged, re-runnable row it always did.
And if Indeed still will not answer, we fetch the same search a different way. As a last
step, and only after every other attempt has failed, the search page is fetched through a
second route and the listings on it are delivered normally. In our own testing this is the
difference between a run that returns nothing and a run that returns a full page of jobs.
You pay for the listings it delivers, at the same price as any other listing, and for
nothing else.
A temporary Indeed server error gets another look too, instead of waiting out its
cooldown and then walking away from the search.
A search that ends blocked now reports what actually happened — a server error is
reported as a server error, not as an access check.
The full-description fetch retries a dropped connection, and never re-fetches a page
that already answered. A dropped connection is not an answer, so it buys one more try; a
page that answered is never bought twice.
null now officially means "use the default". That has been true since 1.0.21, but the
API paste-block still told you not to send it. Corrected.
The full description's length is now stated as the measured range (2,900–7,000
characters) rather than the top of it, and location.country is documented for what it is:
the last piece of Indeed's own location line — "WY 83101", or a "CA" that means
California — never a country code. The country you searched is the row's own country.
1.0.22 — suite links now point at the full live shelf
Every actor named in this README is now a live store link. All 25 steadyfetch
actors are published, so the suite tables link straight to each store page. Nothing
about what this actor delivers or charges changed.
1.0.21 — a search cut off mid-page is never lost
A search stopped part-way by maxItems or the run clock now reports itself and stays
resumable. When a limit landed in the middle of a page, that search could vanish: no row
of its own, and missing from the summary's resume cursor too. It now ships its own row —
uncharged, and saying plainly that a run limit stopped it — and the resume cursor picks it
up, so the next run carries on exactly where this one left off.
When a run is interrupted and later resumed, each part now reports its own share. An
interrupted search previously froze the first attempt's partial figures as the search's
only record, and the jobs delivered after the resume had no search row at all. Each
attempt's rows now stand side by side, so the dataset accounts for everything delivered.
Finishing an interrupted run's billing can never risk a double charge. If settling an
earlier attempt's delivered-but-unbilled rows is itself interrupted, the run now stops at
the exact point of failure and records only what actually went through — a charge that may
already have landed is never sent again, and the rest is finished by the next resume.
Every kind of account-side refusal from our description provider now stops the fetches
straight away. 1.0.20 stopped asking when our key was refused; one further refusal shape
could still be retried once per job. All such refusals now stand down immediately — never
charged to you either way.
A numeric setting sent as null or an empty value now means "use the default" instead
of being read as the smallest allowed value, so templated API inputs that send null for
an untouched knob behave exactly like leaving the field out.
1.0.20 — a run that runs out of time now says so on every job row
A job whose full description the run ran out of time to fetch now says so on its own
row. Turn on "Fetch the full description" and hit "Max run seconds" mid-batch, and those
rows used to arrive with no description, no explanation and no charge — indistinguishable
from a job that has none. Each now carries a plain sentence naming the time limit and what
to change. No Full description charge applies to any of them, exactly as before.
If our description provider ever refuses our key, the run stops asking straight away
instead of trying once per job and telling you each time that re-running might help. That
was our problem to fix, never a temporary one on your side, and you were never charged for
it either way.
A run that is moved to another server keeps the same description pricing it started
under, rather than switching part-way through its own life.
1.0.18 — full descriptions are available now
"Fetch the full description for every job" is live. Turn on includeDescription and
every remaining listing's complete description is fetched from Indeed's own job panel and
delivered on its row, at the current price shown on the Pricing tab.
Every honesty rule the option shipped with holds exactly as documented: a description is
charged only when it is actually delivered, every job row carries chargedDescription
so the invoice reconciles from the dataset, a description that cannot be fetched is
reported on its row uncharged, and each description is verified to belong to the exact job
on the row before it is delivered.
Nothing changes for runs that leave the option off.
1.0.17 — a dropped connection no longer ends a search with "not found"
A dropped or refused connection no longer ends a search with "Indeed answered 'not found' — check the country and search term". Indeed had answered nothing. The search now retries on a fresh connection, and if it still cannot get through you get an uncharged row saying the problem was temporary and asking you to re-run.
Searches that used to stop on the first network hiccup now recover on their own, so a good query no longer comes back empty because of one bad moment on the way out.
Unchanged: when Indeed itself answers "not found" for a page, that is still reported as a real answer, and a search that genuinely matches no jobs is still a clean, uncharged result.
1.0.16 — a typo in an input field name now gets a helpful pointer instead of sample rows
A misspelled input field is named back to you instead of quietly running the sample. If you
send your search terms under a field this actor does not have — query instead of queries, say —
the run used to look identical to an empty run and answered with the uncharged sample rows, so a
typo looked like it had worked. It now returns one uncharged row that names the field it did not
recognise, names the field your search terms belong in, and shows the exact shape to send. Sample
rows are reserved for a genuinely untouched input, and nothing is charged either way.
1.0.15 — large runs finish faster: same rows, same prices
Results are now delivered a whole search page at a time instead of one row at a time,
and each page is billed in one step after it lands. Runs with many searches spend less
of their time (and their timeout) on delivery overhead and more on the searches themselves.
Nothing about the rows or the prices changes: the same listings arrive in the same order,
a delivered listing is still charged only after it is in your dataset, and misses,
off-country listings and per-search summaries still arrive uncharged alongside it.
Every interruption guarantee holds: a run moved to another server or resurrected from the
Runs tab still continues from what is already delivered, never delivers a row twice, and
never charges twice — including for a page delivered in the instant before an interruption.
1.0.14 — the full-description option returns, rebuilt on a route that works
"Fetch the full description for every job" is back (includeDescription, off by
default). The old option relied on each job's own page, which Indeed refuses to serve;
the rebuilt option reads the full description from Indeed's job panel, which does answer.
Delivered descriptions are marked descriptionSource: "job-panel".
Charged only when a description is delivered. Every job row now carries
chargedDescription, so the invoice reconciles from the dataset. A description that
cannot be fetched is reported on its row, uncharged, and the listing itself still ships.
Verified before charged. Each description is checked against the exact job on the row
before it is delivered — a blocked or mismatched answer becomes an uncharged miss, never
the wrong job's text.
If the option is not yet active on this listing, a run that asks for it is told once in
its log and charged nothing extra; everything else behaves exactly as before.
The option can only switch on at its current price. If a run's price list still
carries an out-of-date figure for the description fee, the option stays off for that
run and charges nothing — it activates only once the current price is in place, so no
run is ever billed at a stale number.
A description skipped by the run's cost allowance says so on its own row — and the
listings keep coming. When your maximum cost per run leaves no room to fetch a job's
description, that row carries the same "allowance reached" note as every other unfetched
description, uncharged, and the run's miss count includes it. The run keeps delivering the
job listings your cost allowance still covers, without their descriptions, instead of
stopping at the first one it could not afford.
Description fetching respects the run's time limit. A run out of time stops fetching
further descriptions instead of overrunning maxRunSeconds or the run timeout; the job
listings still ship, and every unfetched description stays uncharged.
A run interrupted by a server move no longer records a partial search summary. The
per-search summary row is written once, with the true figures, by the run that finishes
the search — an interrupted attempt can no longer freeze "delivered 0" into your dataset.
The search-page description included with every listing, and every snippet, are untouched.
1.0.13 — the full-description option is withdrawn
Removed the "Fetch the full description for every job" option. Indeed blocks the job
page it relied on, so it never delivered a description — and every blocked attempt slowed
the search itself down. The option is gone rather than left on the form returning nothing.
The option was never charged. A description was only ever billable when one actually
came back, and none ever did.
Nothing else about descriptions changes. Every job row still carries Indeed's own search
summary, and the one listing Indeed pre-opens on each search still ships its complete
description at no extra charge, marked descriptionSource: "search-page".
A saved task that still sends the old setting keeps running exactly as before — the setting
is ignored, not an error.
1.0.12 — French and Swiss salaries are read the way those pages write them
A French salary is no longer inflated 100×. France writes decimals with a comma, so
11,88 € an hour was being read as 1188. France now joins the countries whose comma is a
decimal point, and the number on the row is the number on the page.
Switzerland's apostrophe no longer splits a salary in two.CHF 50'000 (either
apostrophe character) is one number, not 50 and 000.
par heure and par an are recognised, so French listings get hour and year in
salaryPeriod instead of nothing.
A run that stopped at your row cap or deadline no longer tells you to re-run with
resumeCursor. It is a field on the summary row, not something you can pass in, so the
summary now says what actually works: narrow the search or raise the caps and run again.
1.0.11 — a run that is interrupted never charges you twice
If Apify moves your run to another server part-way through — or you resurrect a finished
run — the run now continues from what is already in your dataset. Jobs it already
delivered are not fetched again, not delivered again, and not charged again, and any
charge that was interrupted mid-way is completed rather than repeated.
maxItems and maxRunSeconds are limits on the run, not on one attempt: an
interrupted run can no longer deliver up to twice your row cap, or run for twice the time
you allowed.
If the run cannot read its own delivery record, it delivers results and charges nothing
rather than risk billing you twice, and the run summary says so.
Clearer counts in run messages ("1 job" / "2 jobs", "1 search" / "2 searches").
1.0.10 — pre-publish sweep: charged now reports what actually happened
The charging regime is read from the run's own price list, so an actor that is not set up
to bill — or is set up to bill an event that does not exist — no longer pushes a row
claiming it was charged, and no longer calls the charge API. charged and
chargedDescription on every row now report what happened rather than what was intended.
If the actor is ever mis-priced so that pushing a row would itself bill you, the run stops
before pushing anything and says so, instead of billing for the uncharged miss rows this
actor is built around.
README leads with the output row, states the price up front, names the two misses you are
most likely to see, and carries the full steadyfetch shelf.
1.0.9 — initial release
Search Indeed by keyword or by pasting a search URL, and pay only for the job listings
that are delivered.
One row per delivered listing with title, company, location, remote flag, employment
type, salary as Indeed printed it, posted date, Indeed's own search summary, a canonical
job link and an apply link — plus one row per search and one summary row per run.
Search several terms across several Indeed country sites in one run. Indeed returns about
15 listings per search and its results pages cannot be walked further, so the actor says
so in every run summary instead of quietly under-delivering against a large row cap.
Country comes from Indeed's own country site, and every listing's country is checked
against the one you asked for. Anything outside it is reported uncharged, never sold as a match.
Full descriptions are optional and off by default: the search page carries a short summary
for every listing and the complete description for only one of them. When switched on, a
description is charged only when one actually comes back.
Nothing is charged for an access check, a rate limit, an unreadable page, a search that
matched nothing, a listing outside your country, an input we could not read, or a run that
stopped at one of your limits.
maxItems and maxRunSeconds are hard limits: the run stops cleanly, tells you which
limit bound, and hands back a resume cursor.
Every row carries charged and missReason so the invoice reconciles from the dataset.
Running with no input returns uncharged sample rows showing the exact output shape.