<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Devil Scrapes</title>
    <description>The latest articles on DEV Community by Devil Scrapes (@devil_scrapes).</description>
    <link>https://dev.to/devil_scrapes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960872%2Ffa930ad0-5ebc-4ca7-b894-6bc6fb3e2b40.png</url>
      <title>DEV Community: Devil Scrapes</title>
      <link>https://dev.to/devil_scrapes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devil_scrapes"/>
    <language>en</language>
    <item>
      <title>UTC Offsets Are Not Integers — A Remote Job Feed With 5.75 In It</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Thu, 10 Sep 2026 04:03:12 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/utc-offsets-are-not-integers-a-remote-job-feed-with-575-in-it-5537</link>
      <guid>https://dev.to/devil_scrapes/utc-offsets-are-not-integers-a-remote-job-feed-with-575-in-it-5537</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;UTC offsets are not integers. If you model a remote job's timezone requirement as &lt;code&gt;list[int]&lt;/code&gt;, you will crash or silently truncate on every role that accepts candidates in India (+5:30), Newfoundland (−3:30), Nepal (+5:45), Adelaide (+9:30) or the Marquesas (−9:30). The Himalayas remote-jobs API returns these as JSON numbers in a &lt;code&gt;timezoneRestrictions&lt;/code&gt; array, and a live pull right now shows &lt;strong&gt;37 distinct offsets, six of them fractional&lt;/strong&gt;: &lt;code&gt;-9.5, -3.5, 3.5, 4.5, 5.5, 5.75&lt;/code&gt;. The correct type is a float — and &lt;code&gt;5.75&lt;/code&gt; is the one that catches people who "fixed" it by switching to half-hour steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a remote job board even have fractional timezones? 🕐
&lt;/h2&gt;

&lt;p&gt;Because remote hiring is expressed as an &lt;em&gt;overlap window&lt;/em&gt;, not a location. A job that says "must overlap 4 hours with UTC+5:30" is describing India, and India has never been on a whole-hour offset. Nepal is stranger still at UTC+5:45, which is why &lt;code&gt;5.75&lt;/code&gt; shows up in real data.&lt;/p&gt;

&lt;p&gt;Here is what the field actually looks like on a live pull:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;distinct offsets in one page : 37
fractional offsets present   : -9.5, -3.5, 3.5, 4.5, 5.5, 5.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of those are not half-hours away from anything convenient. &lt;code&gt;5.75&lt;/code&gt; cannot be represented as an integer, cannot be represented as "hours plus 30 minutes", and will not round-trip through a naive &lt;code&gt;int()&lt;/code&gt; cast — Python will happily give you &lt;code&gt;5&lt;/code&gt;, which is Pakistan, not Nepal. That is a silent 45-minute error in a field whose entire purpose is scheduling overlap.&lt;/p&gt;

&lt;p&gt;So the row contract we ship is explicit about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;timezone_restrictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# NOT list[int]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single annotation is the whole lesson. We shipped this Actor with &lt;code&gt;list[int]&lt;/code&gt; in the first draft of the spec and the live data corrected us before any customer saw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this break in practice?
&lt;/h2&gt;

&lt;p&gt;Three things, in rising order of how long they take to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A hard crash&lt;/strong&gt;, if your parser is strict. &lt;code&gt;int("5.75")&lt;/code&gt; raises; a Pydantic &lt;code&gt;list[int]&lt;/code&gt; rejects the row. This is the good outcome — it fails loudly on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A silent truncation&lt;/strong&gt;, if your parser is permissive. You store &lt;code&gt;5&lt;/code&gt; for a job that means &lt;code&gt;5.75&lt;/code&gt;, and every downstream "can this candidate overlap?" calculation is wrong by 45 minutes for that row only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dropped row&lt;/strong&gt;, if your fault handling is coarse. This is the expensive one: a single un-parseable record takes down the whole batch, and the 249 good jobs on that page go with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third failure mode is the one worth designing against, and it is not specific to timezones. Any job feed will eventually hand you one record shaped differently from the other 249 — a missing salary, a null company slug, an employment type nobody has seen before. If one bad record can end a run, then the reliability of your scrape is set by the &lt;em&gt;worst&lt;/em&gt; row in the feed rather than the typical one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we handle it 🛡️
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/himalayas-remote-jobs-scraper" rel="noopener noreferrer"&gt;Himalayas Remote Jobs Scraper&lt;/a&gt; isolates faults per item. A record that fails to parse is skipped and logged; the run continues and delivers everything else. You are never in the position of paying for a run that returned nothing because one job posting had an unusual field.&lt;/p&gt;

&lt;p&gt;The other thing worth knowing about this API is that it paginates by &lt;strong&gt;cursor&lt;/strong&gt;, not by page number. The response body says so itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cursor pagination is now available and is the preferred way to page through the feed. Pass the &lt;code&gt;nextCursor&lt;/code&gt; value from each response back as &lt;code&gt;?cursor=&lt;/code&gt;. It is faster than offset.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cursor chains are also the safer choice on a feed that changes under you — new jobs are posted while you are walking it, and offset pagination on a shifting list is how you get duplicate and skipped rows. On a per-row-billed scrape, duplicates are not a cosmetic problem: they are rows a customer pays for twice.&lt;/p&gt;

&lt;p&gt;We also do not trust a job feed's own filter parameters without checking them. Some of the documented filters on this API do not narrow the result set the way you would expect, so the Actor fetches and filters client-side. That is slightly more work per run and it means the filter you asked for is the filter you get, rather than the filter the upstream felt like applying today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output
&lt;/h2&gt;

&lt;p&gt;One row per job posting, as JSON, CSV or Excel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id, title, company_name, company_slug, company_logo_url, url,
excerpt, description_html, employment_type, seniority[],
categories[], parent_categories[], location_restrictions[],
timezone_restrictions[]  ← float, and now you know why
min_salary, max_salary, salary_period, currency,
published_at, expires_at, scraped_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pricing is $0.20 to start a run plus $0.0015 per job row — $1.70 for a thousand postings. Zero rows written means you pay only the start fee.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We publish the wire-format quirks we hit, because the failure that costs you a night is rarely the one in the docs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>api</category>
      <category>apify</category>
    </item>
    <item>
      <title>Airbnb's search payload doesn't carry amenities. Here's what we do instead of guessing.</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 21:33:17 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/airbnbs-search-payload-doesnt-carry-amenities-heres-what-we-do-instead-of-guessing-2hge</link>
      <guid>https://dev.to/devil_scrapes/airbnbs-search-payload-doesnt-carry-amenities-heres-what-we-do-instead-of-guessing-2hge</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Airbnb's homes-search page embeds a JSON payload with everything the front end needs to render a results grid — price, rating, host, superhost badge, bedroom count — but not everything a comps dataset wants. Amenities are usually absent from that payload entirely; they live on the listing's own detail page. Building an &lt;a href="https://apify.com/DevilScrapes/airbnb-scraper" rel="noopener noreferrer"&gt;Airbnb scraper&lt;/a&gt; around that reality means returning &lt;code&gt;null&lt;/code&gt; for a field the search payload didn't carry, instead of guessing, and pinning the residential exit IP to the market's own country so a $180 nightly rate doesn't quietly become a $180 rate in the wrong currency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually in Airbnb's search payload
&lt;/h2&gt;

&lt;p&gt;Airbnb doesn't publish a documented API for this data, so the only source of truth is the page itself, as of September 2026. Each search-results page for a given place and date range ships a large embedded JSON blob that server-renders the grid — no separate XHR call needed to get the base fields. Per listing, that blob reliably carries: listing ID, title, room/listing type, coordinates, price and currency as displayed, rating, review count, host name and ID, superhost status, bedroom/bed/bathroom counts, max guest capacity, and a set of photo URLs.&lt;/p&gt;

&lt;p&gt;What it does &lt;em&gt;not&lt;/em&gt; reliably carry: amenities. The search grid doesn't need to render a full amenity list to sell you on a card, so Airbnb doesn't ship one in the search payload. That data lives on the listing's own &lt;code&gt;/rooms/&amp;lt;id&amp;gt;&lt;/code&gt; page, in a different section of embedded JSON entirely, structured around amenity categories rather than a flat list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we return &lt;code&gt;null&lt;/code&gt; instead of inventing a field
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is to infer amenities from the listing title or type — "Entire home" listings statistically have a kitchen more often than a private room does — and ship a best-guess field. We don't do that. A &lt;code&gt;null&lt;/code&gt; amenities field tells the caller exactly what happened: the search payload didn't carry it, and enrichment wasn't requested. A guessed value that's wrong 15% of the time is worse than an honest gap, because nothing downstream flags it as uncertain.&lt;/p&gt;

&lt;p&gt;So the Actor ships &lt;code&gt;enableDetailEnrichment&lt;/code&gt; as an explicit opt-in. Off, amenities is always an empty list and the run only touches the search-results pages — one request per page of results. On, the Actor makes a second pass and fetches each emitted listing's own detail page for its amenity section, which roughly doubles the total request volume for that run. The customer decides whether that trade-off is worth it for their use case; we don't decide it for them by silently doing the expensive thing on every run, and we don't decide it by silently doing the cheap-but-wrong thing either.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# actors/airbnb-scraper/src/models.py (shape, not full source)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResultRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;price_per_night&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;amenities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# amenities stays [] unless enrichment ran — never inferred
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same discipline applies to &lt;code&gt;total_price&lt;/code&gt;: it only populates when both &lt;code&gt;checkIn&lt;/code&gt; and &lt;code&gt;checkOut&lt;/code&gt; are supplied, because that's the only case where Airbnb's own page computes a stay total. Without both dates, you get the base nightly rate and nothing else — not a multiplied guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinning the proxy country to the market's currency
&lt;/h2&gt;

&lt;p&gt;Airbnb resolves the currency it displays partly from the requesting IP's apparent geography. A residential exit that lands in the wrong country doesn't error — it returns a normal-looking &lt;code&gt;200&lt;/code&gt; with prices in that country's currency, and the response gives no indication anything drifted. A run built around "San Francisco, CA" that happened to route through a residential exit in the UK would come back with a fully-formed dataset of listings priced in GBP, every field populated, every row internally consistent — and wrong for the customer's use case.&lt;/p&gt;

&lt;p&gt;The fix is the same shape we use across the fleet for this failure class: never let the exit country be random when the target's response depends on it. &lt;code&gt;proxyCountry&lt;/code&gt; is a required, non-randomized field wired straight into the &lt;code&gt;proxyConfiguration&lt;/code&gt; — &lt;code&gt;apifyProxyCountry&lt;/code&gt; is set explicitly per run, not left to whatever the residential pool happens to hand back. If a customer is comping a market in Spain, the proxy exits from Spain for every request in that run, and &lt;code&gt;currency&lt;/code&gt; on every row reflects it. We don't attempt currency conversion on top of that — &lt;code&gt;price_per_night&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt; are passed through exactly as Airbnb's page reports them, because converting introduces an exchange-rate assumption that's stale the moment it's written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrying without losing the run
&lt;/h2&gt;

&lt;p&gt;Airbnb's search surface sits behind anti-bot defenses rather than a documented, exhaustible rate limit — a blocked request is a session to drop, not a quota to wait out. On &lt;code&gt;403&lt;/code&gt;/&lt;code&gt;429&lt;/code&gt;/&lt;code&gt;5xx&lt;/code&gt;, the Actor rotates to a fresh residential session and browser-fingerprint impersonation profile and retries with capped exponential backoff. A run blocked partway through surfaces as completed with a status message describing what landed, not a hard failure that discards results the customer already paid the per-event price for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that generalizes
&lt;/h2&gt;

