<?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: Tim Zinin</title>
    <description>The latest articles on DEV Community by Tim Zinin (@timmyzinin).</description>
    <link>https://dev.to/timmyzinin</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%2F3781639%2F85dfa3d1-9342-4dfe-8414-a9da0b6fb2a9.png</url>
      <title>DEV Community: Tim Zinin</title>
      <link>https://dev.to/timmyzinin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/timmyzinin"/>
    <language>en</language>
    <item>
      <title>How to Scrape PropertyFinder.ae Listings in the Gulf Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-scrape-propertyfinderae-listings-in-the-gulf-without-a-paid-api-44e2</link>
      <guid>https://dev.to/timmyzinin/how-to-scrape-propertyfinderae-listings-in-the-gulf-without-a-paid-api-44e2</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Gulf property data is a licensing problem before it is a technical one. Listings live behind portals like PropertyFinder.ae, and the organizations that legitimately hold the data — licensed data providers, brokerages with active mandates, owners, property managers — receive it as exports, not APIs. Before those records can enter a CRM, a warehouse, or a review queue, someone has to normalize the fields, deduplicate them, attach provenance and freshness, and produce an audit trail that answers "who gave us this row, when, and under what rights?"&lt;/p&gt;

&lt;p&gt;The paid alternatives are portal-side scraping subscriptions, which put you on the wrong side of both the portal's terms and your own compliance team. If you already hold an authorized export, what you need is a deterministic normalization and evidence layer — not another crawler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The PropertyFinder.ae UAE Real Estate Listings actor (slug &lt;code&gt;propertyfinder-gulf&lt;/code&gt;, kept for existing Tasks) is the Authorized Gulf Property Listing Intelligence layer. Two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Export mode (default, &lt;code&gt;sourceMode: "export"&lt;/code&gt;).&lt;/strong&gt; Fully network-free. You submit one to 100 closed property records you own or are licensed to process, and the actor normalizes them into review-ready evidence rows. Zero external-source requests: no portal page, no recorded URL fetch, no browser, proxy, API, DNS, robots, or sitemap call. Descriptions, photos, contact fields, and free text are deliberately rejected by the closed contract — unknown fields invalidate the record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live mode (&lt;code&gt;sourceMode: "live"&lt;/code&gt;).&lt;/strong&gt; Exactly one bounded request to an allowlisted public &lt;code&gt;www.propertyfinder.ae&lt;/code&gt; results URL you supply, with a 15-second deadline, redirect/challenge/2 MB rejection, reading at most 100 &lt;code&gt;RealEstateListing&lt;/code&gt; schema.org JSON-LD nodes. Live rows are marked &lt;code&gt;property_listing_live&lt;/code&gt; with &lt;code&gt;sourceLicense: null&lt;/code&gt; — public visibility is not a licence or proof of availability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per the README, each delivered row carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The legacy-compatible listing fields (&lt;code&gt;url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;deal_type&lt;/code&gt;, &lt;code&gt;property_type&lt;/code&gt;, &lt;code&gt;rooms&lt;/code&gt;, &lt;code&gt;area_sqm&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;lat&lt;/code&gt;/&lt;code&gt;lng&lt;/code&gt;, &lt;code&gt;posted_date&lt;/code&gt;) plus a deterministic &lt;code&gt;stableId&lt;/code&gt; (SHA-256 over source name plus listing ID) for joins.&lt;/li&gt;
&lt;li&gt;Provenance and rights context: buyer-supplied &lt;code&gt;sourceName&lt;/code&gt;, &lt;code&gt;sourceLicense&lt;/code&gt;, optional &lt;code&gt;sourceRetrievedAt&lt;/code&gt;, and the exact authorization attestation recorded as an operational gate — not legal proof.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;freshness&lt;/code&gt; (fresh/aging/stale/unknown) computed only from your supplied retrieval time, &lt;code&gt;change&lt;/code&gt; always &lt;code&gt;not_measured&lt;/code&gt; (a single snapshot proves nothing), evidence-sufficiency &lt;code&gt;confidence&lt;/code&gt; with explicit &lt;code&gt;dataGaps&lt;/code&gt;, and mechanical &lt;code&gt;pricePerSqm&lt;/code&gt; when area is supplied — explicitly separated from valuation or investment advice.&lt;/li&gt;
&lt;li&gt;A mandatory human route: &lt;code&gt;recommendedAction&lt;/code&gt;, review &lt;code&gt;priority&lt;/code&gt; (HIGH/MEDIUM/LOW from freshness and locality coverage), and &lt;code&gt;safeToAutomate&lt;/code&gt; always false.&lt;/li&gt;
&lt;li&gt;Settlement-neutral &lt;code&gt;billing&lt;/code&gt; intent on the row, with the authoritative paid/free/withheld/unknown reconciliation in the current-run KVS &lt;code&gt;OUTPUT&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deduplication is scoped to lowercase &lt;code&gt;sourceName&lt;/code&gt; plus &lt;code&gt;listingId&lt;/code&gt; and never merges across sources. Legacy search-style inputs (&lt;code&gt;deal_type&lt;/code&gt;, &lt;code&gt;emirate&lt;/code&gt;, &lt;code&gt;max_items&lt;/code&gt;, &lt;code&gt;max_pages&lt;/code&gt;, &lt;code&gt;max_listing_age_days&lt;/code&gt;) are migration-only: one free diagnostic, no network access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;The documented input (from the manifest and README):&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;"sourceMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I confirm I am authorized to process and deliver these property records"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider_licensed_export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listings"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"listingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"listing-101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"listingUrl"&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://inventory.example.com/listing-101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Two-bedroom apartment in Dubai Marina"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2450000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dealType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"propertyType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Apartment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bedrooms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"areaSqm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;128.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dubai Marina"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Licensed Gulf property export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceLicense"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buyer confirms downstream processing and delivery rights"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceRetrievedAt"&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-08-12T12:00:00Z"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README's happy run-bound example shows the KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt for that one paid row (trimmed):&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;"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;"SUCCEEDED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&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;"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;"COMPLETE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"requestedCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uniqueCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duplicateCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"invalidCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"run"&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="nl"&gt;"deliveredRowCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"paidRowCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"freeRowCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"withheldRowCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"replaySafe"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"safeToAutomate"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"delivery"&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="nl"&gt;"eventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"result-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmedEventDelta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmedDatasetWrites"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lastAttempt"&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="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"confirmed_paid"&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="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;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per actor start plus $0.003 per delivered authorized row (&lt;code&gt;result-found&lt;/code&gt;), with tiered discounts on paid Apify plans (the Store headline works out to $2.55 per 1,000 rows). The README computes start plus ten paid rows at $0.035; at list prices a 100-record run costs $0.005 + 100 × $0.003 = $0.305. Apify's free plan includes $5 of usage credits per month, which covers about 16 full 100-record runs — roughly 1,600 delivered rows — before you pay anything. Legacy migration and invalid-record diagnostics are free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Normalize one authorized record first, reconcile the Dataset against the KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt, then scale the batch: &lt;a href="https://apify.com/zinin/propertyfinder-gulf?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=propertyfinder-gulf" rel="noopener noreferrer"&gt;PropertyFinder.ae UAE Real Estate Listings&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, and the README includes an Apify MCP server setup plus an "MCP or agent workflow" recipe: treat the actor as a deterministic normalization tool — an agent may propose a review task but cannot infer source authority, ownership, valuation, availability, or permission to contact. Integrations must read the current-run KVS &lt;code&gt;OUTPUT&lt;/code&gt; as a separate run fact, never copy a final paid/free state into the pre-settlement Dataset row, and never blind-retry an unknown delivery.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>realestate</category>
    </item>
    <item>
      <title>How to Export YouTube Playlists Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Fri, 02 Oct 2026 07:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-export-youtube-playlists-without-a-paid-api-1o52</link>
      <guid>https://dev.to/timmyzinin/how-to-export-youtube-playlists-without-a-paid-api-1o52</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Playlists are the unit of curation on YouTube — a competitor's "best of" collection, a course creator's syllabus, a brand's campaign list — and getting their contents into a spreadsheet means scrolling a page, copying titles, and losing the ordering that makes a playlist useful. The official YouTube Data API will hand you playlist items, but only after a Google Cloud project, an API key, and quota management you should not need for a read-only, one-off export.&lt;/p&gt;

&lt;p&gt;A hand-rolled scraper has subtler traps. A playlist page returns at most 100 videos per request, so a naive parser silently presents a truncated list as complete. YouTube sometimes renders a video entry with no title at all. And a well-formed but nonexistent playlist id answers with HTTP 200 and zero videos — a silent-empty result that looks identical to success unless you know to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The YouTube Playlist Scraper takes any public playlist — the &lt;code&gt;list=&lt;/code&gt; value or a full &lt;code&gt;youtube.com/playlist?list=...&lt;/code&gt; URL, including playlists you do not own — and returns one flat JSON row per video: &lt;code&gt;videoId&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;viewCountText&lt;/code&gt;, and &lt;code&gt;position&lt;/code&gt;. No API key, no login, no browser; one GET request per playlist.&lt;/p&gt;

&lt;p&gt;From the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any playlist, no channel resolution.&lt;/strong&gt; It accepts any &lt;code&gt;list=&lt;/code&gt; value and never looks up channels or builds uploads-playlist ids — if all you have is a channel handle, the README points to the sibling &lt;code&gt;youtube-channel-videos-list&lt;/code&gt; actor instead. Up to 50 playlists per run, duplicates deduplicated before any request, concurrency 1–20 (default 5).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One flat 13-field row regardless of outcome&lt;/strong&gt;: &lt;code&gt;requestedPlaylist&lt;/code&gt;, &lt;code&gt;normalizedPlaylistUrl&lt;/code&gt;, &lt;code&gt;videoId&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;viewCountText&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, &lt;code&gt;partial&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;checkedAt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest status boundary.&lt;/strong&gt; &lt;code&gt;status:"ok"&lt;/code&gt; and &lt;code&gt;status:"partial"&lt;/code&gt; are the billed, video-bearing outcomes. A well-formed but nonexistent playlist returns &lt;code&gt;status:"empty"&lt;/code&gt; with &lt;code&gt;error:null&lt;/code&gt; (HTTP 200, genuinely zero videos); a malformed one returns &lt;code&gt;status:"error"&lt;/code&gt; naming YouTube's own HTTP 404. Both are free, and the HTTP status is checked before any body parsing so the two can never be confused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A measured 100-item ceiling, disclosed.&lt;/strong&gt; When a page returns exactly 100 videos, every row is flagged &lt;code&gt;partial:true&lt;/code&gt; with &lt;code&gt;action:"flag-possible-truncation"&lt;/code&gt; — delivered and billed, but honestly marked as a possible sample rather than the complete list. The actor never paginates past the first page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing titles are data, not failures.&lt;/strong&gt; A &lt;code&gt;title:null&lt;/code&gt; row still ships as &lt;code&gt;ok&lt;/code&gt;/&lt;code&gt;partial&lt;/code&gt; with &lt;code&gt;action:"flag-missing-title"&lt;/code&gt;, so you keep the video id and decide what to do with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View counts are display text.&lt;/strong&gt; Every video row carries &lt;code&gt;confidence:"display-text"&lt;/code&gt; as a standing reminder that &lt;code&gt;viewCountText&lt;/code&gt; (&lt;code&gt;"2.6M views"&lt;/code&gt;) is YouTube's own rounded string, not an exact counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardened fetch layer.&lt;/strong&gt; Host guard (only the three YouTube hosts), DNS-rebinding-safe SSRF protection with pinned addresses, byte caps, and manual redirect re-checks; every request sends the standard consent cookie and &lt;code&gt;hl=en&amp;amp;gl=US&lt;/code&gt; so English parsing cannot silently return empty. No transcripts or comments, ever — the paths serving those are disallowed by YouTube's own &lt;code&gt;robots.txt&lt;/code&gt; and never requested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each run also writes a one-time KVS &lt;code&gt;OUTPUT&lt;/code&gt; roll-up: requested/delivered/paid/free/failed counts, replay safety, and any fatal error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;The prefilled default input is a real, verified 10-video playlist:&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;"playlists"&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="s2"&gt;"PLBCF2DAC6FFB574DE"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;The README's real happy-path row (position 1 of that playlist, verbatim):&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;"requestedPlaylist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PLBCF2DAC6FFB574DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"normalizedPlaylistUrl"&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://www.youtube.com/playlist?list=PLBCF2DAC6FFB574DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GvgqDSnpRQM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Andrew Willis, Skatepark Engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"viewCountText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.6M views"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playlist-page"&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"display-text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partial"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ingest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkedAt"&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-08-17T20:17:57.854Z"&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;A nonexistent id such as &lt;code&gt;PLBCF2DAC6FFB574DF&lt;/code&gt; comes back as one free summary row — &lt;code&gt;status:"empty"&lt;/code&gt;, &lt;code&gt;videoId:null&lt;/code&gt;, &lt;code&gt;action:"skip-not-found"&lt;/code&gt; — and a malformed value comes back &lt;code&gt;status:"error"&lt;/code&gt; quoting the 404. Both cost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per actor start plus $0.001 per delivered video (&lt;code&gt;result-found&lt;/code&gt;), with lower per-event prices on paid Apify tiers (the Store headline works out to $0.85 per 1,000 delivered videos). At list prices, a full 100-video export costs $0.005 + 100 × $0.001 = $0.105. Apify's free plan includes $5 of usage credits per month, which covers about 47 full 100-video runs — roughly 5,000 delivered video rows — before you pay anything. Empty, error, and blocked-host rows are never billed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Run the prefilled playlist with zero edits to see the real row shape, then feed your own list: &lt;a href="https://apify.com/zinin/youtube-playlist-scraper?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=youtube-playlist-scraper" rel="noopener noreferrer"&gt;YouTube Playlist Scraper&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, and the README documents running it from an MCP client plus an agent/MCP pattern. The agent contract is two rules: branch on &lt;code&gt;status&lt;/code&gt; (never on &lt;code&gt;error&lt;/code&gt; alone — an &lt;code&gt;empty&lt;/code&gt; row correctly has &lt;code&gt;error:null&lt;/code&gt;), and treat &lt;code&gt;partial&lt;/code&gt;/&lt;code&gt;action&lt;/code&gt; as machine-readable caveats — &lt;code&gt;flag-possible-truncation&lt;/code&gt; means "sample of the first 100," and &lt;code&gt;flag-missing-title&lt;/code&gt; on an otherwise &lt;code&gt;ok&lt;/code&gt; row is a valid outcome to keep, not a parse failure to retry.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>youtube</category>
    </item>
    <item>
      <title>How to See Which Companies Are Hiring Right Now Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:25:04 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-see-which-companies-are-hiring-right-now-without-a-paid-api-22ei</link>
      <guid>https://dev.to/timmyzinin/how-to-see-which-companies-are-hiring-right-now-without-a-paid-api-22ei</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;"Which companies are hiring right now?" is the starting question for sales prospecting, recruiting, and competitive research — and answering it manually means opening a company's careers page, finding their ATS, and counting roles by hand for every account on your list. Commercial intent platforms will sell you an answer, but it arrives as a black-box score with no source evidence attached, at a price that only makes sense for a funded sales team.&lt;/p&gt;

&lt;p&gt;The underlying data is actually public. Companies on Greenhouse, Lever, or Ashby publish their open roles on unauthenticated job-board endpoints that anyone can read. The work is in turning those raw feeds into a consistent, comparable signal across dozens of boards — and in being honest about what a job posting does and does not prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The Company Hiring Radar reads the public Greenhouse, Lever, and Ashby board APIs for tokens you supply and returns one normalized, evidence-backed observation per board. No login, no ATS API key, no proxies.&lt;/p&gt;

&lt;p&gt;From the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit or auto-detected boards.&lt;/strong&gt; Best input form is &lt;code&gt;provider:token&lt;/code&gt; — &lt;code&gt;greenhouse:stripe&lt;/code&gt;, &lt;code&gt;lever:spotify&lt;/code&gt;, &lt;code&gt;ashby:ramp&lt;/code&gt; — where the token is the slug in the company's careers URL. A bare token (&lt;code&gt;stripe&lt;/code&gt;) probes all three providers and picks the board with the most open roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest identity handling.&lt;/strong&gt; A token is not a company. Bare-token rows disclose the picked board's declared name (&lt;code&gt;ownerName&lt;/code&gt;/&lt;code&gt;ownerWebsite&lt;/code&gt;) and any other real boards found under the same token (&lt;code&gt;candidates&lt;/code&gt;, with &lt;code&gt;sameName&lt;/code&gt; flags). The README's live examples make the point: bare &lt;code&gt;pulse&lt;/code&gt; resolves to Pulse Healthcare on Greenhouse (2,615 roles) while an unrelated &lt;code&gt;runpulse.com&lt;/code&gt; sits on Ashby (9 roles), and bare &lt;code&gt;juno&lt;/code&gt; matches two genuinely different boards both named "Juno". Explicit &lt;code&gt;provider:token&lt;/code&gt; input has nothing to guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalized hiring signals per board&lt;/strong&gt;: total open roles, a plain-English activity band (&lt;code&gt;light&lt;/code&gt; / &lt;code&gt;active&lt;/code&gt; / &lt;code&gt;strong&lt;/code&gt; / &lt;code&gt;aggressive hiring&lt;/code&gt;, derived only from the current posting count), role counts by function (sales, marketing, engineering, product, leadership, data, ops, people, finance, support, legal), remote share, a full by-location breakdown, and how many roles carry a parseable posting timestamp inside your &lt;code&gt;newWindowDays&lt;/code&gt; window (default 30).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword highlights&lt;/strong&gt; in &lt;code&gt;matchedRoles&lt;/code&gt; — only the roles whose title, department, or function contains your keywords — plus the full normalized role list (title, function, location, remote flag, posted date with its &lt;code&gt;postedAtBasis&lt;/code&gt;, apply URL) in one shape across all three providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision context on every row&lt;/strong&gt;: stable &lt;code&gt;entityId&lt;/code&gt;/&lt;code&gt;observationId&lt;/code&gt;/&lt;code&gt;eventId&lt;/code&gt;, &lt;code&gt;evidence[]&lt;/code&gt; with provider URLs and HTTP status, &lt;code&gt;evidenceCoverage&lt;/code&gt;, confidence score with reasons, explicit &lt;code&gt;dataGaps&lt;/code&gt;, a conservative &lt;code&gt;recommendedAction&lt;/code&gt;, and &lt;code&gt;safeToAutomate: false&lt;/code&gt;. A run-level KVS &lt;code&gt;OUTPUT&lt;/code&gt; reconciles requested, unique, delivered, paid, free-failure, withheld, and replay-safe state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README keeps the interpretation boundary in the data: open postings are research evidence — they do not prove headcount growth, budget, expansion, purchase intent, or permission to contact anyone. A clean 404 (no board) is a free row, distinct from a source failure, which is also free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;Input from the manifest:&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;"companies"&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="s2"&gt;"greenhouse:stripe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lever:spotify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ashby:ramp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&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="s2"&gt;"sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marketing"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README's real output row for &lt;code&gt;ashby:ramp&lt;/code&gt;, trimmed:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ashby:ramp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ashby"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matchBasis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explicit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"signals"&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;"totalOpenRoles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hiringMomentum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aggressive hiring"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"salesHiring"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"marketingHiring"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"engineeringHiring"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"remoteShare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.93&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"newRolesLast30d"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&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;"matchedKeywords"&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="s2"&gt;"sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marketing"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ramp (ashby) — 118 open roles, aggressive hiring. Top: sales 46, engineering 30, marketing 16. 93% remote, 63 match your keywords, 28 new in last 30d."&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;Each of the 118 roles also appears in the normalized &lt;code&gt;roles&lt;/code&gt; list with title, function, location, remote flag, &lt;code&gt;postedAt&lt;/code&gt;, &lt;code&gt;postedAtBasis&lt;/code&gt;, and apply URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per run start plus $0.002 per successfully delivered company-board report (&lt;code&gt;result-found&lt;/code&gt;) — one charge per board regardless of whether it contains 0, 10, or 500 roles, with lower per-event prices on paid Apify tiers. At list prices, a 100-board run costs $0.005 + 100 × $0.002 = $0.205. Apify's free plan includes $5 of usage credits per month, covering about 24 full 100-board runs — roughly 2,500 board reports — before you pay anything. Clean not-found and source-failure rows are never billed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Paste three board tokens from careers pages you want to watch, check the &lt;code&gt;candidates&lt;/code&gt; disclosures on any bare-token row, then schedule the watchlist: &lt;a href="https://apify.com/zinin/company-hiring-radar?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=company-hiring-radar" rel="noopener noreferrer"&gt;Company Hiring Radar&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, and the README notes it is callable from the Apify API, the SDK, or the Apify MCP server. The integration guidance is written for agents: join rows across runs on &lt;code&gt;entityId&lt;/code&gt;, gate downstream automation on &lt;code&gt;OUTPUT.status === "COMPLETE"&lt;/code&gt; and &lt;code&gt;replaySafe === true&lt;/code&gt;, keep &lt;code&gt;evidence&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, and &lt;code&gt;dataGaps&lt;/code&gt; attached so a bounded observation never becomes an overclaim, and treat &lt;code&gt;matchedRoles&lt;/code&gt; as a human research queue — row-level &lt;code&gt;safeToAutomate&lt;/code&gt; is always false.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>jobs</category>
    </item>
    <item>
      <title>How to Query Himalayas Remote Jobs as an API Without a Paid Plan</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Thu, 01 Oct 2026 16:25:04 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-query-himalayas-remote-jobs-as-an-api-without-a-paid-plan-1fkb</link>
      <guid>https://dev.to/timmyzinin/how-to-query-himalayas-remote-jobs-as-an-api-without-a-paid-plan-1fkb</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Remote-job discovery is oddly hard to do programmatically on a budget. Job boards want you on their website, and the "API" options are either enterprise-priced feeds or undocumented endpoints that break without warning. If you are a recruiter building a shortlist, a developer feeding a jobs database, or an automation builder who needs structured role facts — title, company, salary, location eligibility, application link — scraping individual job pages is fragile, slow, and rude to the source.&lt;/p&gt;