&lt;p&gt;Any page whose response depends on signals it doesn't expose — currency from IP geography, amenities on a second page only, a total only when both dates are given — will hand back a plausible answer to the wrong question if you don't pin the input and check what's actually present. Pin what the response depends on, and leave a field &lt;code&gt;null&lt;/code&gt; rather than inferring it when the source genuinely didn't say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;$0.20 per run, $0.0029 per listing row — about $3.10 per 1,000 listings, amenities included when &lt;code&gt;enableDetailEnrichment&lt;/code&gt; is on. A search that matches nothing costs only the start fee.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/DevilScrapes/airbnb-scraper" rel="noopener noreferrer"&gt;Run the Airbnb Listings &amp;amp; Pricing Scraper on Apify&lt;/a&gt; — free trial credit, no card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Devil Scrapes&lt;/a&gt;. We pin what the target's response depends on and leave the rest honestly blank.&lt;/em&gt; 😈&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How we keep 200 web scrapers alive</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:09:00 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/how-we-keep-200-web-scrapers-alive-3j0a</link>
      <guid>https://dev.to/devil_scrapes/how-we-keep-200-web-scrapers-alive-3j0a</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/maintenance-is-the-product/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Web scraper maintenance, not the first successful run, is what you're actually paying for. A scraper that returns clean rows today can return zero rows tomorrow because a target moved one field, tightened one rate limit, or started serving a different page to a different exit IP — and nothing in yesterday's green test tells you that happened. We run 202 public Actors on the Apify Store, and the daily discipline of catching that drift before a customer's run silently fails is most of what we do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping is the easy 10%
&lt;/h2&gt;

&lt;p&gt;Every scraper we've ever built worked on day one. That's the least interesting fact about it. The target's HTML structure, its internal API shape, its rate-limit thresholds, and its bot-defense posture are all things the target's own team can change on a Tuesday without telling anyone — and often does. A scraper is a contract with a system you don't control, and the contract gets renegotiated constantly, silently, and without a changelog.&lt;/p&gt;

&lt;p&gt;That's why we treat every Actor we publish as a standing commitment, not a finished artifact. Across our fleet, customers ran our Actors &lt;strong&gt;16,077 times in the last 30 days&lt;/strong&gt;, and &lt;strong&gt;96.1% of those runs succeeded&lt;/strong&gt;. That number isn't a launch-day snapshot — it's a rolling average we watch every day, because it's the only honest measure of whether a scraper is actually alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  A "SUCCEEDED" run can still deliver nothing
&lt;/h2&gt;

&lt;p&gt;The first lesson in web scraper maintenance is that the platform's own status field lies to you if you let it. A run can finish with a green &lt;code&gt;SUCCEEDED&lt;/code&gt; status and zero rows in the dataset — the process didn't crash, it just quietly found nothing. We learned to stop trusting "it finished" as proof of "it worked," and instead check what actually landed in the dataset against what a healthy run for that target normally delivers.&lt;/p&gt;

&lt;p&gt;The second lesson: a scraper's own recent runs aren't the fleet's health signal — the target's 30-day public run stats are, because a handful of our own QA runs passing tells you nothing about the last thousand customer runs. We watch the rolling window, not the latest tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real breakages, not hypothetical ones
&lt;/h2&gt;

&lt;p&gt;Three examples from our own fleet, because concrete beats abstract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One of our job-board Actors kept every retry inside its own bounded loop, but nothing capped the &lt;em&gt;sum&lt;/em&gt; of all the retries across a full career-site crawl. On a large site, the retries stacked up past the platform's own run timeout, and the run died with nothing written — even though every individual request handler was working exactly as designed.&lt;/li&gt;
&lt;li&gt;A live-chat archiver opened a long-lived connection and read from it in a loop with no upper bound. Against a quiet channel with a stalled connection, that loop could hang indefinitely instead of timing out and flushing whatever it had already captured.&lt;/li&gt;
&lt;li&gt;On a geo-sensitive retail target, a residential exit in the wrong country didn't get blocked — it got a normal-looking &lt;code&gt;200 OK&lt;/code&gt; with prices in the wrong currency. No error, no crash, just quietly wrong data next to correctly-labeled rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic. They're the ordinary failure modes of long-running network code, and every one of them looks identical to "everything is fine" until you go looking for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop: monitor, triage, fix, verify in the cloud
&lt;/h2&gt;

&lt;p&gt;Our web scraper maintenance loop has four steps, and skipping the last one is how bugs come back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; every public Actor's rolling 30-day success rate and dataset output, not just the latest run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage&lt;/strong&gt; the failure to a specific cause — a fault that should have been isolated to one item instead of killing the whole run, a retry budget that needed a hard ceiling, or an exit IP that needs to be pinned to the right country.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix&lt;/strong&gt; the root cause: catch and skip per-item failures instead of letting one bad page abort a thousand good ones, cap the run's total wall-clock budget, add a guard that treats an unexpected currency or a country-selector page as a block rather than a result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify in the cloud&lt;/strong&gt;, against a customer-sized input, before calling it done. A fix that only ran against a five-row test fixture hasn't been tested against the failure it was written for — the bug that took down a real crawl needed a real crawl's worth of pages to reproduce.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also learned the hard way that "committed" isn't "deployed": a fix can sit correct and tested on our side while the live Actor is still running the build from before the fix. So the loop always ends by confirming the &lt;em&gt;deployed&lt;/em&gt; version, not just the patched source, is what customers are actually running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price the maintenance, not the first run
&lt;/h2&gt;

&lt;p&gt;This is why our pricing is pay-per-event instead of a flat license: a &lt;strong&gt;$0.20 actor-start fee&lt;/strong&gt;, then a &lt;strong&gt;per-result price&lt;/strong&gt; (typically $1–$5 per 1,000 rows depending on the target). If a run returns nothing, you pay the $0.20 warm-up and nothing else — you're never billed for a scraper that had a bad day. But the flip side matters too: the per-result price isn't just covering the compute for that one run. It's covering the fact that we're still watching this Actor next month, and the month after, catching the day the target changes something and fixing it before it becomes your outage.&lt;/p&gt;

&lt;p&gt;Buyers who evaluate a scraper by running it once and checking the output are pricing the wrong thing. The right question isn't "did it work today" — it's "who's watching it tomorrow."&lt;/p&gt;

&lt;p&gt;Two Actors from our fleet that illustrate the pattern directly: the &lt;a href="https://apify.com/DevilScrapes/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper&lt;/a&gt; survived exactly the run-budget fix described above, and the &lt;a href="https://apify.com/DevilScrapes/kick-chat-archive" rel="noopener noreferrer"&gt;Kick Chat Scraper &amp;amp; Real-Time Archive&lt;/a&gt; survived the unbounded-loop fix. Both are live, monitored, and priced the same way — pay only for rows that land.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it on Apify:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/kick-chat-archive" rel="noopener noreferrer"&gt;Kick Chat Scraper &amp;amp; Real-Time Archive&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does "web scraper maintenance" actually mean?
&lt;/h3&gt;

&lt;p&gt;It means someone is watching whether a published scraper keeps working after the target site changes — its HTML, its internal API shapes, its rate limits, or its bot defenses — and fixing it before those changes turn into failed customer runs. Without it, a scraper's success rate decays quietly over weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you know an Actor is broken before a customer complains?
&lt;/h3&gt;

&lt;p&gt;We watch each Actor's rolling 30-day public run stats — total runs, successes, failures, timeouts — rather than relying on our own last few test runs, which can look green while customer runs are failing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why can a run say "SUCCEEDED" but return no data?
&lt;/h3&gt;

&lt;p&gt;Because the process finishing without crashing and the process actually finding data are two different things. A run can complete cleanly against a page that returned nothing useful — a geo-mismatched exit, an empty result set, a silently changed selector — and still report success unless something checks the output, not just the exit code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do you price per result instead of a flat fee?
&lt;/h3&gt;

&lt;p&gt;A flat fee prices the code as it exists on launch day. Pay-per-event pricing — a small start fee plus a per-result charge — means you only pay when data actually lands, and it keeps our incentives aligned with keeping the Actor working, since a broken Actor earns nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this apply to every Actor you publish?
&lt;/h3&gt;

&lt;p&gt;Yes. Every one of our 202 live Actors goes through the same monitor-triage-fix-verify loop; none of them are "launch and forget."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/google-ads-transparency/" rel="noopener noreferrer"&gt;google-ads-transparency&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/workday-jobs-scraper/" rel="noopener noreferrer"&gt;workday-jobs-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/workday-jobs-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/kick-chat-archive/" rel="noopener noreferrer"&gt;kick-chat-archive&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/kick-chat-archive" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>python</category>
      <category>data</category>
    </item>
    <item>
      <title>Why your scraper gets blocked at 3 a.m.</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:08:28 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/why-your-scraper-gets-blocked-at-3-am-3bbd</link>
      <guid>https://dev.to/devil_scrapes/why-your-scraper-gets-blocked-at-3-am-3bbd</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/why-scrapers-get-blocked/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Why does my scraper get blocked? Usually not because your parser is wrong — it's because the request itself doesn't look like it came from a browser. A plain HTTP client sends a different handshake than Chrome, reuses an IP with no browsing history behind it, drops session continuity between pages, and ignores the target's own rate-limit signals. Any one of those is enough to get flagged; most scrapers hit two or three at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's rarely the parser
&lt;/h2&gt;

&lt;p&gt;When a scraper "stops working," the instinct is to check the CSS selector or the JSON path first. That's usually not it. If a run comes back with a splash page, a stripped-down HTML shell, or a response that doesn't match what a browser would see, the request never made it past the target's front door as a trusted client. The parser was never given real data to parse. Here's what actually decides whether a request gets treated as a browser or a bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLS and HTTP fingerprints
&lt;/h2&gt;

&lt;p&gt;Every HTTP client — Python's &lt;code&gt;requests&lt;/code&gt;, Node's &lt;code&gt;fetch&lt;/code&gt;, curl — negotiates its TLS handshake and orders its HTTP headers in a way that's specific to that library, not to the browser it's pretending to be. Real Chrome, Firefox, and Safari each send a distinctive cipher-suite order, extension list, and header casing. A target that inspects the handshake (plenty do, quietly, without ever showing a challenge page) can tell "Python script" from "Chrome tab" before your request body is even read, independent of your &lt;code&gt;User-Agent&lt;/code&gt; string.&lt;/p&gt;

&lt;p&gt;We rotate through real Chrome, Firefox, and Safari handshake profiles per request, so the target's edge sees a browser-shaped connection rather than a library's default fingerprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  IP reputation: residential vs. datacenter
&lt;/h2&gt;

&lt;p&gt;The exit IP carries its own trust score, built from a history of prior traffic. A datacenter IP block that's been used by a thousand scrapers before yours starts with a reputation deficit, no matter how clean your request looks. A residential IP — an exit that looks like an ordinary home connection — usually starts closer to neutral. Neither is a silver bullet: datacenter exits are faster and cheaper, and they clear plenty of targets fine, so the right call is target-by-target, not a blanket "always residential" rule. We rotate through both residential and datacenter proxy pools and pick per target based on what actually clears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session and cookie continuity
&lt;/h2&gt;

&lt;p&gt;Pagination on modern sites is usually bound to a session — a cursor, a cookie, a token issued on page one that page two expects back. Rotate your exit IP mid-session without carrying that session forward, and the target doesn't see "a new visitor," it sees a broken client and invalidates the cursor. We hold a sticky session — the same exit IP and cookie jar — for the length of a paginated crawl, and only rotate to a fresh session when a page actually needs one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits: &lt;code&gt;Retry-After&lt;/code&gt; is not a suggestion
&lt;/h2&gt;

&lt;p&gt;Most targets that rate-limit tell you exactly how long to wait, in the &lt;code&gt;Retry-After&lt;/code&gt; header on a &lt;code&gt;429&lt;/code&gt; or &lt;code&gt;503&lt;/code&gt; response. Ignoring it and retrying immediately is one of the fastest ways to escalate a soft rate limit into a hard IP ban. We back off exponentially on &lt;code&gt;408&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt;, and &lt;code&gt;5xx&lt;/code&gt; responses and honor &lt;code&gt;Retry-After&lt;/code&gt; when the target sends one, capped at a handful of attempts per page so a stuck request doesn't run forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap that doesn't look like a block at all
&lt;/h2&gt;

&lt;p&gt;The failure mode that cost us the most debugging time wasn't a &lt;code&gt;403&lt;/code&gt; — it was a &lt;code&gt;200 OK&lt;/code&gt;. On a geo-sensitive target, a residential exit in the wrong country doesn't get rejected. It gets served a normal-looking page: prices in the wrong currency, a country-selector splash screen instead of the product page, or a locale variant that silently doesn't match what was requested. There's no error, no challenge, nothing a naive status-code check would ever flag — just data that's confidently wrong sitting next to rows that are correct.&lt;/p&gt;