&lt;p&gt;Himalayas.app is one of the friendlier sources: it publishes an official, public, no-auth JSON API and explicitly permits its use in applications, tools, databases, and job-board operators, with visible attribution. The only thing standing between that endpoint and your workflow is the plumbing: bounded requests, validated responses, clean rows, and honest limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The Himalayas Remote Jobs API actor turns one bounded Himalayas search into up to 20 clean, attributed job rows in an Apify Dataset. It is not a scraper — it sends exactly one request to the official &lt;code&gt;/jobs/api/search&lt;/code&gt; endpoint per run and never touches a job page, never walks additional pages, and never submits an application.&lt;/p&gt;

&lt;p&gt;From the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predictable one-request runs.&lt;/strong&gt; You select exactly one 1-based result page (&lt;code&gt;page&lt;/code&gt;, 1–100); the actor does not paginate further. A successful run writes zero to 20 rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full filter set&lt;/strong&gt;: &lt;code&gt;query&lt;/code&gt; (default &lt;code&gt;software engineer&lt;/code&gt;), optional &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;worldwide&lt;/code&gt; / &lt;code&gt;excludeWorldwide&lt;/code&gt; (mutually exclusive), &lt;code&gt;seniority&lt;/code&gt; (up to 6 levels), &lt;code&gt;employmentTypes&lt;/code&gt; (up to 7), &lt;code&gt;companySlugs&lt;/code&gt; (up to 10 canonical slugs like &lt;code&gt;linear&lt;/code&gt;), &lt;code&gt;timezone&lt;/code&gt; (e.g. &lt;code&gt;UTC-5&lt;/code&gt;), and &lt;code&gt;sort&lt;/code&gt; (&lt;code&gt;relevant&lt;/code&gt;, &lt;code&gt;recent&lt;/code&gt;, &lt;code&gt;salaryAsc&lt;/code&gt;, &lt;code&gt;salaryDesc&lt;/code&gt;, &lt;code&gt;nameAToZ&lt;/code&gt;, &lt;code&gt;nameZToA&lt;/code&gt;, &lt;code&gt;jobs&lt;/code&gt;). Unknown fields and contradictory filter combinations are rejected before the source is contacted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 factual fields per row&lt;/strong&gt;: role title, company name and slug, employment type, disclosed salary range (&lt;code&gt;minSalary&lt;/code&gt;, &lt;code&gt;maxSalary&lt;/code&gt;, &lt;code&gt;salaryPeriod&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;), seniority, &lt;code&gt;locationRestrictions&lt;/code&gt; and &lt;code&gt;timezoneRestrictions&lt;/code&gt;, &lt;code&gt;categories&lt;/code&gt; and &lt;code&gt;parentCategories&lt;/code&gt;, &lt;code&gt;publishedAt&lt;/code&gt;, &lt;code&gt;expiresAt&lt;/code&gt;, the original &lt;code&gt;applicationLink&lt;/code&gt;, and provenance (&lt;code&gt;source&lt;/code&gt;, &lt;code&gt;attributionUrl&lt;/code&gt;, &lt;code&gt;sourceRequestUrl&lt;/code&gt;, &lt;code&gt;fetchedAt&lt;/code&gt;, &lt;code&gt;guid&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribution by design.&lt;/strong&gt; Himalayas permits the API for tools and databases while requiring visible attribution, so every row carries &lt;code&gt;source: "Himalayas"&lt;/code&gt;, the &lt;code&gt;attributionUrl&lt;/code&gt;, and the original application link. Employer descriptions, excerpts, and logos are deliberately excluded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed security posture.&lt;/strong&gt; The only upstream host is &lt;code&gt;himalayas.app&lt;/code&gt; on the fixed search path, with a 15-second timeout and a 2 MiB body limit; redirects, non-JSON responses, rate limits, incomplete records, schema drift, and non-Himalayas application links all fail closed instead of returning uncertain data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honest limits are documented: Himalayas caches its API data and refreshes roughly every 24 hours, so frequent polling adds no freshness; a valid query can return zero rows; 20 is a maximum, not a target; and salary, seniority, and eligibility values are source facts, not independently verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;The manifest's example input runs a broad recent-software search:&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;"software engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"worldwide"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"excludeWorldwide"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seniority"&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;"employmentTypes"&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;"companySlugs"&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;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;The README's illustrative output shape (it labels the company and values as fictional) shows all 21 fields:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Himalayas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attributionUrl"&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://himalayas.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceRequestUrl"&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://himalayas.app/jobs/api/search?q=software+engineer&amp;amp;sort=recent&amp;amp;page=1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fetchedAt"&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-09-01T10:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"guid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"illustrative-guid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Senior Software Engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example 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;"companySlug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-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;"employmentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full Time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minSalary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxSalary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryPeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seniority"&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="s2"&gt;"Senior"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locationRestrictions"&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="s2"&gt;"Worldwide"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timezoneRestrictions"&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;"categories"&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="s2"&gt;"Software Development"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parentCategories"&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="s2"&gt;"Engineering"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishedAt"&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-08-25T10:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicationLink"&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://himalayas.app/companies/example-company/jobs/illustrative-role"&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;Null salary or date values mean the source did not disclose that fact — they are not zero — and the README advises inspecting &lt;code&gt;locationRestrictions&lt;/code&gt; and &lt;code&gt;timezoneRestrictions&lt;/code&gt; together before treating a role as eligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.013 per actor start plus $0.001 per delivered job row (&lt;code&gt;result-found&lt;/code&gt;), charged only after each row is successfully stored; a source failure or invalid response cannot create a paid event. A full 20-job run therefore costs $0.033 at list prices — the same cap the README's public example Task uses. Apify's free plan includes $5 of usage credits per month, which covers about 151 full 20-job runs — roughly 3,000 delivered job rows — before anything is charged. A zero-result run pays only the $0.013 start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Run the public example Task preset or call the actor with your own filters: &lt;a href="https://apify.com/zinin/himalayas-remote-jobs-api?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=himalayas-remote-jobs-api" rel="noopener noreferrer"&gt;Himalayas Remote Jobs API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, so agents can call it directly with &lt;code&gt;apify-client&lt;/code&gt; or plain HTTP, and the README ships an Apify MCP server setup for it. Himalayas also publishes its own AI-agent integration guide, and the actor is built for exactly that pattern: one bounded request, attributed factual rows, original application links for human review, and no page scraping — an agent gets clean job metadata it can branch on without ever touching an employer page.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>jobs</category>
    </item>
    <item>
      <title>How to Scrape Spotify Artist, Album and Playlist Data Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-scrape-spotify-artist-album-and-playlist-data-without-a-paid-api-2m00</link>
      <guid>https://dev.to/timmyzinin/how-to-scrape-spotify-artist-album-and-playlist-data-without-a-paid-api-2m00</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Music analytics, A&amp;amp;R research, and playlist campaign tracking all start with the same humble need: read the public card for a Spotify artist, album, or playlist — the name, the monthly-listener figure, the release year, the track count, the item and save counts. Doing that by hand means opening every page in a browser and re-typing numbers into a spreadsheet, which does not scale past a handful of targets.&lt;/p&gt;

&lt;p&gt;The programmatic alternatives are poor fits for a small watchlist. The Spotify Web API requires an app, OAuth tokens, and approval, and it meters you into quotas you do not need for a dozen pages. A naive scraper has a worse problem, and it is counterintuitive: Spotify's card pages only render their structured data server-side for a plain, non-browser request. Send a real browser User-Agent and you get HTTP 200 with an empty client-side shell — about 156 KB of "Spotify – Web Player" with zero data in it. A confident-looking response containing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The Spotify Artist, Album &amp;amp; Playlist Scraper takes full Spotify URLs you already have — &lt;code&gt;https://open.spotify.com/artist/&amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;/album/&amp;lt;id&amp;gt;&lt;/code&gt;, or &lt;code&gt;/playlist/&amp;lt;id&amp;gt;&lt;/code&gt; — and reads the server-rendered &lt;code&gt;application/ld+json&lt;/code&gt; block each page ships, with one plain non-browser GET per target. No login, no API key, no Web Playback SDK, no browser runtime.&lt;/p&gt;

&lt;p&gt;From the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The non-browser rule is the product.&lt;/strong&gt; The actor always sends its own declared, non-browser User-Agent — never a browser signature — because that is the only request shape Spotify answers with data. Every row carries a &lt;code&gt;requestUserAgent&lt;/code&gt; field echoing the literal header sent, so you can verify the discipline on your own runs; an acceptance test fails the build if that header ever becomes a browser string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type comes from the URL path&lt;/strong&gt;, never guessed: artist rows return name and Spotify's displayed monthly-listener figure; album rows return name, release type, release year, and track count; playlist rows return name, item count, and save count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One flat 15-field row per target&lt;/strong&gt;, regardless of outcome: &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;found&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;spotifyId&lt;/code&gt;, &lt;code&gt;canonicalUrl&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;releaseType&lt;/code&gt;, &lt;code&gt;releaseYear&lt;/code&gt;, &lt;code&gt;trackCount&lt;/code&gt;, &lt;code&gt;monthlyListeners&lt;/code&gt;, &lt;code&gt;itemCount&lt;/code&gt;, &lt;code&gt;savesCount&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;requestUserAgent&lt;/code&gt;, &lt;code&gt;checkedAt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest negatives, free of charge.&lt;/strong&gt; A nonexistent object comes back as Spotify's own genuine HTTP 404 (&lt;code&gt;found:false&lt;/code&gt;, &lt;code&gt;error:"http 404"&lt;/code&gt;), not a silent-empty page. A non-Spotify host is refused before any request is made (&lt;code&gt;requestUserAgent:null&lt;/code&gt; proves nothing was sent). An HTTP 200 with no usable &lt;code&gt;ld+json&lt;/code&gt; block is reported, never guessed at. Only &lt;code&gt;found:true&lt;/code&gt; rows are billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disclosed source instability.&lt;/strong&gt; Playlist &lt;code&gt;itemCount&lt;/code&gt;/&lt;code&gt;savesCount&lt;/code&gt; can legitimately come back &lt;code&gt;null&lt;/code&gt; on the same URL across repeated fetches — the README measured three of five back-to-back fetches carrying the structured counts and two not. The actor reports the null honestly instead of inventing a value.&lt;/li&gt;
&lt;li&gt;Up to 50 targets per run, concurrency 1–15 (default 5), plus a one-time KVS &lt;code&gt;OUTPUT&lt;/code&gt; run summary with requested/delivered/paid/free/failed counts and replay safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limits are stated plainly: no search by name, no track lists, no audio, no full discography, no pagination of playlist contents — the card-level summary only. &lt;code&gt;monthlyListeners&lt;/code&gt; and &lt;code&gt;savesCount&lt;/code&gt; are Spotify's own rounded display text (&lt;code&gt;"100.9M"&lt;/code&gt;), not exact internal numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;Input is one required field:&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;"targets"&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="s2"&gt;"https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;The README's real happy-path row for that artist URL, verbatim:&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;"input"&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://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"artist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spotifyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"06HL4z0CvFAxyc27GXpf02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"Taylor Swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monthlyListeners"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"100.9M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestUserAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (compatible; spotify-artist-intel/0.1; +https://apify.com/zinin/spotify-artist-intel)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkedAt"&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-08-17T20:32:57.777Z"&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;An album row in the same run returned &lt;code&gt;name: "After Hours"&lt;/code&gt;, &lt;code&gt;releaseType: "album"&lt;/code&gt;, &lt;code&gt;releaseYear: 2020&lt;/code&gt;, &lt;code&gt;trackCount: 14&lt;/code&gt;; a playlist row returned &lt;code&gt;name: "Today's Top Hits"&lt;/code&gt;, &lt;code&gt;itemCount: 50&lt;/code&gt;, &lt;code&gt;savesCount: "33.9M"&lt;/code&gt; (with a repeat fetch showing both counts as honest &lt;code&gt;null&lt;/code&gt;s).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per actor start plus $0.002 per delivered card (&lt;code&gt;result-found&lt;/code&gt;); the README computes 100 delivered cards at about $0.205 including the start fee. A run of 50 URLs where 10 are stale links is billed for the 40 it actually delivered, never for the 10 honest 404s. Apify's free plan includes $5 of usage credits per month, which covers about 24 full 100-card runs — roughly 2,500 delivered cards — before you pay anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Run the prefilled artist URL with zero edits to see a real row, then feed your own watchlist: &lt;a href="https://apify.com/zinin/spotify-artist-intel?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=spotify-artist-intel" rel="noopener noreferrer"&gt;Spotify Artist, Album &amp;amp; Playlist Scraper&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, and the README documents an agent/MCP pattern plus the Apify MCP server setup. An agent that already holds a Spotify URL passes it into &lt;code&gt;targets&lt;/code&gt;, branches on &lt;code&gt;found&lt;/code&gt; before trusting any field, uses &lt;code&gt;requestUserAgent&lt;/code&gt; to distinguish a locally blocked host from a genuine source 404, and treats a playlist row with &lt;code&gt;null&lt;/code&gt; counts as "not available on this fetch" — a disclosed source behavior to re-run, not a defect to retry blindly.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>music</category>
    </item>
    <item>
      <title>How to Pull App Store App Intelligence Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Thu, 01 Oct 2026 10:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-pull-app-store-app-intelligence-without-a-paid-api-3ckk</link>
      <guid>https://dev.to/timmyzinin/how-to-pull-app-store-app-intelligence-without-a-paid-api-3ckk</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Tracking a portfolio of iOS apps — competitors, prospects, or your own releases — sounds trivial until you do it by hand. Rating, review count, price, seller, size, age rating, in-app purchases, OS compatibility, and the latest release notes all live on each app's public App Store page, and copying them into a spreadsheet for dozens of apps, every week, is nobody's idea of a good time.&lt;/p&gt;

&lt;p&gt;Building your own scraper is harder than it looks. The App Store page carries two JSON-LD blocks — the first is &lt;code&gt;Organization&lt;/code&gt; with no rating at all, the second is &lt;code&gt;SoftwareApplication&lt;/code&gt; with the data you actually want — so a parser that grabs "the first ld+json block" returns a confident, success-shaped response with an empty rating at HTTP 200. Several fields (seller, size, age rating, IAPs, compatibility) are not in any structured-data block at all; they sit in a plain-HTML "Information" grid, and release notes live in a separate server-rendered "Version History" panel. Apple's official APIs, meanwhile, want keys, enrollment, and rate-limit management you may not need for a simple watchlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The App Store App Intel actor reads the same public app-detail page any browser sees — no login, no Apple API key, no headless browser — and returns one flat JSON row per app. Give it a numeric App Store id (&lt;code&gt;310633997&lt;/code&gt;), a slug+id pair, or a full Store URL; all three forms mix freely, up to 100 ids per run, with configurable concurrency (default 5, max 20).&lt;/p&gt;

&lt;p&gt;From the README, the behavior that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correct JSON-LD block, proven on every row.&lt;/strong&gt; It scans every &lt;code&gt;ld+json&lt;/code&gt; block and selects by &lt;code&gt;@type === "SoftwareApplication"&lt;/code&gt;, never by position, and the &lt;code&gt;evidence&lt;/code&gt; field states which block supplied the data (&lt;code&gt;"block 2 of 2"&lt;/code&gt;) on every delivered row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eight HTML-fallback fields.&lt;/strong&gt; Seller, size, age rating, in-app purchases, and compatibility are parsed from the page's plain-HTML Information grid; &lt;code&gt;whatsNew&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, and &lt;code&gt;releaseDate&lt;/code&gt; come from the server-rendered Version History panel — no JavaScript execution needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest three-state contract.&lt;/strong&gt; Every row is &lt;code&gt;found&lt;/code&gt;, &lt;code&gt;not_found&lt;/code&gt; (Apple's own confirmed HTTP 404), or &lt;code&gt;error&lt;/code&gt; (blocked, timed out, unparseable) — and only &lt;code&gt;found&lt;/code&gt; rows are billed. &lt;code&gt;not_found&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; rows are still written to the dataset with the reason, free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest partials.&lt;/strong&gt; If an HTML field is genuinely absent — the README's real Spotify row has &lt;code&gt;inAppPurchases: null&lt;/code&gt; — the row is &lt;code&gt;partial: true&lt;/code&gt; with &lt;code&gt;confidence: "medium"&lt;/code&gt; instead of an invented value. Long Apple display texts are capped and truncated with a visible &lt;code&gt;…&lt;/code&gt; marker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guards.&lt;/strong&gt; URL input is hostname-checked against &lt;code&gt;apps.apple.com&lt;/code&gt; exactly (never a substring), DNS is resolved and pinned against private/reserved ranges (SSRF/DNS-rebinding guard), responses are byte-capped at 3,000,000 with a &lt;code&gt;truncated&lt;/code&gt; flag, and retries apply only to network failures, 429, and 5xx — never to a 404.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Region is fixed to the &lt;code&gt;/us/&lt;/code&gt; storefront — the only region verified live during the build. Reviews and keyword search are permanently out of scope because the relevant hosts' &lt;code&gt;robots.txt&lt;/code&gt; disallow those paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;Input is minimal:&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;"appIds"&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="s2"&gt;"310633997"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;The README ships real, verbatim run output. The happy path for WhatsApp (id &lt;code&gt;310633997&lt;/code&gt;), trimmed:&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;"appId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"310633997"&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;"found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ld+json SoftwareApplication (block 2 of 2) + html fallback (8/8)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"partial"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WhatsApp Messenger"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ratingValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reviewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18424969&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priceText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0 USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WhatsApp Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WhatsApp Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sizeText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"392.2 MB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"whatsNew"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"We update the app regularly to fix bugs, optimize performance and improve the experience. Thanks for using WhatsApp!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"26.32.73"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"releaseDate"&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-08-17"&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;A nonexistent id returns &lt;code&gt;status: "not_found"&lt;/code&gt; with &lt;code&gt;evidence: "http 404 (App Store's own \"Not Found\" page)"&lt;/code&gt; — free. A blocked host returns &lt;code&gt;status: "error"&lt;/code&gt; with &lt;code&gt;error: "blocked host \"evil.example\" — only apps.apple.com URLs are accepted"&lt;/code&gt; before any request leaves the actor — also free. Each run also writes a one-time KVS &lt;code&gt;OUTPUT&lt;/code&gt; summary with requested/delivered/paid/free/failed counts and replay-safety state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per run start plus $0.002 per delivered app (&lt;code&gt;result-found&lt;/code&gt;), with subscription discounts on paid Apify plans (the Store headline works out to $1.70 per 1,000 delivered apps at the entry tier). At list prices, a 100-app watchlist run costs $0.005 + 100 × $0.002 = $0.205 — and a run where 8 of 10 ids resolve is billed for 8 delivered apps, not 10. Apify's free plan includes $5 of usage credits per month, covering about 24 full 100-app runs — roughly 2,500 delivered app records — before you pay anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Paste a handful of competitor ids, check the &lt;code&gt;evidence&lt;/code&gt; and &lt;code&gt;status&lt;/code&gt; fields on the rows, then schedule the watchlist: &lt;a href="https://apify.com/zinin/appstore-app-intel?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=appstore-app-intel" rel="noopener noreferrer"&gt;App Store App Intel&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, and the README describes an agent/MCP pattern explicitly: callable from the Apify API, the SDK, or the Apify MCP server, with the flat &lt;code&gt;status&lt;/code&gt; field designed to be branched on directly — &lt;code&gt;found&lt;/code&gt; means real data (check &lt;code&gt;partial&lt;/code&gt; for completeness), &lt;code&gt;not_found&lt;/code&gt; means the app genuinely does not exist right now, and &lt;code&gt;error&lt;/code&gt; means retry later or surface the &lt;code&gt;error&lt;/code&gt; string. For change tracking, agents diff &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;releaseDate&lt;/code&gt;, &lt;code&gt;priceText&lt;/code&gt;, or &lt;code&gt;ratingValue&lt;/code&gt; between scheduled runs, since the actor is stateless per run.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>data</category>
    </item>
    <item>
      <title>How to Scrape Storia.ro Listings in Bucharest Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Thu, 01 Oct 2026 07:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-scrape-storiaro-listings-in-bucharest-without-a-paid-api-4jih</link>
      <guid>https://dev.to/timmyzinin/how-to-scrape-storiaro-listings-in-bucharest-without-a-paid-api-4jih</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Bucharest property workflows rarely start from a clean API. Listings arrive as exports — from owners, agents, partner feeds, or your own CRM — and every source shapes its fields differently: prices as text, rooms embedded in titles, timestamps in local formats, no licence context attached. Before any analyst can review a batch, someone has to normalize the records, deduplicate them, and document where each row came from. Done manually, that is spreadsheet work with no audit trail.&lt;/p&gt;

&lt;p&gt;The paid alternatives are portal-side: scraping subscriptions or data contracts that re-sell marketplace content. If your organization already holds authorized exports, what you need is a normalization and evidence layer — deterministic IDs, freshness, explicit gaps, and a review queue — not another crawler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The Storia Romania Real Estate Listings actor (Store slug &lt;code&gt;storia-bucharest&lt;/code&gt;) is the Authorized Bucharest Property Export Normalizer. It processes buyer-owned, owner-authorized, or licensed property records and makes zero external requests — no marketplace, portal, browser, proxy, DNS, or child-Actor calls.&lt;/p&gt;

&lt;p&gt;Per the README, each run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validates a closed input contract: unknown properties fail closed, malformed rows become explicit free diagnostics when other valid work exists, and duplicate &lt;code&gt;(sourceRecordId, sourceName)&lt;/code&gt; identities are suppressed before billing.&lt;/li&gt;
&lt;li&gt;Delivers one stable normalized Dataset row per accepted unique record, preserving the familiar listing fields (title, price, currency, deal type, property type, rooms, area, location, posting date) plus a deterministic &lt;code&gt;stableId&lt;/code&gt; and &lt;code&gt;rowDigest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Carries provenance on every row: source name, source URL, licence statement, buyer-supplied retrieval time, and a &lt;code&gt;changesMade&lt;/code&gt; transformation disclosure.&lt;/li&gt;
&lt;li&gt;Computes freshness against your &lt;code&gt;freshnessHours&lt;/code&gt; threshold (default 168) using only the supplied retrieval time, and a confidence score that measures contract completeness with visible gaps — not factual truth.&lt;/li&gt;
&lt;li&gt;Routes every accepted row to &lt;code&gt;review_required&lt;/code&gt; with &lt;code&gt;safeToAutomate=false&lt;/code&gt;, and records settlement-neutral billing intent; the authoritative paid/free/withheld/unknown reconciliation lives in the current-run KVS &lt;code&gt;OUTPUT&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Legacy portal-style inputs (&lt;code&gt;deal_type&lt;/code&gt;, &lt;code&gt;property_type&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;max_items&lt;/code&gt;, &lt;code&gt;max_pages&lt;/code&gt;) still parse but perform no source request — they emit one free migration diagnostic explaining how to move to &lt;code&gt;records&lt;/code&gt;. The README is unambiguous about boundaries: the actor never verifies availability, price, ownership, or legal status, and it takes no external action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;The documented input submits bounded records with provenance:&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;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I confirm I may process and commercially use these submitted records."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buyer_owned_export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"batchName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storia-bucharest-evidence-review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshnessHours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"records"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceRecordId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-ro-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authorized Bucharest apartment export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example property"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"residential-property"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"occurredAt"&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-08-01T09:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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://exports.example.com/ro/demo-ro-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Licensed Romanian property export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceLicense"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buyer confirms owner, agent, or commercial feed permission."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceRetrievedAt"&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-08-01T10:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"changesMade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Whitespace and numeric representation normalized."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&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;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;195000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"deal_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;"sale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"property_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;"apartment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rooms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"area_sqm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bucharest, Romania"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"posted_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-08-01T09:00:00.000Z"&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="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="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;The README does not ship a full dataset-row fixture; its field dictionary defines the delivered row: &lt;code&gt;recordType&lt;/code&gt;, &lt;code&gt;stableId&lt;/code&gt;, &lt;code&gt;found&lt;/code&gt;, the listing fields above, &lt;code&gt;observedAt&lt;/code&gt;, &lt;code&gt;freshness&lt;/code&gt;, &lt;code&gt;change&lt;/code&gt;, &lt;code&gt;decisionConfidence&lt;/code&gt; with gaps, &lt;code&gt;evidence&lt;/code&gt;, &lt;code&gt;decision&lt;/code&gt; (always &lt;code&gt;review_required&lt;/code&gt;), &lt;code&gt;recommendedAction&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;safeToAutomate=false&lt;/code&gt;, &lt;code&gt;failureDiagnostics&lt;/code&gt;, &lt;code&gt;billing&lt;/code&gt;, and &lt;code&gt;rowDigest&lt;/code&gt;. For an accepted one-record run, the README's illustrative run binding is &lt;code&gt;{"status":"SUCCEEDED","evidenceAccepted":true,"datasetRows":1,"resultFoundDelta":1}&lt;/code&gt; — one paid &lt;code&gt;result-found&lt;/code&gt; event per delivered row. A live sample dataset is linked on the actor page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per actor start plus $0.002 per delivered normalized record (&lt;code&gt;result-found&lt;/code&gt;), with subscription discounts on higher Apify tiers (the Store headline works out to $1.70 per 1,000 records at the entry tier). At list prices, 100 records cost $0.005 + 100 × $0.002 = $0.205 per run. Apify's free plan includes $5 of usage credits per month, which covers about 24 full 100-record runs — roughly 2,500 normalized records — before you pay anything. Migration, invalid, and duplicate diagnostics are free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Start with a single authorized record, read the KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt before the Dataset rows, then scale: &lt;a href="https://apify.com/zinin/storia-bucharest?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=storia-bucharest" rel="noopener noreferrer"&gt;Storia Romania Real Estate Listings&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, so agents can call it directly, and the README includes an Apify MCP server setup plus an "MCP tool call" playbook. The playbook's rule is the one agents must encode: read KVS &lt;code&gt;OUTPUT&lt;/code&gt; first, require the current &lt;code&gt;runId&lt;/code&gt;, route &lt;code&gt;review_required&lt;/code&gt; rows to a human with licence, gaps, freshness, and digest attached, and never treat &lt;code&gt;safeToAutomate=false&lt;/code&gt; as permission to message, trade, publish, or modify an external system.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>realestate</category>
    </item>
    <item>
      <title>How to Scrape SS.ge Real Estate in Tbilisi Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 19:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-scrape-ssge-real-estate-in-tbilisi-without-a-paid-api-3o9c</link>
      <guid>https://dev.to/timmyzinin/how-to-scrape-ssge-real-estate-in-tbilisi-without-a-paid-api-3o9c</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Tbilisi property data is scattered across portals, agent inboxes, and licensed feeds. If you run a portfolio, an agency desk, or a market-analysis workflow, you typically already receive exports — from owners, agents, or a data partner — and the painful part is not finding listings, it is turning those heterogeneous exports into one consistent, reviewable evidence layer. Doing it by hand means reconciling spreadsheets, chasing missing provenance, and having no defensible answer to "where did this row come from and when was it retrieved?"&lt;/p&gt;