&lt;p&gt;We pin the exit country on every geo-sensitive target instead of letting the proxy pick one at random, and we added a guard that treats an unexpected currency symbol or a country-selector marker as a failed fetch to retry — not a result to parse and ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like for you
&lt;/h2&gt;

&lt;p&gt;Every one of the mechanics above is a maintenance burden if you own it yourself: fingerprint drift as browsers update, proxy pool churn, session bugs that only show up at scale, rate-limit tuning per target, and geo bugs that pass every test except the one that matters. We absorb all of it inside the Actor, so what lands in your dataset is rows, not a debugging session at 3 a.m.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it on Apify:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/amazon-reviews-scraper" rel="noopener noreferrer"&gt;Amazon Reviews Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/bestbuy-price-scraper" rel="noopener noreferrer"&gt;Best Buy Product &amp;amp; Price Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my scraper get blocked even with a correct User-Agent header?
&lt;/h3&gt;

&lt;p&gt;Because the &lt;code&gt;User-Agent&lt;/code&gt; string is just one header among many. The TLS handshake and the full HTTP header order and casing are usually what a target's edge actually inspects, and a plain HTTP client sends a library-specific fingerprint there regardless of what &lt;code&gt;User-Agent&lt;/code&gt; you set on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a residential proxy always better than a datacenter proxy?
&lt;/h3&gt;

&lt;p&gt;No — it depends on the target. Residential exits generally carry a better starting trust score, but datacenter exits are faster, cheaper, and clear plenty of targets without issue. The right choice is per-target, decided by what actually gets through, not a fixed rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did my scraper get a 200 response with wrong data instead of an error?
&lt;/h3&gt;

&lt;p&gt;Geo-sensitive targets often don't reject a foreign exit outright — they serve a normal-looking response in the wrong locale: wrong currency, a country-selector page, or a mismatched variant. It looks like success unless you specifically check the content against what's expected for the country you asked for.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does honoring &lt;code&gt;Retry-After&lt;/code&gt; actually prevent?
&lt;/h3&gt;

&lt;p&gt;It prevents a soft, temporary rate limit from escalating into a hard IP ban. Retrying immediately after a &lt;code&gt;429&lt;/code&gt; tells the target your client isn't respecting its limits, which is exactly the pattern that gets an IP or session blacklisted rather than just throttled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can session continuity really break a scrape on its own?
&lt;/h3&gt;

&lt;p&gt;Yes. If a target's pagination cursor is bound to a session cookie and you rotate the exit IP between pages without carrying the session forward, the target sees an inconsistent client and drops the cursor — the crawl stalls or restarts from page one, not because the parser broke, but because the session did.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/amazon-reviews-scraper/" rel="noopener noreferrer"&gt;amazon-reviews-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/amazon-reviews-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/bestbuy-price-scraper/" rel="noopener noreferrer"&gt;bestbuy-price-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/bestbuy-price-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/google-ads-transparency/" rel="noopener noreferrer"&gt;google-ads-transparency&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>python</category>
      <category>data</category>
    </item>
    <item>
      <title>Export a competitor's entire Google Ads library</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:07:56 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/export-a-competitors-entire-google-ads-library-1pia</link>
      <guid>https://dev.to/devil_scrapes/export-a-competitors-entire-google-ads-library-1pia</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/google-ads-transparency-export/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; The &lt;a href="https://adstransparency.google.com" rel="noopener noreferrer"&gt;Google Ads Transparency Center&lt;/a&gt; publicly lists every ad creative Google is currently serving for a given advertiser, across Search, YouTube, Display, Shopping, Maps, and Play — but Google ships no official API for it. Our &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt; exports it by domain or advertiser ID to JSON or CSV, at $0.20 per run plus $3.00 per 1,000 ads landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A public registry with no export button
&lt;/h2&gt;

&lt;p&gt;Google has quietly built one of the most useful competitive-intelligence datasets on the web and given it almost no interface. Search or paste a domain into the Transparency Center and you get a paginated UI showing every ad creative currently running for that advertiser — 40 at a time, one click at a time. For a brand the size of Nike, that's tens of thousands of creatives behind an interface built for spot-checking one ad, not exporting a library.&lt;/p&gt;

&lt;p&gt;There's no bulk download, no CSV export, and no documented API. If you want the dataset rather than the UI, you need to talk to whatever the UI itself talks to — which is exactly what this Actor does. It replays the same internal request the browser makes, carrying a real browser's TLS handshake so the request isn't distinguishable from the page you'd load by hand, and turns the response into typed rows instead of rendered HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes back per ad
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;advertiser_id&lt;/code&gt; / &lt;code&gt;advertiser_name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Who's running the ad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;creative_id&lt;/code&gt; / &lt;code&gt;creative_url&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A stable ID and a deep link back into the Transparency Center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;landing_domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where the click actually goes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text, image, or video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;first_shown_ts&lt;/code&gt; / &lt;code&gt;last_shown_ts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;How long the creative has been live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;impressions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google's own reported impression count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;preview_image_url&lt;/code&gt; / &lt;code&gt;preview_content_js_url&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The creative asset itself, not just its metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The locale you tagged the request with&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scraped_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ISO-8601 timestamp of the pull&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last point matters more than it looks: plenty of scrapers in this space return who's advertising and where the click goes, but drop the creative asset entirely. Getting &lt;code&gt;preview_image_url&lt;/code&gt; back means you can actually look at the ad, not just its metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agencies running competitive intel&lt;/strong&gt; — pull a client's top three rivals weekly and diff the creative set to catch new launches before the client asks about them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand protection teams&lt;/strong&gt; — sweep your own brand domain and trademark terms to see who else is buying ads against your name, including resellers and affiliates you never authorized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affiliate-fraud and compliance teams&lt;/strong&gt; — cross-reference &lt;code&gt;landing_domain&lt;/code&gt; against &lt;code&gt;advertiser_name&lt;/code&gt;; a mismatch is the fastest signal that an advertiser isn't who their name claims.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DevilScrapes/google-ads-transparency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;searchDomains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nike.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advertiser_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;creative_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landing_domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can pass multiple domains in one run, or drop in advertiser IDs directly if you already have them from a Transparency Center URL. Each ad lands as one dataset row, exportable as JSON, CSV, Excel, or JSONL from the run's Storage tab, or pulled straight over the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing, exactly
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: a &lt;strong&gt;$0.20 actor-start fee&lt;/strong&gt; once per run, then &lt;strong&gt;$3.00 per 1,000 ads&lt;/strong&gt; written to the dataset. Nothing is charged for ads you don't get back.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pull&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;500 ads (the snippet above)&lt;/td&gt;
&lt;td&gt;$1.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 ads&lt;/td&gt;
&lt;td&gt;$3.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000 ads&lt;/td&gt;
&lt;td&gt;$30.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000 ads (a monthly sweep across a portfolio of brands)&lt;/td&gt;
&lt;td&gt;$300.20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A weekly sweep of five competitors at roughly 200 ads each costs about $4 a month in per-result charges plus five runs' worth of start fees — a fraction of what a dedicated ad-intelligence subscription runs, for exactly the slice of that data you actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're upfront about
&lt;/h2&gt;

&lt;p&gt;Google's underlying endpoint ignores the region parameter server-side — we tested every plausible request shape to confirm it rather than shipping a region filter that quietly does nothing. We still let you tag exports by intended locale, but it's metadata, not a live filter. Video and rich creatives return a script bundle URL rather than a renderable video file, since actually playing the creative requires executing Google's own JS. And Google itself retains roughly 12 months of history, so a date range older than that clips to whatever Google still has.&lt;/p&gt;

&lt;p&gt;If you need the full picture before committing to a run, the &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Actor's Store listing&lt;/a&gt; has the complete field reference and input options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it on Apify:&lt;/strong&gt; &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt; — paste a domain, get every creative back as clean rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Google have an official Ads Transparency API?
&lt;/h3&gt;

&lt;p&gt;No. As of 2026, Google publishes no official API for the Transparency Center. This Actor replays the same internal request the browser UI makes and returns typed rows instead of rendered pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is scraping the Google Ads Transparency Center legal?
&lt;/h3&gt;

&lt;p&gt;The Transparency Center is a public registry Google operates under regulatory pressure in the EU and US. This Actor reads only what the public UI already exposes, at a measured pace, and doesn't collect personal data — only advertiser-level ad metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I filter results by country or region?
&lt;/h3&gt;

&lt;p&gt;Not effectively — Google's own endpoint ignores the region parameter, a limitation we verified directly rather than shipping a filter that silently does nothing. The &lt;code&gt;region&lt;/code&gt; field is available for tagging your export, not for narrowing results.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does it cost to export 500 ads?
&lt;/h3&gt;

&lt;p&gt;$1.70 total: the $0.20 per-run start fee plus $3.00 per 1,000 ads, prorated to $1.50 for 500 ads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I automate a weekly competitor sweep?
&lt;/h3&gt;

&lt;p&gt;Yes — attach the Actor to an Apify Console schedule and run it weekly. Google's Transparency Center updates at most daily, so a weekly cadence catches new creative launches without wasting runs on unchanged data.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/google-ads-transparency/" rel="noopener noreferrer"&gt;google-ads-transparency&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>python</category>
      <category>data</category>
    </item>
    <item>
      <title>Pay-per-result scraping: what you actually pay</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:07:24 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/pay-per-result-scraping-what-you-actually-pay-3mf</link>
      <guid>https://dev.to/devil_scrapes/pay-per-result-scraping-what-you-actually-pay-3mf</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/pay-per-result-pricing-explained/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Pay per result web scraping means you're billed for what actually lands in your dataset, not for the compute time a run consumed or a flat monthly fee regardless of usage. On Apify, that's Pay-Per-Event: a small &lt;strong&gt;actor-start fee&lt;/strong&gt; charged once per run, then a &lt;strong&gt;per-result price&lt;/strong&gt; — typically $1 to $5 per 1,000 rows — charged only for the rows that were actually written. A run that finds nothing costs only the start fee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two charges, and only two
&lt;/h2&gt;

&lt;p&gt;Every Actor we publish bills exactly two kinds of event:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;actor-start&lt;/code&gt;&lt;/strong&gt; — a fixed fee, charged once when the run begins, regardless of how much data it finds. It covers the warm-up cost: spinning up the run, resolving a proxy, establishing the first session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A per-result event&lt;/strong&gt; — charged once for each row that's validated and written to the dataset. Nothing charged for rows that were requested but never landed — a blocked page, an empty search, a target with no matching data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second point is the one buyers underrate. If you run a scraper against a query that returns nothing — a product that's out of stock, a search term with zero hits, a page the target took down — you are not billed as if the run failed on your end. You pay the start fee and nothing else, because nothing else happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three real price points
&lt;/h2&gt;

&lt;p&gt;Pricing varies by target, because the engineering cost varies by target — a static RSS feed and a JavaScript-heavy conversation graph are not the same amount of work to keep working. Here's what that looks like in practice, using our own published prices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;Start fee&lt;/th&gt;
&lt;th&gt;Per-result price&lt;/th&gt;
&lt;th&gt;1,000 results&lt;/th&gt;
&lt;th&gt;10,000 results&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/rss-feed-scraper" rel="noopener noreferrer"&gt;RSS Feed Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$0.005&lt;/td&gt;
&lt;td&gt;$1.00 / 1,000&lt;/td&gt;
&lt;td&gt;$1.01&lt;/td&gt;
&lt;td&gt;$10.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;$3.00 / 1,000&lt;/td&gt;
&lt;td&gt;$3.20&lt;/td&gt;
&lt;td&gt;$30.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/threads-reply-tree" rel="noopener noreferrer"&gt;Threads Reply Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;$5.00 / 1,000&lt;/td&gt;
&lt;td&gt;$5.20&lt;/td&gt;
&lt;td&gt;$50.20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things worth noticing in that table. First, the start fee itself varies: a feed parser that runs on a schedule against a lightweight, static format costs almost nothing to warm up, while a target that requires session handling and fingerprint rotation carries a heavier fixed cost per run. Second, the per-result price tracks how much reliability engineering the target demands — a conversation-graph scrape that has to hold a session across a nested reply tree costs more per row than parsing a well-formed feed. Third, and most importantly: at zero results, every row in that table collapses to just the start fee. A misconfigured input or a target with nothing new to return costs cents, not dollars.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an empty run actually costs
&lt;/h2&gt;