&lt;p&gt;The paid alternatives push you toward portal scraping subscriptions or per-seat SaaS contracts. For teams who hold authorized exports, a normalization layer with stable identity, freshness, and explicit gaps is the more useful primitive — and it keeps source rights where they belong: with the buyer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The SS.ge Georgia Real Estate Listings actor (Store slug &lt;code&gt;ss-ge-tbilisi&lt;/code&gt;, kept for API continuity) is a provenance-preserving normalizer for Tbilisi property records. It is not affiliated with SS.ge, and its current version deliberately does not crawl the portal. It works in two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authorized-export mode (default).&lt;/strong&gt; You submit buyer-owned, owner- or agent-authorized, or licensed-feed records as structured JSON. The actor validates a closed schema, deduplicates &lt;code&gt;(sourceName, sourceListingId)&lt;/code&gt; identities, and makes zero source network requests — no browser, proxy, login, or URL fetching. Recorded listing and image URLs are never opened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live public observation mode (&lt;code&gt;live_public_listing&lt;/code&gt;).&lt;/strong&gt; Exactly one bounded, login-free request to a fixed, allowlisted public SS.ge Tbilisi results page, returning up to 20 source-visible listing cards (&lt;code&gt;public_property_listing_observation&lt;/code&gt; rows). It is a single observation, not a crawl, and the description field is omitted because the public payload can contain broker contact text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every accepted unique record becomes one Dataset row carrying the property fields (URL, title, price, currency, deal type, property type, rooms, area, location, coordinates, posting date, description, images, floor), a deterministic &lt;code&gt;stableId&lt;/code&gt; plus input/request/row digests, full provenance (source name, source URL, licence statement, retrieval time, changes made), freshness computed only from your supplied retrieval timestamp, an evidence-completeness confidence score with concrete gaps, and a decision block: &lt;code&gt;review_required&lt;/code&gt; outcome, priority, and &lt;code&gt;safeToAutomate: false&lt;/code&gt;. A run-level KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt reconciles delivered, paid, free, withheld, and unknown counts. Legacy &lt;code&gt;city_id&lt;/code&gt;-style inputs still parse but return a free migration diagnostic.&lt;/p&gt;