&lt;p&gt;Take the RSS Feed Scraper on a daily schedule watching for new posts. Most days, nothing new has been published — zero new items, zero per-result charges, just the $0.005 start fee. Only on the days something actually publishes does the per-result charge kick in. Contrast that with a model that bills for compute time regardless of output: a scheduled run that finds nothing still burns the same runtime cost as one that finds fifty items, because the meter is measuring seconds, not results.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to cap spend before it happens
&lt;/h2&gt;

&lt;p&gt;Pay-per-event pricing doesn't remove the need to set a ceiling — it just moves the ceiling to a place you control directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the Actor's own result-limit input.&lt;/strong&gt; Most of our Actors expose a &lt;code&gt;maxResults&lt;/code&gt; (or equivalent) field — set it and the run stops pulling once it hits that number, capping the per-result charge at a known maximum before the run even starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Apify's account-level usage limits.&lt;/strong&gt; Apify Console lets you cap total monthly spend across all Actors, which stops runs platform-wide once you hit it — a backstop independent of any single Actor's input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start small and scale the input, not the trust.&lt;/strong&gt; Run a capped test — a few dozen results — to confirm the output shape and rate before pointing the same Actor at a 10,000-row pull.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this compares to the alternatives
&lt;/h2&gt;

&lt;p&gt;Two other pricing models exist on scraping platforms, and neither is wrong — they're just measuring something different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute-unit pricing&lt;/strong&gt; bills for the CPU and memory time a run consumes, independent of what it finds. It rewards efficient code and can be cheaper for very high-volume, low-per-item-cost targets, but it means a run that finds nothing still costs whatever compute it burned trying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat subscription pricing&lt;/strong&gt; bills a fixed monthly rate regardless of usage. It's predictable and can be cheaper at very high, steady volume, but it means you're paying the same in a month you barely use it as in a month you lean on it hard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pay-per-result sits between the two: you're not billed for idle compute, and you're not paying a flat rate for a month you didn't use — you're billed for what showed up in your dataset, which is usually the number a buyer actually cares about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it on Apify:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/rss-feed-scraper" rel="noopener noreferrer"&gt;RSS Feed Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/DevilScrapes/threads-reply-tree" rel="noopener noreferrer"&gt;Threads Reply Scraper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happens if my scraper run finds zero results?
&lt;/h3&gt;

&lt;p&gt;You pay only the actor-start fee — typically a few cents to $0.20 depending on the Actor — and nothing else. The per-result charge only applies to rows that were actually validated and written to the dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is pay-per-result cheaper than a flat monthly subscription?
&lt;/h3&gt;

&lt;p&gt;It depends on your usage pattern. For sporadic or variable-volume use, pay-per-result usually costs less because you're never paying for a month you didn't use. For very high, steady, predictable volume, a flat rate can work out cheaper — the two models are optimized for different usage shapes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do different Actors charge different per-result prices?
&lt;/h3&gt;

&lt;p&gt;The price reflects the ongoing engineering cost of keeping that specific target working — session handling, proxy rotation, and reliability engineering vary a lot by target, and the price tracks that, not an arbitrary markup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I set a hard spending cap before running a scraper?
&lt;/h3&gt;

&lt;p&gt;Yes, two ways: most Actors expose a result-limit input (like &lt;code&gt;maxResults&lt;/code&gt;) that caps the run's own output, and Apify Console lets you set an account-wide monthly usage limit as a platform-level backstop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the actor-start fee apply every time I run an Actor?
&lt;/h3&gt;

&lt;p&gt;Yes — it's charged once per run, regardless of how many results come back, because it covers the fixed cost of starting the run itself: spinning up the container, resolving a proxy, establishing the first session.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/google-ads-transparency/" rel="noopener noreferrer"&gt;google-ads-transparency&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/threads-reply-tree/" rel="noopener noreferrer"&gt;threads-reply-tree&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/threads-reply-tree" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/rss-feed-scraper/" rel="noopener noreferrer"&gt;rss-feed-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/rss-feed-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>python</category>
      <category>data</category>
    </item>
    <item>
      <title>Run an Apify Actor From n8n, Make or Zapier</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:06:52 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/run-an-apify-actor-from-n8n-make-or-zapier-376b</link>
      <guid>https://dev.to/devil_scrapes/run-an-apify-actor-from-n8n-make-or-zapier-376b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/run-apify-actor-from-n8n-make-zapier/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you want to run an Apify Actor from n8n, Make, or Zapier, the short version is: install the official connector, point it at the Actor slug, pass your input as JSON, and either wait for the run to finish inline or fire a webhook when it does. The whole setup takes about 10 minutes once you know which of those two patterns your workflow needs. Here's both, with a working example against a live Actor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why wire a scraper into a no-code tool at all
&lt;/h2&gt;

&lt;p&gt;Most teams that reach for n8n, Make, or Zapier already have the destination sorted — a Slack channel, a Google Sheet, a CRM field, an AI agent's context window. What they're missing is the step that gets fresh, structured data &lt;em&gt;into&lt;/em&gt; that destination on a schedule, without someone manually running a script. An Apify Actor is that step: point it at a target, get back Pydantic-validated rows, and let your automation platform do the routing.&lt;/p&gt;

&lt;p&gt;We'll use &lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/strong&gt; as the running example — it pulls a competitor's live ad creatives by domain, which is a natural fit for a recurring "check what our competitors are running this week" workflow. The pattern below applies to any of our 202 live Actors; only the input fields change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A — n8n
&lt;/h2&gt;

&lt;p&gt;n8n ships an official &lt;a href="https://www.npmjs.com/package/@apify/n8n-nodes-apify" rel="noopener noreferrer"&gt;Apify node&lt;/a&gt; (community package, install it from the n8n node panel).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the Apify node&lt;/strong&gt; to your workflow canvas and connect it with your Apify API token (create one under Apify Console → Settings → Integrations).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the operation.&lt;/strong&gt; For a short run, use &lt;strong&gt;Run Actor synchronously and get dataset items&lt;/strong&gt; — this maps to Apify's &lt;code&gt;run-sync-get-dataset-items&lt;/code&gt; endpoint and returns the rows directly into the next node, no polling required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the Actor&lt;/strong&gt; to &lt;code&gt;DevilScrapes/google-ads-transparency&lt;/code&gt; and pass the input JSON:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"advertiserDomain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"competitor.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxAds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chain a downstream node&lt;/strong&gt; — a Sheets append, a Slack message, or an AI-agent node that summarizes new creatives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a Schedule Trigger&lt;/strong&gt; upstream to run it daily or weekly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Option B — Make
&lt;/h2&gt;

&lt;p&gt;Make (formerly Integromat) has an &lt;a href="https://www.make.com/en/integrations/apify" rel="noopener noreferrer"&gt;Apify app&lt;/a&gt; with two modules that matter here: &lt;strong&gt;Run Actor&lt;/strong&gt; and &lt;strong&gt;Get Dataset Items&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the &lt;strong&gt;Apify → Run Actor&lt;/strong&gt; module, authenticate with your API token, and select the Actor.&lt;/li&gt;
&lt;li&gt;Fill the input fields — Make renders the Actor's input schema as a form automatically, so you rarely need to hand-write JSON here.&lt;/li&gt;
&lt;li&gt;For a fast Actor, tick &lt;strong&gt;wait for finish&lt;/strong&gt; and the module blocks until the run completes, then hands the run ID to the next module.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;Apify → Get Dataset Items&lt;/strong&gt;, feed it the dataset ID from step 3, and route the rows into your scenario — a filter, a router, a CRM update.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Option C — Zapier
&lt;/h2&gt;

&lt;p&gt;Zapier's &lt;a href="https://zapier.com/apps/apify/integrations" rel="noopener noreferrer"&gt;Apify integration&lt;/a&gt; exposes &lt;strong&gt;Run Actor&lt;/strong&gt; as an action and &lt;strong&gt;New Dataset Item&lt;/strong&gt; as a trigger.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Zap with your usual trigger (a form submission, a new row, a schedule).&lt;/li&gt;
&lt;li&gt;Add the &lt;strong&gt;Apify: Run Actor&lt;/strong&gt; action, select the Actor and fill the input fields from the trigger's data.&lt;/li&gt;
&lt;li&gt;If you need the results in the same Zap, add a second &lt;strong&gt;Apify: Get Dataset Items&lt;/strong&gt; action after a short delay step, since Zapier's action model doesn't block on long-running Actor runs the way n8n's synchronous option does.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  run-sync-get-dataset-items vs async + webhook
&lt;/h2&gt;

&lt;p&gt;This is the one decision that actually matters, and it comes down to how long the Actor takes to finish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;run-sync-get-dataset-items&lt;/code&gt; (synchronous) when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The run typically finishes in well under the platform's timeout window (n8n, Make, and Zapier all cap how long a single step can block — usually somewhere between 30 seconds and a few minutes depending on your plan).&lt;/li&gt;
&lt;li&gt;You're pulling a small-to-medium batch — a handful of URLs, one domain, one channel.&lt;/li&gt;
&lt;li&gt;You want the simplest possible workflow: one node in, rows out, no state to track between steps.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/DevilScrapes~google-ads-transparency/run-sync-get-dataset-items?token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"advertiserDomain": "competitor.com", "maxAds": 100}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use async + webhook when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The run can take minutes to hours — a channel back-catalog pull, a multi-thousand-row batch, or anything where we're deliberately pacing requests against a target's rate limits rather than hammering it and getting the whole run blocked.&lt;/li&gt;
&lt;li&gt;You don't want your automation platform's execution sitting idle (and possibly billed) while a run completes.&lt;/li&gt;
&lt;li&gt;You want retry/notification logic independent of the trigger — e.g. post to Slack only on &lt;code&gt;SUCCEEDED&lt;/code&gt;, alert on &lt;code&gt;FAILED&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The async pattern: call the regular &lt;code&gt;run-actor&lt;/code&gt; endpoint (no &lt;code&gt;-sync&lt;/code&gt;), attach a &lt;code&gt;webhooks&lt;/code&gt; parameter pointing at your automation platform's webhook-catching URL, and let Apify call you back when the run finishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DevilScrapes/google-ads-transparency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advertiserDomain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;competitor.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxAds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;webhooks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventTypes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACTOR.RUN.SUCCEEDED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requestUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://hook.eu1.make.com/your-webhook-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n8n and Make both expose a &lt;strong&gt;Webhook trigger&lt;/strong&gt; node — drop that URL in, and the rest of your workflow only runs once the Actor has actually finished, with the run ID and dataset ID available in the payload for a follow-up &lt;strong&gt;Get Dataset Items&lt;/strong&gt; call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs
&lt;/h2&gt;

&lt;p&gt;Pay-Per-Event pricing applies the same whether you trigger a run from the Apify Console or from n8n — there's no automation surcharge. Google Ads Transparency Scraper charges a &lt;strong&gt;$0.20&lt;/strong&gt; actor-start fee plus &lt;strong&gt;$3.00 per 1,000 ad rows&lt;/strong&gt; returned. Pulling 100 ads through the n8n flow above costs &lt;strong&gt;$0.20 + $0.30 = $0.50&lt;/strong&gt;; a 1,000-ad batch runs &lt;strong&gt;$3.20&lt;/strong&gt;. If the domain has no ads live, you're charged only the $0.20 start fee — never for rows that never landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on Apify
&lt;/h2&gt;

&lt;p&gt;Grab an Apify account (free trial credit, no card required) and connect it to your automation tool of choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper on Apify&lt;/a&gt;&lt;/strong&gt; — the Actor used in this walkthrough.&lt;/li&gt;
&lt;li&gt;Browse the rest of the catalog for an Actor that fits your workflow — every one of our 202 live Actors works with the same n8n / Make / Zapier setup described above, just with different input fields.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a paid n8n, Make, or Zapier plan to run an Apify Actor?
&lt;/h3&gt;

&lt;p&gt;No. All three platforms' free tiers support HTTP-based integrations and the official Apify connectors. What limits you on a free tier is usually execution time or monthly task/operation count, not the ability to call Apify at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which is faster to set up, the node/app or a raw HTTP request?
&lt;/h3&gt;

&lt;p&gt;The official connectors (n8n's Apify node, Make's Apify app) are faster for most people because they render the Actor's input schema as a form. A raw HTTP Request node calling the REST API directly works identically and is sometimes preferable if you're already comfortable with JSON and want fewer moving parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the Actor run fails mid-workflow?
&lt;/h3&gt;

&lt;p&gt;Apify marks the run &lt;code&gt;FAILED&lt;/code&gt; and, if you're on the async + webhook pattern, still fires the webhook (filter on &lt;code&gt;ACTOR.RUN.FAILED&lt;/code&gt; to catch it). We fail loud on genuine blocks rather than handing back a silent empty dataset, so a &lt;code&gt;FAILED&lt;/code&gt; status means something upstream actually broke — check the run log for the reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I pass a schedule from n8n/Make instead of using Apify's own Scheduler?
&lt;/h3&gt;

&lt;p&gt;Yes, and it's a common pattern — use your automation platform's schedule trigger to control cadence and business logic (e.g. skip weekends, only run for active accounts) while Apify just executes the single run you ask for.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/google-ads-transparency/" rel="noopener noreferrer"&gt;google-ads-transparency&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>automation</category>
      <category>python</category>
    </item>
    <item>
      <title>Twitch Chat Scraper and 2 More Live-Stream Chat Archives</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:06:21 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/twitch-chat-scraper-and-2-more-live-stream-chat-archives-4c0b</link>
      <guid>https://dev.to/devil_scrapes/twitch-chat-scraper-and-2-more-live-stream-chat-archives-4c0b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/live-stream-chat-archives/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Twitch chat scraper, a Kick chat archive, and a YouTube transcript export solve the same underlying problem from three different angles: none of the three platforms hands you a clean, structured record of what was said during a broadcast. Twitch retains a raw replay but no export tool; Kick has no chat history API at all; YouTube captions exist but aren't built for bulk pulls. Here's what each of our three Actors returns, what teams actually build with the output, and what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The platform gap, briefly
&lt;/h2&gt;

&lt;p&gt;Live-stream chat and captions are some of the richest unstructured data on the internet — real-time audience reaction, sponsor call-outs, moderation events, comprehension-level language samples — and almost none of it is queryable in bulk from the platform itself. Twitch's chat replay lives behind an internal GraphQL endpoint, not the public Helix API. Kick has no chat-history endpoint whatsoever; once a stream ends, the conversation is gone from every Kick surface. YouTube's caption tracks are public but the timedtext endpoint isn't designed for hundreds of sequential pulls. We built one Actor per platform because each one needed a different transport, not because the underlying job differs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twitch: VOD chat, after the fact
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/twitch-vod-chat-archive" rel="noopener noreferrer"&gt;Twitch Chat Scraper — VOD Chat Archive&lt;/a&gt;&lt;/strong&gt; walks the same VOD-replay endpoint Twitch's own player uses and returns one row per chat message from a published VOD — no login required. Each row carries &lt;code&gt;message_text&lt;/code&gt;, structured &lt;code&gt;message_fragments&lt;/code&gt; (so emote IDs survive, not just literal emote shortcodes), &lt;code&gt;user_color&lt;/code&gt;, &lt;code&gt;badges&lt;/code&gt;, and &lt;code&gt;message_offset_seconds&lt;/code&gt;, the position within the VOD the message was posted at.&lt;/p&gt;

&lt;p&gt;That offset field is the one people underrate. Join it against the VOD timeline and you can locate exactly where chat spiked — a common proxy for "something worth clipping happened here" — without watching the broadcast back.&lt;/p&gt;

&lt;p&gt;Pricing: &lt;strong&gt;$0.20&lt;/strong&gt; actor-start plus &lt;strong&gt;$0.001&lt;/strong&gt; per message, i.e. &lt;strong&gt;$1.20 per 1,000 messages&lt;/strong&gt;. A 5,000-message stream costs about &lt;strong&gt;$5.20&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kick: real-time, because there's no other option
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/kick-chat-archive" rel="noopener noreferrer"&gt;Kick Chat Scraper &amp;amp; Real-Time Archive&lt;/a&gt;&lt;/strong&gt; connects to the same public Pusher WebSocket Kick's own site uses to render chat, subscribes to up to 20 channel slugs at once, and archives every message live — sender, derived role (&lt;code&gt;moderator&lt;/code&gt;, &lt;code&gt;vip&lt;/code&gt;, &lt;code&gt;subscriber&lt;/code&gt;, etc.), raw badges, chat color, and both the platform's send timestamp and our scrape timestamp.&lt;/p&gt;

&lt;p&gt;The limitation that matters here isn't a limitation of the Actor, it's a limitation of Kick: this only captures chat sent &lt;strong&gt;while the run is active&lt;/strong&gt;. There's no upstream to query for last week's stream, because Kick doesn't store it either. If you want a record of a future stream, the run has to be live during it.&lt;/p&gt;

&lt;p&gt;Pricing: identical structure to Twitch — &lt;strong&gt;$0.20&lt;/strong&gt; actor-start plus &lt;strong&gt;$0.001&lt;/strong&gt; per message, so &lt;strong&gt;$1.20 per 1,000 messages&lt;/strong&gt; archived across all subscribed channels combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  YouTube: transcripts, not chat, but the same job
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/youtube-transcript-scraper" rel="noopener noreferrer"&gt;YouTube Transcript Scraper &amp;amp; Bulk Downloader&lt;/a&gt;&lt;/strong&gt; is a different shape of data — one row per &lt;em&gt;video&lt;/em&gt;, not per message — but it answers the same underlying question: what did the audience actually hear or say. It pulls manual or auto-generated caption tracks in any language and returns the full joined &lt;code&gt;transcript_text&lt;/code&gt;, optional timed &lt;code&gt;segments&lt;/code&gt;, channel name, title, duration, and every &lt;code&gt;available_languages&lt;/code&gt; code on the video, from a list of URLs or bare video IDs. No API key needed.&lt;/p&gt;

&lt;p&gt;Pricing: &lt;strong&gt;$0.20&lt;/strong&gt; actor-start plus &lt;strong&gt;$0.004&lt;/strong&gt; per transcript, i.e. &lt;strong&gt;$4.20 per 1,000 transcripts&lt;/strong&gt; — a different unit from the chat Actors (per-video, not per-message), so don't compare the two rates directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people actually build with this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Moderation research&lt;/strong&gt; — pull Twitch or Kick chat across a batch of streams and quantify how often a term, slur list, or brigading pattern appears before deciding on a moderation policy change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment and hype detection&lt;/strong&gt; — bucket &lt;code&gt;message_offset_seconds&lt;/code&gt; into time windows and chart message volume against the stream timeline; spikes correlate strongly with in-game or in-broadcast events worth reviewing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sponsor-mention auditing&lt;/strong&gt; — search archived chat and transcript text for a brand or product name to check whether — and how — a sponsored segment actually landed with the audience, independent of what the creator's read reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creator analytics&lt;/strong&gt; — a talent agency or network archiving chat and transcripts across a roster of streamers to build engagement benchmarks that don't depend on the platform's own (often coarse) analytics dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NLP and RAG datasets&lt;/strong&gt; — transcripts feed directly into a vector store or LLM context window for conference-talk or podcast search; chat archives are commonly used as training or evaluation data for toxicity and spam classifiers, with &lt;code&gt;badges&lt;/code&gt; and &lt;code&gt;sender_role&lt;/code&gt; as useful features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-broadcast review&lt;/strong&gt; — a streamer or their mod team reviewing an old VOD's chat for context around a moment, a decision, or a dispute, without scrubbing back through hours of stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How we handle the platform-side friction
&lt;/h2&gt;

&lt;p&gt;All three Actors sit on the same infrastructure the rest of our fleet uses: we rotate &lt;code&gt;curl-cffi&lt;/code&gt; browser TLS fingerprints (Chrome, Firefox, Safari) on every hop that could fingerprint a client, route through Apify Proxy with fresh sessions when a target pushes back, and retry with exponential backoff on &lt;code&gt;408/429/5xx&lt;/code&gt; up to five attempts per request, honoring &lt;code&gt;Retry-After&lt;/code&gt; when it's sent. Every row is Pydantic-validated before it lands in your dataset, with ISO-8601 timestamps and stable IDs — so you can pull results as JSON, CSV, or Excel straight from the Apify Console, or via the API, without a cleanup pass first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on Apify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/twitch-vod-chat-archive" rel="noopener noreferrer"&gt;Twitch Chat Scraper — VOD Chat Archive&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/kick-chat-archive" rel="noopener noreferrer"&gt;Kick Chat Scraper &amp;amp; Real-Time Archive&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/youtube-transcript-scraper" rel="noopener noreferrer"&gt;YouTube Transcript Scraper &amp;amp; Bulk Downloader&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every new Apify account starts with free trial credit, no card required, enough to test each Actor's output shape before committing spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I get live Twitch chat, not just VOD chat, with these Actors?
&lt;/h3&gt;

&lt;p&gt;Not with the Twitch Actor described here — it reads the VOD replay endpoint, which only exists once a broadcast has ended and Twitch has processed the recording. Live IRC-over-WebSocket chat is a separate protocol and a different Actor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does Kick chat archiving require the run to be live during the stream?
&lt;/h3&gt;

&lt;p&gt;Because Kick itself doesn't store chat history anywhere — not for us, not for anyone. There's no upstream endpoint to backfill from once a stream ends, so real-time capture is the only way to keep a record.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do these Actors need a Twitch, Kick, or Google account to run?
&lt;/h3&gt;

&lt;p&gt;No login is required for any of the three. Twitch's VOD replay and Kick's chat WebSocket are both public surfaces every viewer's browser reaches without authentication, and YouTube captions are public metadata attached to public videos.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if a video has captions disabled or a VOD has expired?
&lt;/h3&gt;

&lt;p&gt;Each Actor treats that as a normal, non-error outcome: the row (or VOD) is skipped, logged, and the run finishes &lt;code&gt;SUCCEEDED&lt;/code&gt; rather than failing the whole batch over one unavailable item.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one should I use for sentiment analysis on a specific game or topic?
&lt;/h3&gt;

&lt;p&gt;If you need audience reaction as it happened during a broadcast, use the Twitch or Kick chat Actor and bucket by &lt;code&gt;message_offset_seconds&lt;/code&gt; or &lt;code&gt;sent_at&lt;/code&gt;. If you need what the &lt;em&gt;creator&lt;/em&gt; said, use the YouTube transcript Actor against their video and run your sentiment model over &lt;code&gt;transcript_text&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/twitch-vod-chat-archive/" rel="noopener noreferrer"&gt;twitch-vod-chat-archive&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/twitch-vod-chat-archive" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/kick-chat-archive/" rel="noopener noreferrer"&gt;kick-chat-archive&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/kick-chat-archive" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/youtube-transcript-scraper/" rel="noopener noreferrer"&gt;youtube-transcript-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/youtube-transcript-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>socialmedia</category>
      <category>python</category>
    </item>
    <item>
      <title>Scrape Car Listings Europe: 22 Marketplaces, One Schema</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:05:49 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/scrape-car-listings-europe-22-marketplaces-one-schema-5303</link>
      <guid>https://dev.to/devil_scrapes/scrape-car-listings-europe-22-marketplaces-one-schema-5303</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/eu-car-marketplace-scrapers/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you want to scrape car listings across Europe, the problem isn't finding a scraper for one marketplace — it's that "the European used-car market" is actually 20-plus independent national marketplaces, each with its own layout, its own currency, its own language, and its own defenses. We built a country-specific Actor for each one and normalized the output into one shared row schema, so pulling German and Serbian listings side by side doesn't mean writing two parsers and reconciling two field naming conventions yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The buyer problem this actually solves
&lt;/h2&gt;