&lt;p&gt;The README is explicit about boundaries: it does not verify availability, price, ownership, or investment quality, does not track changes across runs (&lt;code&gt;change.status&lt;/code&gt; is always &lt;code&gt;not_measured&lt;/code&gt;), and does not make property decisions — every row routes to human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;Authorized-export input (from the README; the example domains and listing are fictional fixtures):&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;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I confirm I may process and commercially use these property records."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"owner_or_agent_authorized_export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"batchName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbilisi-property-review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshnessHours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listings"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceListingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-tbilisi-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Two-bedroom apartment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;195000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"deal_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;"sale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"property_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;"Apartment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rooms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"area_sqm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tbilisi, Georgia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"posted_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-08-12T09:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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://properties.example.com/listings/demo-tbilisi-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example authorized property export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceLicense"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Owner-authorized export for internal property review."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceRetrievedAt"&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-08-12T10:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"changesMade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Selected fields and normalized whitespace."&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README's happy-path fixture for that record shows the shape of a delivered row (trimmed):&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;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"licensed_property_record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stableId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"licensed-property:…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Two-bedroom apartment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;195000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deal_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;"sale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partial"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshness"&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;"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;"fresh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"basis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buyer_supplied_source_retrieval_time"&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;"change"&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;"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;"not_measured"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"basis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single_authorized_export_record"&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;"decision"&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;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"review_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"normal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"safeToAutomate"&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;span class="nl"&gt;"billing"&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;"billingEligible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"eventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"result-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"settlementSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current_run_kvs_output"&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="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;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: $0.005 per actor start plus $0.002 per delivered normalized record (&lt;code&gt;result-found&lt;/code&gt;); the README's own table prices 100 records at $0.205 including the start event. Apify's free plan gives $5 of usage credits per month, which covers about 24 full 100-record runs — roughly 2,500 individual normalized records — before any charge. Free diagnostics (duplicates, invalid records, legacy migration) are not billed as results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Normalize one authorized record first, reconcile the Dataset against the KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt, then scale the batch: &lt;a href="https://apify.com/zinin/ss-ge-tbilisi?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=ss-ge-tbilisi" rel="noopener noreferrer"&gt;SS.ge Georgia Real Estate Listings&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, so agents can call it directly with &lt;code&gt;apify-client&lt;/code&gt; or the REST API; the README also ships an MCP server setup (&lt;code&gt;https://mcp.apify.com&lt;/code&gt; with this actor's tool). Its warehouse recipe gives agents a deterministic contract: &lt;code&gt;stableId&lt;/code&gt; as the natural identity, &lt;code&gt;rowDigest&lt;/code&gt; for version comparison, and explicit instruction that the same stable ID with a different digest means "review a changed supplied record," not proof of a portal edit — with &lt;code&gt;safeToAutomate: false&lt;/code&gt; preserved downstream.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>realestate</category>
    </item>
    <item>
      <title>How to Detect Newly Registered UK Companies Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 16:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-detect-newly-registered-uk-companies-without-a-paid-api-1751</link>
      <guid>https://dev.to/timmyzinin/how-to-detect-newly-registered-uk-companies-without-a-paid-api-1751</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Newly incorporated companies are one of the oldest lead seeds in B2B: a business that registered last week does not yet have an accountant, a bank, an insurer, or a software stack. The raw material is public — Companies House publishes every registration — but turning it into a usable list is tedious. You have to search the register keyword by keyword, check incorporation dates against your window, copy company numbers and status by hand, and somehow avoid counting the same entity twice when two keywords match it.&lt;/p&gt;

&lt;p&gt;Commercial "new company data" feeds exist, but they bundle the same public register behind a subscription and often dress a registration up as qualified demand. For a small team that just wants "active companies matching these keywords, incorporated in the last N days," a bounded, pay-per-result tool is a better fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The New UK Company Detector queries the public Companies House search-results pages and returns the first results page of active companies matching your keywords, filtered to a configurable incorporation lookback window. There is no register API key and no bulk-data contract involved.&lt;/p&gt;

&lt;p&gt;From the README, the concrete behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts up to 25 case-insensitive company-name keywords per run; case variants are deduplicated, and each keyword returns the first search page — typically up to about 20 active matches incorporated within the window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;windowDays&lt;/code&gt; bounds how recent "new" means: 1 to 365 days, default 60. &lt;code&gt;maxConcurrency&lt;/code&gt; (1–20) controls parallel keyword searches.&lt;/li&gt;
&lt;li&gt;Emits one row per unique company number with the official register facts: company name, company number, direct Companies House URL, incorporation date, legal type, status, and registered-office locality (town/region only).&lt;/li&gt;
&lt;li&gt;Deduplicates across keywords inside the run: if two keywords return the same company, it is delivered and charged once, and the repeat increments &lt;code&gt;duplicateCompanyCount&lt;/code&gt; instead of selling a duplicate row.&lt;/li&gt;
&lt;li&gt;Wraps each row in decision metadata: which keyword and window produced it, observation timestamp, incorporation age and freshness band, an evidence confidence score with explicit &lt;code&gt;dataGaps&lt;/code&gt; (website, industry, contacts, decision maker, commercial fit), a recommended enrichment-first action, action priority, and a plain-language &lt;code&gt;interpretationBoundary&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirmed no-match and operational outcomes are explicit, free rows — never a silent empty dataset — and a run-level KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt reconciles delivered, paid, free, withheld, duplicate, and failed counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README is equally clear about what it never claims: a found row is a recent official registered-name observation — a lead seed requiring enrichment — not proof of trading activity, buyer intent, revenue, or consent to outreach. It also does not page beyond the first search result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;The input is three fields:&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;"items"&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="s2"&gt;"capital"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"consulting"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"windowDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;The README does not ship a fixed output fixture; the dataset schema is a row per unique delivered company number. Per the field dictionary, a delivered company row carries &lt;code&gt;entityId&lt;/code&gt;, &lt;code&gt;companyNumber&lt;/code&gt;, &lt;code&gt;officialCompanyUrl&lt;/code&gt;, &lt;code&gt;companyName&lt;/code&gt;, &lt;code&gt;incorporatedOn&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;companyType&lt;/code&gt;, &lt;code&gt;addressLocality&lt;/code&gt;, plus &lt;code&gt;keyword&lt;/code&gt; and &lt;code&gt;searchWindowDays&lt;/code&gt; explaining why the row matched, &lt;code&gt;observedAt&lt;/code&gt; / &lt;code&gt;incorporationAgeDays&lt;/code&gt; / &lt;code&gt;freshnessBand&lt;/code&gt; for timing context, &lt;code&gt;confidenceScore&lt;/code&gt; / &lt;code&gt;confidenceBand&lt;/code&gt; / &lt;code&gt;confidenceConflict&lt;/code&gt; for evidence support, &lt;code&gt;dataGaps&lt;/code&gt; listing what enrichment is still missing, &lt;code&gt;sourceEvidence&lt;/code&gt; with the official register link, and routing fields &lt;code&gt;recommendedAction&lt;/code&gt; (for example &lt;code&gt;ENRICH_COMPANY_BEFORE_OUTREACH&lt;/code&gt;), &lt;code&gt;actionPriority&lt;/code&gt;, and &lt;code&gt;safeToAutomate&lt;/code&gt;. A live sample dataset is linked on the actor page for inspection before you integrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pricing is pay-per-event: $0.005 per actor start plus $0.005 per delivered company evidence row (&lt;code&gt;result-found&lt;/code&gt;), with lower per-event prices on paid Apify plans — the README headline works out to $4.25 per 1,000 rows at the entry discount tier. At list prices, a run delivering 100 companies costs $0.005 + 100 × $0.005 = $0.505. Apify's free plan includes $5 of usage credits per month, which covers about 9 such 100-row runs — roughly 1,000 individual company rows — before anything is charged. Explicit no-match and operational advisory rows are free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Start with one narrow keyword and a 30–60 day window, inspect the official URLs and confidence fields, then schedule the watch: &lt;a href="https://apify.com/zinin/new-company-detector?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=new-company-detector" rel="noopener noreferrer"&gt;New UK Company Detector &amp;amp; Lead Seeds&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON rows plus a KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt via the Apify API, so an agent can call it directly. The README documents both an &lt;code&gt;apify-client&lt;/code&gt; recipe and an Apify MCP call (&lt;code&gt;call-actor&lt;/code&gt; against &lt;code&gt;zinin/new-company-detector&lt;/code&gt;, with an MCP server setup block), and its webhook consumer policy tells agents to branch on &lt;code&gt;failureType&lt;/code&gt; and &lt;code&gt;safeToAutomate&lt;/code&gt; and to send &lt;code&gt;safeToAutomate=false&lt;/code&gt; rows to a human-review queue rather than treating a registration as outreach permission.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>leads</category>
    </item>
    <item>
      <title>How to Monitor 1688 Supplier Prices and MOQ Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:25:04 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-monitor-1688-supplier-prices-and-moq-without-a-paid-api-5bpb</link>
      <guid>https://dev.to/timmyzinin/how-to-monitor-1688-supplier-prices-and-moq-without-a-paid-api-5bpb</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If you source from 1688, supplier terms move constantly: prices drift, minimum order quantities change, stock disappears, and tiered pricing gets quietly restructured. Catching those changes means comparing two exports — last month's and this month's — line by line, offer by offer, in a spreadsheet. With a real supplier list, that manual diff is slow, error-prone, and impossible to audit later.&lt;/p&gt;