&lt;p&gt;Nobody wakes up wanting "an AutoScout24 scraper" as an end in itself. The people who reach for one of these Actors are usually doing one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-border price arbitrage&lt;/strong&gt; — the same 2021 BMW 3 Series sells for a meaningfully different price in Germany versus Poland versus Romania, once you normalize for mileage and trim. Spotting that gap requires comparable data from multiple countries at once, not a single-market snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dealer and fleet intelligence&lt;/strong&gt; — dealer groups and export brokers track inventory turnover, pricing moves, and stock mix across competitor lots in several countries, which means pulling the same fields from several different sites on a recurring schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market research and pricing models&lt;/strong&gt; — building a used-car valuation model that works across the EU needs training data with consistent fields (make, model, year, mileage, fuel type, price) across enough countries that a single national quirk doesn't skew the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three jobs fail the moment your data pipeline has to special-case each country's field names. That's the actual product here: not 22 scrapers, one schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared schema
&lt;/h2&gt;

&lt;p&gt;Every Actor in the fleet returns the same core fields, adapted to what each marketplace actually publishes. Compare &lt;strong&gt;leboncoin (France)&lt;/strong&gt; and &lt;strong&gt;AutoScout24 (Germany)&lt;/strong&gt; — different sites, different languages, same shape:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;leboncoin (FR)&lt;/th&gt;
&lt;th&gt;AutoScout24 (DE)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;listing_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"3117822207"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"a15406b4-db66-..."&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;make&lt;/code&gt; / &lt;code&gt;model&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"MORRIS"&lt;/code&gt; / &lt;code&gt;"Autre"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"Opel"&lt;/code&gt; / &lt;code&gt;"Corsa"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;year&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1968&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2013&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;price&lt;/code&gt; / &lt;code&gt;currency&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;34900&lt;/code&gt; / &lt;code&gt;"EUR"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;3490&lt;/code&gt; / &lt;code&gt;"EUR"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mileage_km&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;101890&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;101125&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fuel_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"Essence"&lt;/code&gt; (local label)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"Benzin"&lt;/code&gt; (local label)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transmission&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"Manuelle"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"Schaltgetriebe"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;engine_power_hp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;null&lt;/code&gt; (not always published)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;101&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;body_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"Citadine"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"Limousine"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seller_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"private"&lt;/code&gt; / &lt;code&gt;"dealer"&lt;/code&gt; (normalized)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"private"&lt;/code&gt; / &lt;code&gt;"dealer"&lt;/code&gt; (normalized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;photo_urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array of full-size image URLs&lt;/td&gt;
&lt;td&gt;array of full-size image URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scraped_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ISO-8601 timestamp&lt;/td&gt;
&lt;td&gt;ISO-8601 timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stay deliberately un-normalized, because normalizing them would lose information: &lt;strong&gt;currency&lt;/strong&gt; (most of these markets price in EUR, but a few — Iceland's ISK, Norway's NOK, Sweden's SEK, Ukraine's USD/UAH — genuinely aren't) and &lt;strong&gt;raw field labels for fuel type and transmission&lt;/strong&gt;, which we preserve verbatim in the source language alongside the normalized &lt;code&gt;seller_type&lt;/code&gt; field, since collapsing "Essence" and "Benzin" into a single enum would need a lookup table you may want to own yourself. Everything structural — field names, types, the presence of &lt;code&gt;mileage_km&lt;/code&gt; rather than miles, ISO timestamps — is identical across all 22 Actors, so a downstream pipeline written against one country's output works against any other with zero changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Country to Actor
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Marketplace&lt;/th&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;leboncoin.fr&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/leboncoin-france-cars" rel="noopener noreferrer"&gt;leboncoin France Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;AutoScout24.de&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/autoscout24-germany-cars" rel="noopener noreferrer"&gt;AutoScout24 Germany Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serbia&lt;/td&gt;
&lt;td&gt;Polovni Automobili&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/polovniautomobili-serbia-cars" rel="noopener noreferrer"&gt;Polovni Automobili Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finland&lt;/td&gt;
&lt;td&gt;Tori.fi&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/tori-finland-cars" rel="noopener noreferrer"&gt;Tori.fi Marketplace Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lithuania&lt;/td&gt;
&lt;td&gt;Autoplius.lt&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/autoplius-lithuania-cars" rel="noopener noreferrer"&gt;Autoplius Lithuania Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poland&lt;/td&gt;
&lt;td&gt;OTOMOTO.pl&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/otomoto-poland-cars" rel="noopener noreferrer"&gt;OTOMOTO Poland Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portugal&lt;/td&gt;
&lt;td&gt;Standvirtual.com&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/standvirtual-portugal-cars" rel="noopener noreferrer"&gt;Standvirtual Portugal Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulgaria&lt;/td&gt;
&lt;td&gt;Mobile.bg&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/mobile-bg-bulgaria-cars" rel="noopener noreferrer"&gt;Mobile.bg Bulgaria Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Belgium&lt;/td&gt;
&lt;td&gt;2dehands.be&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/2dehands-belgium-cars" rel="noopener noreferrer"&gt;2dehands Belgium Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlands&lt;/td&gt;
&lt;td&gt;Marktplaats.nl&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/marktplaats-netherlands-cars" rel="noopener noreferrer"&gt;Marktplaats Netherlands Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Austria&lt;/td&gt;
&lt;td&gt;willhaben.at&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/willhaben-austria-cars" rel="noopener noreferrer"&gt;willhaben Austria Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ukraine&lt;/td&gt;
&lt;td&gt;AUTO.RIA&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/autoria-ukraine-cars" rel="noopener noreferrer"&gt;AUTO.RIA Ukraine Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iceland&lt;/td&gt;
&lt;td&gt;Bilasolur.is&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/bilasolur-iceland-cars" rel="noopener noreferrer"&gt;Bilasolur Iceland Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Norway&lt;/td&gt;
&lt;td&gt;FINN.no&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/finn-norway-cars" rel="noopener noreferrer"&gt;FINN.no Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;Subito.it&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/subito-italy-cars" rel="noopener noreferrer"&gt;Subito Italy Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ireland&lt;/td&gt;
&lt;td&gt;Carzone.ie&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/carzone-ireland-cars" rel="noopener noreferrer"&gt;Carzone Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spain&lt;/td&gt;
&lt;td&gt;coches.net&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/coches-spain-cars" rel="noopener noreferrer"&gt;coches.net Spain Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romania&lt;/td&gt;
&lt;td&gt;Autovit.ro&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/autovit-romania-cars" rel="noopener noreferrer"&gt;Autovit Romania Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sweden&lt;/td&gt;
&lt;td&gt;Blocket.se&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/blocket-sweden-cars" rel="noopener noreferrer"&gt;Blocket Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estonia&lt;/td&gt;
&lt;td&gt;Auto24.ee&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/auto24-estonia-cars" rel="noopener noreferrer"&gt;Auto24 Estonia Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulgaria&lt;/td&gt;
&lt;td&gt;Cars.bg&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/carsbg-bulgaria-cars" rel="noopener noreferrer"&gt;Cars.bg Bulgaria Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Croatia&lt;/td&gt;
&lt;td&gt;Njuškalo.hr&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/DevilScrapes/njuskalo-croatia-cars" rel="noopener noreferrer"&gt;Njuškalo Car Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Bulgaria has two: Mobile.bg and Cars.bg are separate marketplaces with separate Actors, both listed above.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What we handle so the schema stays clean
&lt;/h2&gt;

&lt;p&gt;Every listing site in this fleet is a JavaScript-heavy classifieds marketplace with its own rate-limiting behavior, and several run detail-page enrichment as a second hop per listing. We rotate &lt;code&gt;curl-cffi&lt;/code&gt; browser TLS fingerprints across requests, route through Apify Proxy with a fresh session on every block, and retry on &lt;code&gt;408/429/5xx&lt;/code&gt; with exponential backoff, honoring &lt;code&gt;Retry-After&lt;/code&gt;. A search that returns genuinely zero matching listings finishes &lt;code&gt;SUCCEEDED&lt;/code&gt; with an empty dataset and a status message describing what was searched — we fail loud only when the target itself couldn't be reached or parsed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Every Actor in the fleet uses the same Pay-Per-Event structure: a &lt;strong&gt;$0.20&lt;/strong&gt; actor-start fee plus &lt;strong&gt;$2.00 per 1,000 listings&lt;/strong&gt; returned. A 500-listing pull from any single country costs &lt;strong&gt;$1.20&lt;/strong&gt;; a 1,000-listing run costs &lt;strong&gt;$2.20&lt;/strong&gt;. There's no bundled multi-country pricing tier — each Actor bills independently on Apify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on Apify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/leboncoin-france-cars" rel="noopener noreferrer"&gt;leboncoin France Car Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/autoscout24-germany-cars" rel="noopener noreferrer"&gt;AutoScout24 Germany Car Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/polovniautomobili-serbia-cars" rel="noopener noreferrer"&gt;Polovni Automobili Car Scraper (Serbia)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every new Apify account gets free trial credit, no card required — enough to pull a first batch from any country in the table above before deciding whether to add more.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do all 22 Actors return exactly the same field names?
&lt;/h3&gt;

&lt;p&gt;The core fields — &lt;code&gt;listing_id&lt;/code&gt;, &lt;code&gt;make&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;year&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;mileage_km&lt;/code&gt;, &lt;code&gt;fuel_type&lt;/code&gt;, &lt;code&gt;transmission&lt;/code&gt;, &lt;code&gt;seller_type&lt;/code&gt;, &lt;code&gt;photo_urls&lt;/code&gt;, &lt;code&gt;scraped_at&lt;/code&gt; — are identical across every Actor in the fleet. A handful of countries add fields the others don't publish (Iceland's engine size in cc, Germany's separate &lt;code&gt;engine_power_kw&lt;/code&gt;, Ukraine's VIN field), which show up as extra columns rather than breaking the shared shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why isn't price normalized to a single currency across countries?
&lt;/h3&gt;

&lt;p&gt;Because normalizing it would require us to pick an exchange-rate source and a timestamp for the conversion, and that decision belongs to whoever is building the comparison — a snapshot rate baked into our dataset would silently go stale. We return the price in the currency the listing was actually posted in, plus the ISO currency code, so you can convert with whatever rate source your use case actually needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I search by make and model, or only by URL?
&lt;/h3&gt;

&lt;p&gt;Both, depending on the country. Most Actors accept either a direct search-results URL (paste it from the marketplace and get the same filtered results back structured) or make/model/price-range filters as separate input fields. Check the individual Actor's Input table for which mode it supports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is detail-page enrichment (full description, extra photos) always included?
&lt;/h3&gt;

&lt;p&gt;It's usually an optional input flag rather than the default, since enrichment means an extra request per listing and roughly doubles the run's page-fetch cost. Turn it on when you need the full listing description or full-resolution photo set; leave it off for a fast, list-page-only pull.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if a country's marketplace changes its page layout?
&lt;/h3&gt;

&lt;p&gt;Every Actor in the fleet is checked daily by our monitoring; when a target changes its markup, we triage and ship a fix — the same maintenance model that covers all 202 of our live Actors, not something specific to the car fleet.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/leboncoin-france-cars/" rel="noopener noreferrer"&gt;leboncoin-france-cars&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/leboncoin-france-cars" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/autoscout24-germany-cars/" rel="noopener noreferrer"&gt;autoscout24-germany-cars&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/autoscout24-germany-cars" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/polovniautomobili-serbia-cars/" rel="noopener noreferrer"&gt;polovniautomobili-serbia-cars&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/polovniautomobili-serbia-cars" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/tori-finland-cars/" rel="noopener noreferrer"&gt;tori-finland-cars&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/tori-finland-cars" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/autoplius-lithuania-cars/" rel="noopener noreferrer"&gt;autoplius-lithuania-cars&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/autoplius-lithuania-cars" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/otomoto-poland-cars/" rel="noopener noreferrer"&gt;otomoto-poland-cars&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/otomoto-poland-cars" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>automotive</category>
      <category>python</category>
    </item>
    <item>
      <title>OpenCorporates Alternative: US Business Registry Data</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:05:17 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/opencorporates-alternative-us-business-registry-data-pha</link>
      <guid>https://dev.to/devil_scrapes/opencorporates-alternative-us-business-registry-data-pha</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/us-business-registry-data/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're looking for an OpenCorporates alternative because its API returns a &lt;code&gt;401&lt;/code&gt; without a paid key, the fix is simpler than it looks: the underlying US company data OpenCorporates re-publishes is, for a meaningful slice of states, already free and keyless at the source. New York, Colorado, Connecticut, and Oregon each publish their own Secretary-of-State business registry as an open Socrata dataset. We built three Actors on top of those open feeds — one for point-in-time company verification, one for a daily new-filings feed, and one for professional license rosters — so KYB analysts, SDRs, and sales-ops teams get structured rows without licensing a global registry they may not need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually needs this, and who doesn't
&lt;/h2&gt;

&lt;p&gt;Be honest about scope before you build a pipeline on it: if you need OpenCorporates' 140-plus-jurisdiction global coverage — UK, EU, Asia-Pacific entities — nothing here replaces that. These three Actors cover &lt;strong&gt;US state-level registries only&lt;/strong&gt;, currently New York, Colorado, Connecticut, and Oregon for company data, plus Delaware and Colorado for professional licenses. If your buyers, vendors, or prospect list are US companies concentrated in (or reachable through) those states, keep reading. If you need Cayman Islands holding companies, this isn't that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification Actor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/opencorporates-alternative-scraper" rel="noopener noreferrer"&gt;OpenCorporates Alternative Scraper&lt;/a&gt;&lt;/strong&gt; takes a batch of company names and checks each one against all four state registries, returning one row per matched entity per jurisdiction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Robotics LLC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entity_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20251664337"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entity_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACME ROBOTICS LLC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entity_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DLLC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Good Standing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status_normalized"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formation_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-06-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"principal_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9159 Anasazi Indian Trl, Highlands Ranch, CO 80129"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registered_agent_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACME ROBOTICS LLC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"officers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_record_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://data.colorado.gov/resource/4ykn-tg5h.json?entityid=20251664337"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scraped_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-18T12:00:00+00:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status_normalized&lt;/code&gt; is the field most buyers filter on first, because every state publishes its own raw status vocabulary — Colorado says "Good Standing," other states say something else — and we collapse that into a consistent &lt;code&gt;active&lt;/code&gt; / &lt;code&gt;inactive&lt;/code&gt; / &lt;code&gt;unknown&lt;/code&gt; enum so a downstream filter doesn't need per-state logic. Connecticut is the one jurisdiction that also publishes a separate officers dataset, so &lt;code&gt;officers&lt;/code&gt; populates there and stays &lt;code&gt;[]&lt;/code&gt; elsewhere — documented behavior, not a gap in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new-filings feed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/new-business-filings-leads-scraper" rel="noopener noreferrer"&gt;New Business Filings — US State Registry Leads Feed&lt;/a&gt;&lt;/strong&gt; answers a different question: not "does this company exist," but "what companies newly formed in a date range." Feed it a &lt;code&gt;[dateFrom, dateTo]&lt;/code&gt; window and a jurisdiction, and it returns every entity that registered in that window — company name, entity type, &lt;code&gt;formation_date&lt;/code&gt;, registered agent, and address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entity_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7969358"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entity_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URGB LLC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entity_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOMESTIC LIMITED LIABILITY COMPANY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formation_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registered_agent_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"32 Jagger Court, Melville, NY 11747"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scraped_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-19T12:00:00+00:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the shape sales and lead-gen teams actually want for outbound: a recurring, scheduled pull that surfaces brand-new LLCs and corporations the day (or week) they form, before they show up in any paid data broker's monthly refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The professional license Actor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/professional-license-lookup-scraper" rel="noopener noreferrer"&gt;Professional License Lookup Scraper&lt;/a&gt;&lt;/strong&gt; covers a related but distinct need — verifying an individual or business holds an active professional license, not that a company is registered. It exports rosters from Delaware and Colorado's open-data licensing portals across nursing, real estate, contracting, medical, and accountancy categories, with a &lt;code&gt;disciplinary_flag&lt;/code&gt; surfaced where the state publishes disciplinary action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"licensee_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SHAHAN,BRIAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS-0037427"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"profession"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Real Estate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status_normalized"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiration_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2028-04-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disciplinary_flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who buys this and why
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;KYB onboarding&lt;/strong&gt; — batch-verify a vendor or customer list before signing a contract; flag anything &lt;code&gt;inactive&lt;/code&gt; or unmatched before finance cuts a PO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sales-ops list hygiene&lt;/strong&gt; — confirm the companies on an SDR's prospect list are real, active, on-file entities before a team spends a quarter's outreach budget on dead accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead generation from new formations&lt;/strong&gt; — a recurring feed of brand-new LLCs is a standard top-of-funnel source for business insurance, accounting, and B2B SaaS sales teams who want to reach a company in its first weeks of existence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Due diligence&lt;/strong&gt; — pull incorporation date, entity type, and registered-agent data as a first-pass screen before a deeper manual review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance and licensing checks&lt;/strong&gt; — confirm a contractor, real-estate agent, or medical professional's license is active (and clean) before onboarding them as a vendor or partner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is more than "just hit the state API yourself"
&lt;/h2&gt;

&lt;p&gt;Each state runs its own Socrata dataset with its own field names, its own status vocabulary, and its own rate-limit behavior under a batch of a few hundred names in one run. New York's snapshot updates monthly, not in real time; Colorado, Connecticut, and Oregon each use different resource IDs and query parameters. None of that is hidden — these are public open-data portals — but normalizing four raw schemas into one row shape, fuzzy-matching a company name against each state's index, and handling one state's timeout without losing the other three states' results is the actual engineering work. We also deliberately never surface home addresses that Connecticut's principals dataset happens to publish alongside business addresses — a KYB dataset leaking an officer's home address because a state technically made it public is a decision we didn't want to leave to accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing — exact numbers
&lt;/h2&gt;

&lt;p&gt;All three Actors use the same Pay-Per-Event rate: &lt;strong&gt;$0.05&lt;/strong&gt; (verification and license Actors) or &lt;strong&gt;$0.20&lt;/strong&gt; (new-filings feed) actor-start fee, plus &lt;strong&gt;$5.00 per 1,000 matched rows&lt;/strong&gt;. A batch of 1,000 matched company or license rows costs &lt;strong&gt;$5.05–$5.20&lt;/strong&gt; depending on the Actor; a search that returns zero matches costs only the start fee. Apify gives every new account $5 of free trial credit, which covers roughly the first 1,000 matched rows before you spend anything real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;This is a &lt;strong&gt;four-state footprint for company data&lt;/strong&gt; (NY, CO, CT, OR) and a &lt;strong&gt;two-state footprint for licenses&lt;/strong&gt; (DE, CO) — not an 18-state or global claim. We picked these because each one publishes a genuinely free, keyless, Secretary-of-State-sourced open dataset, not a scrape of a login-gated search page. More states are a per-state adapter away, not a rewrite, and are on the roadmap. Officer data is sparse by design — only Connecticut publishes it — and freshness varies by state, with New York's feed explicitly monthly rather than real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on Apify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/opencorporates-alternative-scraper" rel="noopener noreferrer"&gt;OpenCorporates Alternative Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/new-business-filings-leads-scraper" rel="noopener noreferrer"&gt;New Business Filings — US State Registry Leads Feed&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/professional-license-lookup-scraper" rel="noopener noreferrer"&gt;Professional License Lookup Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every new Apify account starts with free trial credit, no card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is this the same data as OpenCorporates?
&lt;/h3&gt;

&lt;p&gt;No. These Actors query each state's own free, keyless Secretary-of-State open-data API directly, not opencorporates.com. They serve the same category of buyer but pull from a different — and for these specific states, more current — data source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why only four states for company verification?
&lt;/h3&gt;

&lt;p&gt;Those are the four we've confirmed publish a genuinely free, keyless Socrata dataset sourced from the state's own Secretary of State, rather than a login-gated or heavily defended search UI. Adding a state is a contained adapter, not a rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I search by entity ID instead of company name?
&lt;/h3&gt;

&lt;p&gt;Not in the current release of the verification Actor — it's name-search only. Direct ID lookup is a straightforward addition; open an issue on the Actor's page if you need it sooner.&lt;/p&gt;

&lt;h3&gt;
  
  
  How current is the new-business-filings data?
&lt;/h3&gt;

&lt;p&gt;It depends on the state's own publishing cadence — some states update closer to real time, New York's snapshot is explicitly monthly. Check the registry_url field in your results for the state's own dataset page, which usually states its refresh schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need an API key or login for any of these?
&lt;/h3&gt;

&lt;p&gt;No. All three Actors query public, keyless state open-data portals. There's no account to create, no key to request, and no login step in the Actor itself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/opencorporates-alternative-scraper/" rel="noopener noreferrer"&gt;opencorporates-alternative-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/opencorporates-alternative-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/new-business-filings-leads-scraper/" rel="noopener noreferrer"&gt;new-business-filings-leads-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/new-business-filings-leads-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/professional-license-lookup-scraper/" rel="noopener noreferrer"&gt;professional-license-lookup-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/professional-license-lookup-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>kyb</category>
      <category>leadgeneration</category>
    </item>
    <item>
      <title>Custom Web Scraper Development vs Building In-House</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 19:04:45 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/custom-web-scraper-development-vs-building-in-house-4bo1</link>
      <guid>https://dev.to/devil_scrapes/custom-web-scraper-development-vs-building-in-house-4bo1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://devilscrapes.com/blog/custom-actor-vs-in-house/" rel="noopener noreferrer"&gt;Devil Scrapes blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The honest answer to "custom web scraper development or build in-house" is: it depends on whether the target actively defends itself, and whether scraping is core to your product or a one-time data need. Below is the framework we actually use when a prospective customer asks us to build against a target we haven't already covered — total cost of ownership, blocking risk, time-to-data, and compliance — plus exactly how our commissioned-build engagement works if you decide a custom Actor is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Total cost of ownership
&lt;/h3&gt;

&lt;p&gt;The build is rarely the expensive part. A working scraper against an undefended target can be a weekend project. What isn't a weekend project is &lt;strong&gt;maintenance&lt;/strong&gt;: the target changes its markup, rotates an internal API parameter, or ships a redesign, and your scraper silently starts returning empty or wrong data until someone notices — usually a customer, not you. In-house, that's an ongoing engineering cost with no natural stopping point: someone owns "is the scraper still working" forever, on top of whatever they were hired to build. A commissioned Actor shifts that ownership to a team whose whole job is watching for exactly that kind of breakage across a portfolio of targets, which is a materially different cost curve than one engineer's part-time attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Blocking risk
&lt;/h3&gt;

&lt;p&gt;If the target has no meaningful anti-bot defenses — a plain REST API, a static site, a small internal tool — building in-house is usually the right call. The risk profile changes sharply once a target actively fingerprints clients, rate-limits aggressively, or serves different content to suspected bots. That's not a "write better code" problem; it's an ongoing arms race that needs browser-fingerprint rotation, proxy rotation, session management, and backoff tuned per target — infrastructure that's expensive to build once and genuinely expensive to keep current as the target's defenses evolve. We've run that gauntlet on 202 live targets so far; every one of them needed a different combination of the same underlying toolkit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Time-to-data
&lt;/h3&gt;

&lt;p&gt;In-house builds compete with your team's existing roadmap — a scraper is rarely the highest-priority ticket in the backlog, so "we'll get to it" often means weeks, not days. A commissioned build runs on a fixed timeline agreed at quote time, independent of your team's other priorities, because it's the only thing the person building it is doing that week.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Compliance and data handling
&lt;/h3&gt;

&lt;p&gt;Neither path removes the need to think about what you're allowed to do with scraped data — that's a legal question, not an engineering one, and it doesn't change based on who writes the code. What does change: a vendor who's built compliance-conscious defaults into their process (never scraping data gated behind a login without authorization, never surfacing personal data a public dataset happens to include when it isn't the point of the ask, documenting exactly what a scraper does and doesn't touch) saves you from re-deriving those defaults yourself on a project-by-project basis.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. When in-house wins outright
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Scraping is a &lt;strong&gt;core, differentiating&lt;/strong&gt; part of your product, not a supporting data feed — you want the expertise in-house long-term regardless of this one target.&lt;/li&gt;
&lt;li&gt;The target is genuinely simple and stable (a documented public API, a slow-moving static site).&lt;/li&gt;
&lt;li&gt;You need extremely tight coupling with proprietary internal systems that a third party can't reasonably access or shouldn't need to.&lt;/li&gt;
&lt;li&gt;You already have engineers with free capacity and the specific anti-blocking expertise this requires — which is rarer than it sounds, since it's a narrow specialty most product engineering teams don't maintain day to day.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How our commissioned-build engagement works
&lt;/h2&gt;

&lt;p&gt;If the framework above points toward "outsource this," here's the actual process, start to finish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Brief&lt;/strong&gt; — you describe the target, the fields you need, expected volume, and how fresh the data has to be. We tell you upfront if the target looks straightforward or if it's the kind that needs heavier anti-blocking investment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed quote&lt;/strong&gt; — a fixed price and timeline based on the brief, not a time-and-materials open tab. You know the cost before we write a line of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; — the same stack and standards behind our 202 live Actors: Pydantic-validated input and output, browser-fingerprint rotation, proxy rotation through Apify Proxy, retry with backoff on transient failures, and a clean typed dataset schema documented from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud QA&lt;/strong&gt; — before handover, the Actor runs a real cloud smoke test against the live target — not just a local test suite — verifying it actually reaches the target, handles a block gracefully, and produces the schema you asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover&lt;/strong&gt; — the finished Actor deploys to Apify under your account or ours, whichever you prefer. Either way you get the source, the documentation, and a working, tested Actor you can run on demand or on a schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance retainer (optional)&lt;/strong&gt; — targets change. A retainer keeps the Actor monitored and patched when the source site updates, so you're not the one who finds out it broke when a customer complains.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we bring that a from-scratch build doesn't start with
&lt;/h2&gt;

&lt;p&gt;Two Actors from our own fleet are useful reference points for the kind of target this process handles well. &lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/strong&gt; pulls structured ad-creative data from a JavaScript-heavy transparency center with no public API — 77 users and over 9,400 runs to date. &lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/youtube-transcript-scraper" rel="noopener noreferrer"&gt;YouTube Transcript Scraper &amp;amp; Bulk Downloader&lt;/a&gt;&lt;/strong&gt; bulk-extracts captions from an endpoint that isn't designed for repeated automated pulls, handling rate-limit pacing and endpoint drift so a batch of hundreds of videos doesn't fall over halfway through. Both started as exactly this kind of build: a target with no clean API, a defined schema, and a customer who needed the data on a recurring basis rather than a one-off pull.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on Apify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Google Ads Transparency Scraper&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://apify.com/DevilScrapes/youtube-transcript-scraper" rel="noopener noreferrer"&gt;YouTube Transcript Scraper &amp;amp; Bulk Downloader&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your target isn't already in our catalog, that's exactly what a commissioned build is for. Read more about the engagement at &lt;a href="https://dev.to/custom-actors/"&gt;/custom-actors/&lt;/a&gt; or reach out directly via &lt;a href="https://dev.to/contact/"&gt;/contact/&lt;/a&gt; with your target and expected volume, and we'll tell you honestly whether it's a fixed-quote build or a weekend project you can handle yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How long does a typical custom Actor build take?
&lt;/h3&gt;

&lt;p&gt;It depends entirely on the target's defenses and the schema's complexity — a straightforward site with a stable layout can turn around in days; a heavily defended or JavaScript-rendered target with a large field set takes longer. You get a specific timeline at quote time, not an estimate that shifts later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who owns the Actor after handover?
&lt;/h3&gt;

&lt;p&gt;Whichever account you specify — yours or ours. If it deploys under your Apify account, you have full control over pricing, scheduling, and access from day one. If it deploys under ours, you get run access and the same dataset output without managing an Apify account yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the target changes its site after handover?
&lt;/h3&gt;

&lt;p&gt;Without a maintenance retainer, a change to the target is your team's responsibility to catch and report, same as any in-house scraper. With a retainer, we monitor the Actor and ship a fix as part of the ongoing agreement — the same daily-monitoring model that covers all 202 of our live Actors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a minimum volume or contract length for a custom build?
&lt;/h3&gt;

&lt;p&gt;No minimum volume — the fixed quote scales with the actual scope of the brief, whether that's a single target or several. The maintenance retainer is separate and optional, and only makes sense once you've decided you want the Actor to keep working without you watching it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you build against a target that requires a login?
&lt;/h3&gt;

&lt;p&gt;We evaluate this case by case at the brief stage — some authenticated targets are reasonable to automate on your behalf with your own credentials, others raise terms-of-service or compliance questions we'll flag before quoting rather than after building.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Devil Scrapes&lt;/strong&gt; builds and maintains 200+ pay-per-result web scrapers on the &lt;a href="https://apify.com/DevilScrapes" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. Blocks, retries and proxies handled by us. Browse the &lt;a href="https://devilscrapes.com/actors/" rel="noopener noreferrer"&gt;full catalog&lt;/a&gt; or &lt;a href="https://devilscrapes.com/custom-actors/" rel="noopener noreferrer"&gt;commission a custom Actor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actors mentioned in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/google-ads-transparency/" rel="noopener noreferrer"&gt;google-ads-transparency&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/google-ads-transparency" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devilscrapes.com/actors/youtube-transcript-scraper/" rel="noopener noreferrer"&gt;youtube-transcript-scraper&lt;/a&gt; · &lt;a href="https://apify.com/DevilScrapes/youtube-transcript-scraper" rel="noopener noreferrer"&gt;Run it on Apify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>engineering</category>
      <category>python</category>
    </item>
    <item>
      <title>The US sanctions list still ships with a 1980s DOS end-of-file byte</title>
      <dc:creator>Devil Scrapes</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:26:17 +0000</pubDate>
      <link>https://dev.to/devil_scrapes/the-us-sanctions-list-still-ships-with-a-1980s-dos-end-of-file-byte-3lka</link>
      <guid>https://dev.to/devil_scrapes/the-us-sanctions-list-still-ships-with-a-1980s-dos-end-of-file-byte-3lka</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;The US Treasury's OFAC sanctions list still ships with a &lt;strong&gt;DOS end-of-file marker&lt;/strong&gt;. The last three bytes of &lt;code&gt;SDN.CSV&lt;/code&gt; are &lt;code&gt;0d 0a 1a&lt;/code&gt; — CR, LF, then &lt;code&gt;0x1A&lt;/code&gt;, the Ctrl-Z character MS-DOS used to mark end-of-text in the 1980s. Python's &lt;code&gt;splitlines()&lt;/code&gt; treats that trailing &lt;code&gt;\x1a&lt;/code&gt; as content and hands you a final one-column row that is not a record. If your parser validates column counts — and a sanctions parser absolutely should — it will reject the entire file on the last line, after correctly parsing 19,364 real ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is there a 1980s EOF byte in a live compliance feed? 🔍
&lt;/h2&gt;

&lt;p&gt;Because the file format is old and stable, and stability is the point. &lt;code&gt;SDN.CSV&lt;/code&gt; is a fixed-shape export that downstream compliance systems have consumed for decades, so nobody is going to modernise the trailer and break them. Here are the actual last bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000: 2041 4e4f 4e49 4d20 5349 524b 4554 492e   ANONIM SIRKETI.
00000010: 220d 0a1a                                 "...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That final &lt;code&gt;1a&lt;/code&gt; is the whole problem. It is invisible in a text editor, survives every naive &lt;code&gt;.strip()&lt;/code&gt; that only removes whitespace (&lt;code&gt;\x1a&lt;/code&gt; is not whitespace), and produces a phantom row precisely at the point where most parsers stop paying attention.&lt;/p&gt;

&lt;p&gt;The fix is a guard clause, not a heuristic: strip the EOF marker explicitly before splitting, and keep the strict column-count check for every remaining row. What you must not do is relax the column-count validation to make the error go away — that check is the thing standing between you and silently mis-parsing a sanctions record.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is unusual about the SDN format?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-0-&lt;/code&gt; is null.&lt;/strong&gt; OFAC does not use empty fields. A missing value is the literal three-character string &lt;code&gt;-0-&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="mf"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"AEROCARIBBEAN AIRLINES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"CUBA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the trailing space after each one, too. If you load this into a dataframe without translating the sentinel, you get a column full of the string &lt;code&gt;"-0-"&lt;/code&gt; that is not null, not empty, and will happily pass an &lt;code&gt;if value:&lt;/code&gt; check. Every one of those fields then reads as populated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The download is a redirect to a signed, expiring URL.&lt;/strong&gt; A request to the export path answers &lt;code&gt;302&lt;/code&gt; with a &lt;code&gt;Location&lt;/code&gt; header pointing at a presigned S3 object carrying &lt;code&gt;X-Amz-Expires=3600&lt;/code&gt; and a signature. You cannot cache that URL — it dies within the hour. You have to re-request the export path each time and follow the hop. Worth also &lt;em&gt;not&lt;/em&gt; forwarding your original headers onto the S3 leg: presigned URLs can reject requests carrying unexpected headers.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://apify.com/DevilScrapes/ofac-sdn-sanctions-list-scraper" rel="noopener noreferrer"&gt;OFAC SDN Sanctions List Scraper&lt;/a&gt; handles all three — the EOF marker, the &lt;code&gt;-0-&lt;/code&gt; sentinel, and the redirect chain — and joins &lt;code&gt;ALT.CSV&lt;/code&gt; (aliases) and &lt;code&gt;ADD.CSV&lt;/code&gt; (addresses) onto the primary records by entity number.&lt;/p&gt;

&lt;h2&gt;
  
  
  How big is the list, and how fast does it move?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SDN.CSV&lt;/code&gt; is roughly &lt;strong&gt;5.6 MB across about 19,365 rows&lt;/strong&gt; as of 2026-09-09, with a separate alias file of comparable size. That matters for a practical reason people discover in production rather than in testing: if you pull the primary list, the aliases and the addresses concurrently through a single network egress, they share bandwidth, and a per-request timeout tuned on a laptop will be far too tight. A 30-second total timeout that passes locally every time will die in a datacenter having received 991,217 of 1,062,698 bytes — 93% of the way through the smallest of the three files.&lt;/p&gt;

&lt;p&gt;The lesson generalises past this API: for multi-megabyte concurrent downloads, a &lt;em&gt;total&lt;/em&gt; request timeout is not the same knob as a connect timeout, and the number you need is set by your slowest egress, not your fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need an API key?&lt;/strong&gt;&lt;br&gt;
No. The OFAC sanctions list is published by the US Treasury as open data, with no key, no registration and no IP whitelisting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is screening a name against the SDN list enough for compliance?&lt;/strong&gt;&lt;br&gt;
No, and please do not treat it as such. The SDN list is one list among several — consolidated non-SDN lists, EU and UK regimes, and local requirements all exist. Fuzzy name matching also produces both false positives and false negatives, especially across transliterated names, which is why match scores need human review rather than being wired straight into a block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do aliases live in a separate file?&lt;/strong&gt;&lt;br&gt;
Because one entity can have many. &lt;code&gt;ALT.CSV&lt;/code&gt; maps alias records back to the primary entity by &lt;code&gt;ent_num&lt;/code&gt;, so a single sanctioned individual can appear under a dozen spellings. Screening only against primary names in &lt;code&gt;SDN.CSV&lt;/code&gt; will miss the alias that a counterparty actually uses — which is, in practice, the whole reason alias data exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How current is the file?&lt;/strong&gt;&lt;br&gt;
OFAC republishes on change, and the presigned URL path includes a dated segment. Treat the list as something to re-pull rather than cache; a stale sanctions list is worse than no sanctions list, because it feels like diligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A 5.6 MB CSV, a 1980s EOF byte on the last line, &lt;code&gt;-0-&lt;/code&gt; where a null should be, and a one-hour signed URL in front of it. None of these are hard once you know; all of them are silent when you don't.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Byte-level details in this post were verified against &lt;code&gt;sanctionslistservice.ofac.treas.gov&lt;/code&gt; on 2026-09-09.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>opendata</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