&lt;p&gt;The paid alternatives are not much better for this specific job. A SaaS "1688 API" wants recurring fees and often your supplier URLs or platform credentials, and it still hands you raw data rather than a reviewed change list. What a sourcing or procurement team actually needs is a deterministic diff of the offer data it already lawfully possesses, with evidence attached to every detected change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The 1688 Supplier Price &amp;amp; MOQ Monitor is a comparison engine for buyer-owned, merchant-authorized, or licensed 1688 offer exports. One boundary up front: it does not scrape 1688. It makes zero source network requests — no login, no proxy, no API call, no offer-URL fetching. It processes only the JSON records supplied in the run input, and the README is explicit that it never verifies suppliers, prices, stock, or purchasing suitability.&lt;/p&gt;

&lt;p&gt;What it does do, deterministically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normalizes text, currency, price tiers, string lists, and timestamps in closed &lt;code&gt;previous&lt;/code&gt; and &lt;code&gt;current&lt;/code&gt; snapshots, then diffs the top-level fields.&lt;/li&gt;
&lt;li&gt;Detects changes in price range, MOQ, stock, pricing tiers, unit, supplier fields, badges, and promotions.&lt;/li&gt;
&lt;li&gt;Emits one reviewable comparison row per unique &lt;code&gt;offerId&lt;/code&gt; with a &lt;code&gt;changeType&lt;/code&gt; of &lt;code&gt;baseline_created&lt;/code&gt;, &lt;code&gt;changed&lt;/code&gt;, &lt;code&gt;no_change&lt;/code&gt;, or a diagnostic status — never an invented change.&lt;/li&gt;
&lt;li&gt;Attaches a stable &lt;code&gt;1688-offer:&amp;lt;offerId&amp;gt;&lt;/code&gt; identity plus SHA-256 record and request digests for tamper comparison and downstream joins.&lt;/li&gt;
&lt;li&gt;Computes freshness from your supplied retrieval time, an evidence-completeness confidence score with explicit data gaps, a review priority, and a recommended human action such as &lt;code&gt;REVIEW_OFFER_CHANGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Always sets &lt;code&gt;safeToAutomate:false&lt;/code&gt; and writes an authoritative settlement receipt to the key-value store &lt;code&gt;OUTPUT&lt;/code&gt;, reconciling delivered, paid, free, and withheld counts for the exact run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Legacy URL-based &lt;code&gt;productUrls&lt;/code&gt; input still parses, but only produces one free migration diagnostic; those URLs are never fetched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;Input is a closed JSON contract. Each offer carries a &lt;code&gt;previous&lt;/code&gt; snapshot (null on the first comparison, which yields &lt;code&gt;baseline_created&lt;/code&gt;), a required &lt;code&gt;current&lt;/code&gt; snapshot, and provenance fields:&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;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I confirm I am authorized to process and deliver these 1688 offer records"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buyer_owned_1688_export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"watchName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usb-c-supplier-review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxChanges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"offerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000406623486"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"previous"&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;"offerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000406623486"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USB-C cable wholesale pack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CNY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"moq"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"piece"&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;"current"&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;"offerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000406623486"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USB-C cable wholesale pack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CNY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"moq"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"piece"&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;"sourceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buyer-owned 1688 export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceLicense"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buyer confirms authorization to process and deliver this export."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceRetrievedAt"&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-08-13T00:00:00Z"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that input, the README's checked-in fixture delivers a dataset row whose core decision fields look like this (trimmed):&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;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"china_1688_supplier_monitor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1688-offer:1000406623486"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changedFields"&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="s2"&gt;"priceMax"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"priceMin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"priceTiers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"promotions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendedAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REVIEW_OFFER_CHANGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"safeToAutomate"&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;p&gt;The run-level KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt then reconciles the exact run: one requested, one unique, one delivered, one paid, zero failed, &lt;code&gt;networkRequestsMade: 0&lt;/code&gt;, &lt;code&gt;platformLoginUsed: false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;The actor is pay-per-event: $0.005 per actor start plus $0.006 per delivered comparison row (the &lt;code&gt;item-checked&lt;/code&gt; event), with lower per-event prices on paid Apify plans. At those list prices, a 100-offer comparison run costs $0.005 + 100 × $0.006 = $0.605. Apify's free plan includes $5 of usage credits per month, which covers about 8 full 100-offer runs — roughly 830 individual comparison rows — before you pay anything. Free diagnostics (legacy migration, invalid records, duplicates) are not billed as results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Run a one-record comparison first, verify the dataset row and the KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt, then scale the batch: &lt;a href="https://apify.com/zinin/china-1688-supplier-monitor?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=china-1688-supplier-monitor" rel="noopener noreferrer"&gt;1688 Supplier Price &amp;amp; MOQ Monitor&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes JSON in and returns structured JSON via the Apify API, so an agent can call it directly with &lt;code&gt;apify-client&lt;/code&gt; or the REST API. The README ships an MCP server setup (&lt;code&gt;https://mcp.apify.com&lt;/code&gt; with this actor's tool) and an "MCP or agent workflow" recipe: an agent may submit records already present in its authorized workspace, but it must not invent authorization, browse 1688, or decide a purchase — every row carries &lt;code&gt;safeToAutomate:false&lt;/code&gt;, explicit gaps, and a human-review route by design.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>How to Turn Company Domains Into Verified Work Emails Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 10:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-turn-company-domains-into-verified-work-emails-without-a-paid-api-53gl</link>
      <guid>https://dev.to/timmyzinin/how-to-turn-company-domains-into-verified-work-emails-without-a-paid-api-53gl</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Sales, recruiting and partnerships workflows all hit the same wall: you have a person's name and their company domain, and you need a work email address. The common shortcuts are guessing patterns (&lt;code&gt;first.last@domain&lt;/code&gt;) and SMTP probing — both damage sender reputation, and a "mailbox accepted the message" probe proves nothing about who owns it. Paid people-search databases sell the same guess with a nicer UI.&lt;/p&gt;

&lt;p&gt;The defensible alternative is boring and manual: open the company's own website, find the team page, and check whether the address is literally published next to the person's name. That does not scale past a handful of lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The Domain to Verified Work Email actor automates exactly that check — and nothing more. For each &lt;code&gt;{fullName, domain}&lt;/code&gt; pair (1–50 per run) it visits only the buyer-selected company's own website, reading at most four fixed pages in a fixed relevance order — &lt;code&gt;/team&lt;/code&gt;, &lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/contact&lt;/code&gt;, then &lt;code&gt;/&lt;/code&gt; — over HTTPS, honoring &lt;code&gt;robots.txt&lt;/code&gt; fail-closed, with no login, proxy, CAPTCHA-solving or JavaScript execution. Per the README, every row lands in one of four outcome classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Published person match&lt;/strong&gt; (billed): the literal address exists as visible text or a &lt;code&gt;mailto:&lt;/code&gt; link, the full name appears in the same bounded context, the local part follows a personal-name form, and the same HTML document carries a schema.org &lt;code&gt;Person&lt;/code&gt; JSON-LD assertion whose normalized name and email exactly match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Published visible match&lt;/strong&gt;: every condition holds except the schema.org assertion — the page prints the address in the person's own block with no competing person. This is what most real company sites publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free candidate list&lt;/strong&gt;: name-shaped addresses (&lt;code&gt;jdoe@&lt;/code&gt;, &lt;code&gt;jane@&lt;/code&gt;) that the page never prints next to the name are listed in &lt;code&gt;candidateEmails&lt;/code&gt; for a human, never promoted to a match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest free decision&lt;/strong&gt;: no address, only role mailboxes (&lt;code&gt;info@&lt;/code&gt;, &lt;code&gt;press@&lt;/code&gt; — listed in &lt;code&gt;roleEmails&lt;/code&gt;), an ambiguous tie, a robots-blocked page, or a budget stop. Every free row still reports checked pages, unreachable pages and reasons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every row — paid or free — carries &lt;code&gt;safeToAutomate: false&lt;/code&gt; and &lt;code&gt;safeForOutreach: false&lt;/code&gt;: a published page proves the address is printed, not mailbox ownership, consent or deliverability. Role-token local parts (&lt;code&gt;contact-us&lt;/code&gt;, &lt;code&gt;recruiters&lt;/code&gt;, &lt;code&gt;human-resources&lt;/code&gt;, …) never become paid rows, and hidden CSS content is excluded via offline layout evaluation before matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&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;"people"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fullName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"King Lee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"visionwide.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;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Visionwide"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPagesPerPerson"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;The README's published person match example (abridged to key fields):&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;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"person_email_decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fullName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"King Lee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Visionwide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"visionwide.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;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"king.lee@visionwide.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;"verificationStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"published_person_match"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchScore"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchReasons"&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="s2"&gt;"literal_email_published"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"full_name_on_page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schema_org_person_name_email_assertion"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceUrl"&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://visionwide.co/team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceEvidence"&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;"schemaOrgPersonAssertion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"publicationChannel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"visible_text"&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;"roleEmails"&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="s2"&gt;"info@visionwide.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;"checkedPages"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/team"&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="mi"&gt;200&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/about"&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="mi"&gt;200&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidenceBand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendedAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ingest_evidence_then_apply_your_legal_outreach_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"safeToAutomate"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"safeForOutreach"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chargedEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"result-found"&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;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: &lt;strong&gt;$0.005 per run start plus $0.01 per published person match&lt;/strong&gt;. A published visible match is billed only while its &lt;code&gt;visible-match-found&lt;/code&gt; event carries a Store price — otherwise it is free. Every other outcome (candidates, role mailboxes, no-match, ambiguous, partial, robots-blocked) costs nothing beyond the start fee. A run delivering 100 published matches costs $0.005 + 100 × $0.01 = &lt;strong&gt;$1.005&lt;/strong&gt;. Apify's free plan gives $5 of usage credits per month, so $5 covers about &lt;strong&gt;4 such runs — roughly 400 verified addresses&lt;/strong&gt; — plus any number of free-outcome rows at the start fee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Submit a name and a domain, and read the evidence-tagged row before any outreach decision: &lt;a href="https://apify.com/zinin/domain-to-verified-work-email?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=domain-to-verified-work-email" rel="noopener noreferrer"&gt;Domain to Verified Work Email&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor is built as a machine-first microservice: a closed input schema, a uniform 34-field row shape regardless of outcome, and a schema-validated &lt;code&gt;OUTPUT&lt;/code&gt; summary — callable from the Apify API, a scheduled task, or the hosted Apify MCP server documented in the README. An agent can switch directly on &lt;code&gt;verificationStatus&lt;/code&gt; and gate spend on &lt;code&gt;chargedEvent&lt;/code&gt;, with &lt;code&gt;safeToAutomate&lt;/code&gt; and &lt;code&gt;safeForOutreach&lt;/code&gt; fixed at &lt;code&gt;false&lt;/code&gt; so a lookup can never silently escalate into an autonomous send.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>leads</category>
    </item>
    <item>
      <title>How to Scrape Computrabajo Job Postings in LatAm Without a Paid API</title>
      <dc:creator>Tim Zinin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 07:25:03 +0000</pubDate>
      <link>https://dev.to/timmyzinin/how-to-scrape-computrabajo-job-postings-in-latam-without-a-paid-api-4kjd</link>
      <guid>https://dev.to/timmyzinin/how-to-scrape-computrabajo-job-postings-in-latam-without-a-paid-api-4kjd</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Latin American job data — Mexico, Colombia, Chile, Argentina, Peru — usually arrives as exports: an employer's ATS dump, a recruiting partner's feed, a licensed regional batch. Those exports are messy in the same ways every time: duplicate postings, malformed timestamps, missing licence statements, and no stable way to tell whether a row was actually delivered and billed.&lt;/p&gt;

&lt;p&gt;Scraping a job board directly is the obvious alternative, but it raises rights and fragility questions — and for Computrabajo specifically, the historical Store slug now belongs to a different kind of tool entirely. The painful part is rarely getting the data; it is turning it into clean, provenance-linked, review-ready records.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actor does
&lt;/h2&gt;

&lt;p&gt;The Licensed LatAm Job Export Normalizer (the actor behind the &lt;code&gt;computrabajo-jobs&lt;/code&gt; slug) is explicit about what it is today: it does &lt;strong&gt;not&lt;/strong&gt; call Computrabajo or any other job board in export mode, makes zero source network requests, and is not affiliated with Computrabajo. Instead it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalizes authorized exports&lt;/strong&gt;: 1–100 structured job records you own or are licensed to use (&lt;code&gt;buyer_owned_job_export&lt;/code&gt;, &lt;code&gt;employer_authorized_export&lt;/code&gt;, &lt;code&gt;ats_authorized_export&lt;/code&gt;, &lt;code&gt;other_licensed_job_export&lt;/code&gt;), validated against a closed schema — unknown fields, bad types, impossible timestamps and control characters are rejected from paid work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicates before billing&lt;/strong&gt; on case-insensitive &lt;code&gt;sourceName + sourceJobId&lt;/code&gt;, with a free diagnostic reporting suppression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserves provenance on every row&lt;/strong&gt;: source name, source URL, licence statement, retrieval timestamp and transformation disclosure are required fields, never optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adds review machinery&lt;/strong&gt;: stable deterministic IDs, input and row digests for idempotent ingestion, freshness computed from the buyer-supplied retrieval time, a deterministic contract-completeness confidence score with explicit gaps, a human-review summary and priority — and &lt;code&gt;safeToAutomate: false&lt;/code&gt; on every row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciles the run&lt;/strong&gt;: a KVS &lt;code&gt;OUTPUT&lt;/code&gt; receipt records requested, unique, duplicate, invalid, delivered, paid, free, withheld and unknown counts, with an exact &lt;code&gt;+1&lt;/code&gt; named-event check per paid row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Legacy &lt;code&gt;country:query&lt;/code&gt; inputs still parse but produce a free migration diagnostic — no board is queried. An optional &lt;code&gt;sourceMode: "live"&lt;/code&gt; observes exactly one bounded public Computrabajo search page, clearly labelled &lt;code&gt;actor_fetched_public_page&lt;/code&gt;, without inheriting export-mode authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: input and output
&lt;/h2&gt;

&lt;p&gt;The documented minimal licensed-export input:&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;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I confirm I may process and commercially use these job records."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"employer_authorized_export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"batchName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latam-sales-review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshnessHours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobs"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceQuery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ventas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceJobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-sales-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Account Executive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Employer SA de CV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"companyRating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ciudad de México"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"postedAt"&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-08-12T09:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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://careers.example.com/jobs/demo-sales-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"employmentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full-time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"descriptionSnippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buyer-supplied summary of the role."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example employer ATS export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceUrl"&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://careers.example.com/exports/jobs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceLicense"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Employer-authorized export for recruiting operations."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceRetrievedAt"&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-08-12T10:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"changesMade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Selected fields and normalized whitespace."&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A delivered row (&lt;code&gt;licensed_job_record&lt;/code&gt;), excerpted from the README's checked example:&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;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"licensed_job_record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stableId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"licensed-job:…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Account Executive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Employer SA de CV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ciudad de México"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshness"&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;"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;"fresh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"basis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buyer_supplied_source_retrieval_time"&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;"change"&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;"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;"not_measured"&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;"decision"&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;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"review_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"normal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"safeToAutomate"&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;span class="nl"&gt;"billing"&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;"billingEligible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"eventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"result-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"settlementSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current_run_kvs_output"&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="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;
  
  
  Pricing and the free limit
&lt;/h2&gt;

&lt;p&gt;Pay-per-event: &lt;strong&gt;$0.005 per run start plus $0.002 per delivered normalized job record&lt;/strong&gt;. Migration, invalid-record and duplicate diagnostics are free. Per the README's own table, 100 delivered records cost &lt;strong&gt;$0.205&lt;/strong&gt;. Apify's free plan gives $5 of usage credits per month, so $5 covers about &lt;strong&gt;24 such runs — roughly 2,400 normalized records&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Confirm the rights statement, map one authorized record, and reconcile the &lt;code&gt;OUTPUT&lt;/code&gt; receipt before scaling the batch: &lt;a href="https://apify.com/zinin/computrabajo-jobs?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=apify-actors&amp;amp;utm_content=computrabajo-jobs" rel="noopener noreferrer"&gt;Licensed LatAm Job Export Normalizer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents and MCP
&lt;/h2&gt;

&lt;p&gt;The actor takes a closed JSON contract and returns schema-enforced rows plus a machine-readable &lt;code&gt;OUTPUT&lt;/code&gt; receipt, callable from the Apify API, the SDKs, or the hosted Apify MCP server documented in the README. An agent can key idempotent ingestion on &lt;code&gt;stableId&lt;/code&gt; and the row digest, route rows by &lt;code&gt;freshness.status&lt;/code&gt; and &lt;code&gt;decision.priority&lt;/code&gt;, and reconcile spend from the receipt's named-event counters — while &lt;code&gt;safeToAutomate: false&lt;/code&gt; keeps every record in a human review queue.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>api</category>
      <category>jobs</category>
    </item>
  </channel>
</rss>
