<?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: Nikita Iakovlev</title>
    <description>The latest articles on DEV Community by Nikita Iakovlev (@nikita_iakovlev_415524c19).</description>
    <link>https://dev.to/nikita_iakovlev_415524c19</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%2F4108814%2F1fd8a7b2-6bb2-458e-aa51-a97f46032554.png</url>
      <title>DEV Community: Nikita Iakovlev</title>
      <link>https://dev.to/nikita_iakovlev_415524c19</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nikita_iakovlev_415524c19"/>
    <language>en</language>
    <item>
      <title>Email Verification Without Sending a Single Email (And Why Port 25 Is a Dead End)</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:33:29 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/email-verification-without-sending-a-single-email-and-why-port-25-is-a-dead-end-13ie</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/email-verification-without-sending-a-single-email-and-why-port-25-is-a-dead-end-13ie</guid>
      <description>&lt;p&gt;Every email verification service advertises the same thing: paste a list, get back deliverable or not. Almost none of them explain what they actually check, and the difference matters, because one of the checks people assume is happening is usually impossible.&lt;/p&gt;

&lt;p&gt;Here's what's really under the hood, what each signal is worth, and the one that quietly doesn't work on any major cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that doesn't work anymore
&lt;/h2&gt;

&lt;p&gt;The textbook method is an SMTP handshake. You connect to the recipient's mail server on port 25, say &lt;code&gt;HELO&lt;/code&gt;, &lt;code&gt;MAIL FROM&lt;/code&gt;, then &lt;code&gt;RCPT TO: &amp;lt;the address&amp;gt;&lt;/code&gt;, and read the response code. A 250 means the mailbox exists. You disconnect before &lt;code&gt;DATA&lt;/code&gt;, so no mail is ever sent.&lt;/p&gt;

&lt;p&gt;It's elegant. It's also unavailable to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outbound port 25 is blocked by default on AWS, Google Cloud, Azure, DigitalOcean, Hetzner and most serverless platforms.&lt;/strong&gt; It's an anti-spam measure and it has been standard for years. Some providers will unblock it — AWS after a request form, Google automatically for projects it scores as low-risk, Hetzner's dedicated servers by self-service after a month. DigitalOcean blocks 25, 465 and 587 with no path at all.&lt;/p&gt;

&lt;p&gt;The point isn't that it's impossible everywhere. It's that on whatever you actually deploy to, it's probably off, and finding out after you've built the feature is expensive. I checked before building anything: on the platform I deploy to, a raw socket to &lt;code&gt;gmail-smtp-in.l.google.com:25&lt;/code&gt; times out from every region I tried.&lt;/p&gt;

&lt;p&gt;Two more reasons SMTP verification is worse than it looks even when you can run it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Catch-all domains accept everything.&lt;/strong&gt; A large share of corporate domains return 250 for any address at all, so a positive result carries no information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Greylisting and reputation.&lt;/strong&gt; Repeated &lt;code&gt;RCPT TO&lt;/code&gt; probes from one IP get throttled, then blocked. Providers treat the pattern as harvesting, because it is the same pattern harvesters use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which is why the honest services return a &lt;strong&gt;three-state answer&lt;/strong&gt;, not a boolean. Anyone selling you a clean yes/no on a corporate domain is guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can actually be checked
&lt;/h2&gt;

&lt;p&gt;Five signals, each independently useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Syntax against the real grammar.&lt;/strong&gt; Not a regex from Stack Overflow. Length limits (64 characters local part, 254 total), consecutive dots, leading and trailing dots, quoting rules, IDN handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Domain resolution.&lt;/strong&gt; Does the domain exist at all? An &lt;code&gt;NXDOMAIN&lt;/code&gt; is a definite negative and the only cheap definite negative you get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. MX records.&lt;/strong&gt; A domain can exist and still have no mail exchanger. No MX means nothing can receive mail there, which is again definite. This one catches more than you'd expect, including a surprising number of live business websites whose owners never finished setting up mail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Disposable domain list.&lt;/strong&gt; Mailinator, Guerrilla Mail, 10minutemail and several thousand throwaway providers, including the typo-squat domains registered specifically to catch mistyped Gmail addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Classification.&lt;/strong&gt; Role address (&lt;code&gt;info@&lt;/code&gt;, &lt;code&gt;support@&lt;/code&gt;, &lt;code&gt;sales@&lt;/code&gt;, &lt;code&gt;noreply@&lt;/code&gt;), free mailbox (Gmail, Yahoo, Outlook), and a typo suggestion when the domain is one edit away from a major provider.&lt;/p&gt;

&lt;p&gt;The output of those five is not "valid" or "invalid". It's:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;deliverable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Domain resolves, has an MX, not disposable, syntax clean&lt;/td&gt;
&lt;td&gt;Send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;risky&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reachable but flagged — disposable domain, or no MX record&lt;/td&gt;
&lt;td&gt;Decide by campaign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;undeliverable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Domain doesn't exist, or syntax is invalid&lt;/td&gt;
&lt;td&gt;Drop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Role and free-mailbox are &lt;strong&gt;flags, not statuses&lt;/strong&gt;. &lt;code&gt;support@apify.com&lt;/code&gt; is deliverable &lt;em&gt;and&lt;/em&gt; a role address; whether that disqualifies it depends entirely on what you're sending. The status says whether mail can arrive. The flags say whether you want it to.&lt;/p&gt;

&lt;p&gt;The reason a missing MX lands in &lt;em&gt;risky&lt;/em&gt; rather than &lt;em&gt;undeliverable&lt;/em&gt; is a detail of the standard: with no MX record, a sending server falls back to the domain's A record and tries to deliver there (RFC 5321 §5.1). That almost always fails, because the machine behind the A record is a web server with no mail daemon — but "almost always" isn't "never", so the honest label is risky.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real output
&lt;/h2&gt;

&lt;p&gt;This is an actual run, not a mockup. Six addresses and four phone numbers went in on 4 September 2026; four of the email results:&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="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;"support@apify.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deliverable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailDomain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apify.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"emailIsRole"&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;"emailIsFree"&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;"emailIsDisposable"&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;"contactScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&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;"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;"fake@nonexistentdomain-xyz-99.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"undeliverable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"domain_not_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;"contactScore"&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="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;"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;"test@mailinator.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"risky"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disposable_domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailIsDisposable"&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;"contactScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&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;"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;"noreply@gmial.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"risky"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disposable_domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emailIsRole"&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;"emailIsDisposable"&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;"contactScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&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;That last one is the interesting case. &lt;code&gt;gmial.com&lt;/code&gt; is a misspelling of &lt;code&gt;gmail.com&lt;/code&gt;, it is registered, it resolves, and it sits on the public disposable-domain blocklists. A syntax check passes it. A domain-resolution check passes it. It takes the blocklist — or, independently, the missing MX record — to flag it, which is the argument for running all five checks rather than the one you think is decisive.&lt;/p&gt;

&lt;p&gt;One more from that run, which I'll return to at the end: a live business domain that came back &lt;strong&gt;risky, reason &lt;code&gt;no_mx_record&lt;/code&gt;&lt;/strong&gt;. Working website, working DNS, no mail exchanger. Every email sent to that domain bounces, and its owner had no idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phone numbers: the rules are published
&lt;/h2&gt;

&lt;p&gt;North American phone validation has a real specification, which means real verification instead of a regex.&lt;/p&gt;

&lt;p&gt;The North American Numbering Plan Administrator publishes the assignment file at &lt;code&gt;reports.nanpa.com/public/npa_report.csv&lt;/code&gt;. It lists every area code, whether it's geographic, which state, province or territory it belongs to, and its time zone. In the file dated 3 September 2026 there are &lt;strong&gt;378 geographic area codes in the United States&lt;/strong&gt;, and &lt;strong&gt;454 across the whole plan&lt;/strong&gt; once Canada and the Caribbean members are counted.&lt;/p&gt;

&lt;p&gt;From that file plus the plan's own rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The area code must be &lt;strong&gt;assigned and in service&lt;/strong&gt;. &lt;code&gt;999&lt;/code&gt; is not: it's one of &lt;strong&gt;79 codes with a 9 in the middle&lt;/strong&gt; that the file marks &lt;code&gt;Expansion Code&lt;/code&gt;, held back for when the plan runs out of numbers. Every one of them fails.&lt;/li&gt;
&lt;li&gt;Area code and exchange both start with &lt;strong&gt;2 through 9&lt;/strong&gt;, never 0 or 1.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;N11 codes are service codes&lt;/strong&gt; — &lt;code&gt;911&lt;/code&gt;, &lt;code&gt;411&lt;/code&gt;, &lt;code&gt;211&lt;/code&gt; and the rest — and are never assignable as exchanges. A handful more, &lt;code&gt;555&lt;/code&gt;, &lt;code&gt;950&lt;/code&gt;, &lt;code&gt;958&lt;/code&gt;, &lt;code&gt;959&lt;/code&gt; and &lt;code&gt;976&lt;/code&gt;, are held back by convention rather than by rule.&lt;/li&gt;
&lt;li&gt;Within &lt;code&gt;555&lt;/code&gt;, only the block &lt;strong&gt;&lt;code&gt;555-0100&lt;/code&gt; to &lt;code&gt;555-0199&lt;/code&gt;&lt;/strong&gt; is formally set aside for fictional use. This is the part most validators get wrong in the other direction: &lt;code&gt;555-1212&lt;/code&gt; is a live directory-assistance number, so "reject anything with 555" produces false negatives on real data. Reject the reserved block, not the exchange.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real output from the same run:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phone"&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 415 555 0132"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"phoneStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phoneReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reserved_exchange"&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;"phone"&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 999 123 4567"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"phoneStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phoneReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nanp_rule"&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;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+62 361 123456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"phoneStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phoneType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"international"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"phoneReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"structure_only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"phoneE164"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+62361123456"&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;Note the third one. &lt;code&gt;0361&lt;/code&gt; is genuinely the Denpasar area code, so the number is correctly formed for Bali, and the honest answer is &lt;code&gt;structure_only&lt;/code&gt; — the number is correctly formed, and outside the NANP there is no equivalent public assignment file to check it against. A tool that claims to "validate" any international number without saying that is telling you it checked something it didn't.&lt;/p&gt;

&lt;p&gt;For valid North American numbers you also get the state and the time zone, which is what you actually wanted — a lead list sorted by local business hours. One gotcha worth hard-coding: Arizona does not observe daylight saving, so it needs &lt;code&gt;America/Phoenix&lt;/code&gt;, not &lt;code&gt;America/Denver&lt;/code&gt;, or half your calls land an hour off for eight months of the year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who needs this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Anyone buying or scraping lead lists.&lt;/strong&gt; Bounce rate is what gets a sending domain blacklisted. The clearest published numbers come from Amazon SES: a hard-bounce rate at or above &lt;strong&gt;5% puts an account under review, 10% suspends it&lt;/strong&gt;. Most other providers publish nothing and act on their own thresholds, which is worse, not better — you find out by being throttled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sales teams paying per contact.&lt;/strong&gt; Enrichment vendors charge whether the contact is alive or not. Filtering before import is cheaper than filtering after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anyone with a signup form.&lt;/strong&gt; Blocking disposable domains at registration removes most throwaway accounts, and the typo suggestion recovers real users who would otherwise never receive the confirmation email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRM hygiene.&lt;/strong&gt; Contact data goes stale continuously as people change jobs, companies fold and domains lapse. The often-quoted "22.5% a year" traces back to a single HubSpot blog post and shouldn't be treated as measured, but the direction isn't in doubt, and a quarterly pass over the whole database catches the decay before a campaign does.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://apify.com/lergassy/email-phone-verifier" rel="noopener noreferrer"&gt;Email &amp;amp; Phone Verifier&lt;/a&gt; actor does the checks above with no SMTP dependency, which is why it works from anywhere.&lt;/p&gt;

&lt;p&gt;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;"emails"&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;"support@apify.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test@mailinator.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phones"&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;"+1 415 555 0132"&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 212 555 0199"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"all"&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;From the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/lergassy~email-phone-verifier/runs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"emails":["support@apify.com"],"phones":["+1 415 555 0132"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also reads another actor's dataset directly, which is the part worth knowing. Point &lt;code&gt;inputDatasetId&lt;/code&gt; at the output of any scraper that produced contacts and it verifies that dataset in place:&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;"inputDatasetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123def456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emailField"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phoneField"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deliverable"&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;Set &lt;code&gt;keep&lt;/code&gt; to &lt;code&gt;deliverable&lt;/code&gt; and the output contains only contacts that passed. Scrape, verify, import, with nothing in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It does not confirm that a specific mailbox exists. Nothing that runs on ordinary cloud infrastructure can, for the port 25 reason above, and on catch-all domains nothing can regardless of where it runs.&lt;/p&gt;

&lt;p&gt;It does not tell you whether a phone is mobile or landline. That requires a carrier lookup against paid HLR data, which is a different product at a different price.&lt;/p&gt;

&lt;p&gt;It does not check whether a person still works somewhere. That's job-change data, sold by LinkedIn-adjacent vendors, and no amount of DNS will produce it.&lt;/p&gt;

&lt;p&gt;What it does is remove the addresses that will definitely bounce and the numbers that cannot possibly ring, and label the rest with the reason. On the scraped lists I've run it through, that has been somewhere between a seventh and a third of the rows — and it's that fraction which costs you a sending domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The postscript
&lt;/h2&gt;

&lt;p&gt;That domain with no MX record from the run above? It was my own company's.&lt;/p&gt;

&lt;p&gt;The website works, the DNS is fine, mail was never configured. A sender with no MX to aim at falls back to the A record, which here is a web server that has never spoken SMTP — so mail to that domain has been failing quietly for as long as the domain has existed. I found it by running my own address through a tool I built for other people's lists.&lt;/p&gt;

&lt;p&gt;Run your own domain through something before you run anyone else's.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/lergassy/email-phone-verifier" rel="noopener noreferrer"&gt;apify.com/lergassy/email-phone-verifier&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://apify.com/lergassy/us-business-filings" rel="noopener noreferrer"&gt;US New Business Leads&lt;/a&gt; delivers newly registered US companies with these same checks already applied to every phone and email in the feed.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>webdev</category>
      <category>data</category>
    </item>
    <item>
      <title>Telegram Channel Analytics Without an API Key — Subscriber Counts Lie, Reach Doesn't</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:33:13 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/telegram-channel-analytics-without-an-api-key-subscriber-counts-lie-reach-doesnt-4nd6</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/telegram-channel-analytics-without-an-api-key-subscriber-counts-lie-reach-doesnt-4nd6</guid>
      <description>&lt;p&gt;A Telegram channel with 10.9 million subscribers and a channel with 172,000 subscribers. Which one would you advertise in?&lt;/p&gt;

&lt;p&gt;Here are their actual numbers, from a run over the last 100 posts of each on 4 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pavel Durov&lt;/th&gt;
&lt;th&gt;Bloomberg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subscribers&lt;/td&gt;
&lt;td&gt;10,900,000&lt;/td&gt;
&lt;td&gt;172,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median views per post&lt;/td&gt;
&lt;td&gt;5,600,000&lt;/td&gt;
&lt;td&gt;39,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reach&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reach trend&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−10.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+44.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactions per post&lt;/td&gt;
&lt;td&gt;1,030&lt;/td&gt;
&lt;td&gt;144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Posts per day&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Durov's channel reaches half its audience — extraordinary, and 140× more people in absolute terms. But its reach is &lt;em&gt;falling&lt;/em&gt;, while Bloomberg's is up 45% over the same window. Depending on whether you're buying one placement or a six-month presence, those two facts point in different directions.&lt;/p&gt;

&lt;p&gt;None of this is in the subscriber count, which is the only number most people look at. All of it is available without an API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways into Telegram data, and why two of them don't work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bot API.&lt;/strong&gt; Requires your bot to be an administrator of the target channel. Perfect for channels you own, useless for every channel you don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MTProto / user API.&lt;/strong&gt; Full access to everything a logged-in user sees. Requires a phone number, a 2FA session and careful rate limiting. Reading public channel history through it is relatively low-risk; bulk member-list exports and messaging strangers are what actually get accounts limited. Either way you are putting a real phone number behind the request, and a restriction lands on the number, not the IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The public preview page.&lt;/strong&gt; Telegram serves &lt;code&gt;t.me/s/&amp;lt;channel&amp;gt;&lt;/code&gt; to any visitor, no account required. It's the same HTML a search engine sees.&lt;/p&gt;

&lt;p&gt;For monitoring channels you don't own, the third option is the only sane one. What it gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Channel header: title, subscriber count, description, photo, verified badge, media counts&lt;/li&gt;
&lt;li&gt;20 posts per request, paged backwards with &lt;code&gt;?before=&amp;lt;message_id&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Per post: message id, ISO timestamp, full text, &lt;strong&gt;view count&lt;/strong&gt;, reactions with their emoji and counts, media flags, forward source, reply flag, author signature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 1.4 seconds per page, so 200 posts — ten pages — in roughly fifteen seconds. No token, no proxy required, no ban risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why views are the number that matters
&lt;/h2&gt;

&lt;p&gt;Telegram is unusual in how far it goes here: every post in a public channel carries a view count, visible to anyone, with no login and no API. X and Instagram both surface view counts too, but only inside their own apps, behind authentication and rate limits — which is the difference that matters if you want to compute anything across a list of channels.&lt;/p&gt;

&lt;p&gt;That single number lets you compute the thing advertisers actually price against:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reach = median views per post / subscribers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Median, never average.&lt;/strong&gt; One viral post drags an average up by a factor of three and tells you nothing about what a normal post does. On the Durov channel the average is 6.66M against a 5.6M median, and the best post did 24.2M — if you quoted the average to a client you would be promising reach that a typical post won't deliver.&lt;/p&gt;

&lt;p&gt;Reach separates real audiences from bought ones instantly. A channel with 200,000 subscribers and 4% reach was inflated. A channel with 50,000 subscribers and 40% reach is worth more per post than the inflated one, and usually costs less.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else the timestamps give you
&lt;/h2&gt;

&lt;p&gt;Once you have a few hundred posts with timestamps and view counts, several useful things fall out of simple arithmetic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reach trend.&lt;/strong&gt; Median views of the newer half of the window against the older half. Bloomberg's +44.9% and Durov's −10.4% above come from exactly this. It's a direction, not a forecast, but it's the difference between buying into a channel on the way up and one on the way down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best hours and weekdays.&lt;/strong&gt; Group posts by hour, take the median views of each group, and require at least two posts per group so a single outlier can't win. This is a channel's own data, which beats every generic "best time to post" article ever written, because those articles are about a different audience in a different time zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cadence.&lt;/strong&gt; Posts per day and per week. A channel posting fifty times a day burns its audience differently from one posting twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content mix.&lt;/strong&gt; Share of posts with photos, videos, polls, links. Share that are forwards, and which channels they're forwarded from — run that across twenty channels in one niche and you have a map of who feeds whom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reaction rate.&lt;/strong&gt; Reactions per post as a share of median views. Rough, but it separates channels that are read from channels that are merely delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes worth avoiding
&lt;/h2&gt;

&lt;p&gt;I got most of these wrong before the data corrected me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A slow channel's 100 posts can span a year.&lt;/strong&gt; Bloomberg's channel above posts 0.2 times a day, so 100 posts covers well over a year. Any "per day" figure computed over that window is an average across seasons. Always publish the number of days the window covers alongside anything rated per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hashtag extraction needs a letter.&lt;/strong&gt; A naive &lt;code&gt;#\w+&lt;/code&gt; regex turns &lt;code&gt;#036&lt;/code&gt; from a price into a top hashtag. Require at least one letter in the match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reach above 100% is not a bug.&lt;/strong&gt; Telegram counts a view once per user, including people who saw the post forwarded into another channel or opened it on the web. A post that travels beyond its channel can be seen by more people than the channel has subscribers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamps are UTC.&lt;/strong&gt; Convert to the channel's own time zone before concluding anything about a morning slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal implementation
&lt;/h2&gt;

&lt;p&gt;The page is server-rendered HTML with stable class names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;channel_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://t.me/s/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?before=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tgme_widget_message_wrap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;views&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tgme_widget_message_views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time_el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tgme_widget_message_date time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[data-post]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time_el&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datetime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time_el&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things this snippet doesn't do, and you need both: view counts arrive as &lt;code&gt;53K&lt;/code&gt; and &lt;code&gt;1.4M&lt;/code&gt;, so they need parsing into integers before any arithmetic; and paging requires passing the &lt;em&gt;lowest&lt;/em&gt; message id you've seen back as &lt;code&gt;before&lt;/code&gt;, then stopping when a page returns nothing new.&lt;/p&gt;

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

&lt;p&gt;I packaged the whole thing — paging, the K/M parser, all the metrics above, error rows for channels that have no public page — as &lt;a href="https://apify.com/lergassy/telegram-channel-analytics" rel="noopener noreferrer"&gt;Telegram Channel Analytics&lt;/a&gt;.&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;"channels"&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;"@durov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@bloomberg"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postsPerChannel"&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;"topPosts"&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="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;One row per channel, with a ready-made sentence at the end of it:&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;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bloomberg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscribers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;172000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postsAnalysed"&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;"daysCovered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;415.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;"viewsMedian"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;39900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engagementRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;23.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;"viewsTrendPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;44.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reactionsPerPost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;143.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bestHours"&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;"hour"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"posts"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"medianViews"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62300&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;"Bloomberg has 172,000 subscribers, median post reaches 39,900 views (23.2% of the audience), posts 0.2 times a day, reach up 44.9% across the 100 posts analysed."&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;One naming wrinkle worth flagging: the field is called &lt;code&gt;engagementRate&lt;/code&gt;, but it holds what this article calls &lt;strong&gt;reach&lt;/strong&gt; — median views over subscribers. The reaction rate is a separate field.&lt;/p&gt;

&lt;p&gt;Switch on &lt;code&gt;includePosts&lt;/code&gt; and every post arrives as its own row too, with views, reach, reactions, media flags, hashtags and links.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Private channels and groups are invisible.&lt;/strong&gt; No public page, no data. Those come back as an explicit error row with the reason, never as a silent zero. Reading private chats requires a real account and a session, which is a different product with different risks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscriber history doesn't exist in the source.&lt;/strong&gt; Telegram publishes the current count and nothing else. "How fast is this channel growing" can only be answered by running on a schedule and keeping the results — a daily run turns &lt;code&gt;subscribers&lt;/code&gt;, &lt;code&gt;viewsMedian&lt;/code&gt; and &lt;code&gt;engagementRate&lt;/code&gt; into a time series. There is no way to recover last month's number today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments aren't in the preview.&lt;/strong&gt; Discussion-group replies live elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is worth the effort
&lt;/h2&gt;

&lt;p&gt;Buying advertising is the obvious one: compare candidates on reach instead of subscribers and the shortlist reorders immediately.&lt;/p&gt;

&lt;p&gt;Selling it is the mirror image. A channel owner quoting reach, reaction rate, cadence and best hours is quoting a media kit. Most don't, because nobody computes it for them.&lt;/p&gt;

&lt;p&gt;And competitor tracking, which is the use that compounds: run the same twenty channels weekly, watch whose reach is climbing, then look at what they changed in the same window. The content mix and posting hours are right there in the same row.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/lergassy/telegram-channel-analytics" rel="noopener noreferrer"&gt;apify.com/lergassy/telegram-channel-analytics&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>datascience</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hotel Review Data in 2026 — Why Agoda Gives You Two Datasets for the Price of One</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:32:51 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/hotel-review-data-in-2026-why-agoda-gives-you-two-datasets-for-the-price-of-one-38ml</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/hotel-review-data-in-2026-why-agoda-gives-you-two-datasets-for-the-price-of-one-38ml</guid>
      <description>&lt;p&gt;If you're collecting hotel review data, Agoda is the source most people skip and the one with the most on it. Not because it has more reviews than Booking.com, but because it carries &lt;strong&gt;both&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Agoda was bought in 2007 by priceline.com, the company that renamed itself Booking Holdings in 2018 and also owns Booking.com, Kayak and OpenTable. One consequence is visible on almost every hotel page: Agoda displays its own guest reviews &lt;em&gt;and&lt;/em&gt; Booking.com's, served by separate review providers behind the same page.&lt;/p&gt;

&lt;p&gt;A real example, pulled this month:&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;"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;"hotel"&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;"The Magani Hotel And Spa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hotelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;335650&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agodaReviewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1347&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bookingReviewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1025&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;That's 2,372 reviews for one mid-sized Bali hotel as of 4 September 2026, from two review pools with different reviewer bases, reachable through one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hotel review data is harder than it looks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There is no public API.&lt;/strong&gt; Agoda has an affiliate and partner program with data access for approved partners. For everyone else — an analyst, a hotel owner benchmarking competitors, a developer building a comparison tool — there's the website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review counts on the page don't match what the page shows.&lt;/strong&gt; A hotel page displays a paginated slice of reviews. The provider structure isn't visible in the HTML — you have to know that provider &lt;code&gt;332&lt;/code&gt; is Agoda's own pool and &lt;code&gt;3038&lt;/code&gt; is Booking.com's, and request each separately, or you silently collect half the data and never know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The interesting fields aren't in the review text.&lt;/strong&gt; Rating and comment are the easy part. Traveler type, room type booked, length of stay and reviewer country are what make the data analysable, and they're attached to the review record rather than rendered as prose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prices and availability are dynamic; reviews are not.&lt;/strong&gt; This is the part that makes reviews the more useful target. A scraped price is stale in an hour. A review stays true to what its author wrote, give or take the occasional edit or deletion, which means a review dataset compounds while a price dataset decays.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a review record actually contains
&lt;/h2&gt;

&lt;p&gt;Real output, one review, unedited:&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;"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;"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;"hotelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;335650&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hotelName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Magani Hotel And Spa"&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;"agoda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1175631129&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.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;"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;"Magani"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Great location, lovely staff , great breakfast &amp;amp; comfy bed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"September 03, 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkInDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"August 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Natalie"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewerCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"au"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"travelerType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Couple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roomType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deluxe Room Double Bed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lengthOfStay"&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;"reviewProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Agoda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"helpfulVotes"&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="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;Four fields there are worth more than the review text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;reviewerCountry&lt;/code&gt;&lt;/strong&gt; turns a review set into a demand map. Which markets actually book this property, and how do their ratings differ? Australian and Chinese guests rate the same Bali hotel differently, consistently, and if you run a property you need to know in which direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;travelerType&lt;/code&gt;&lt;/strong&gt; — Couple, Solo, Family with young children, Business — segments the ratings. A hotel averaging 8.4 might be a 9.1 for couples and a 6.8 for families, and the average hides the entire problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;roomType&lt;/code&gt;&lt;/strong&gt; attributes complaints to inventory. If the 4-star average is being dragged down by one room category, that's an operational fix, not a marketing one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;lengthOfStay&lt;/code&gt;&lt;/strong&gt; separates the one-night airport stopover from the week-long stay. They are different products reviewed on the same page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;checkInDate&lt;/code&gt;&lt;/strong&gt; matters more than &lt;code&gt;reviewDate&lt;/code&gt;: it tells you when the guest was actually there, which is what you need to correlate a rating drop with a renovation, a management change or a rainy season.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who uses this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hotel operators benchmarking competitors.&lt;/strong&gt; Five properties in the same district, all their reviews for the last two years, segmented by traveler type. The gaps are specific and actionable in a way that a star rating never is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue managers.&lt;/strong&gt; Rating movements lead pricing power. A property whose cleanliness sub-score has fallen half a point over two quarters will lose rate before it loses occupancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Investors and acquirers.&lt;/strong&gt; Reviews are the only public operational data on a private hotel. Trajectory over three years, complaint themes, and whether ratings recovered after the last renovation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Travel product builders.&lt;/strong&gt; Aggregating reviews across sources for a comparison site, a recommendation engine, or a chatbot that answers "quiet hotel in Ubud, good for families" from evidence instead of marketing copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sentiment and NLP work.&lt;/strong&gt; Hotel reviews are unusually good training data: a numeric rating attached to free text, in dozens of languages, with structured metadata about who wrote it.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://apify.com/lergassy/agoda-reviews-scraper" rel="noopener noreferrer"&gt;Agoda Reviews Scraper&lt;/a&gt; takes hotel page URLs and handles both review providers.&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;"startUrls"&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;"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://www.agoda.com/the-magani-hotel-and-spa/hotel/bali-id.html"&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;"maxReviewsPerHotel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewsProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewsSort"&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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reviewsProvider&lt;/code&gt; accepts &lt;code&gt;agoda&lt;/code&gt;, &lt;code&gt;booking&lt;/code&gt; or &lt;code&gt;all&lt;/code&gt;. Use &lt;code&gt;all&lt;/code&gt; unless you specifically want one pool. Note that these are the two large providers, not the only ones — some properties also carry smaller pools from regional partners, which this run doesn't pull.&lt;/p&gt;

&lt;p&gt;From the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/lergassy~agoda-reviews-scraper/runs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "startUrls": [{"url":"https://www.agoda.com/the-magani-hotel-and-spa/hotel/bali-id.html"}],
    "maxReviewsPerHotel": 200,
    "reviewsProvider": "all"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is one &lt;code&gt;hotel&lt;/code&gt; row per property with both review counts, then one &lt;code&gt;review&lt;/code&gt; row per review. Sort options are recent, highest rating and lowest rating — and lowest-first is usually the more useful run, because the complaints cluster and the praise doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't get
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No prices.&lt;/strong&gt; Review extraction and price extraction are separate problems, because prices depend on dates, occupancy and currency. For dated hotel pricing with the rate for your own check-in and check-out, that's &lt;a href="https://apify.com/lergassy/tripcom-scraper" rel="noopener noreferrer"&gt;Trip.com Scraper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No reviewer identity beyond a first name and country.&lt;/strong&gt; That's all Agoda publishes, and it's the right amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hotel replies are hit and miss.&lt;/strong&gt; Agoda does publish management responses, but adoption varies wildly by property — some hotels answer nearly every review, plenty answer none. Don't build an analysis that assumes replies exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review counts vary by provider availability.&lt;/strong&gt; Some properties have a large Agoda pool and almost no Booking.com reviews, or the reverse. The &lt;code&gt;hotel&lt;/code&gt; row reports both counts before any reviews are pulled, so you can check coverage before committing to a large run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build versus buy, honestly
&lt;/h2&gt;

&lt;p&gt;The hard parts here aren't conceptually hard: find the hotel id from a URL slug, call the review endpoint per provider, page through, normalise dates. A competent developer does the first version in a day.&lt;/p&gt;

&lt;p&gt;The cost is the second year. Hotel ids get reassigned, the provider list changes per property, date formats differ by locale, and the endpoint shape shifts without notice. It's not difficult work, it's &lt;em&gt;unscheduled&lt;/em&gt; work, arriving on the day you need the data.&lt;/p&gt;

&lt;p&gt;The actor is free to run while it's new — you pay Apify's platform usage and nothing per review — which makes the build-versus-buy arithmetic hard to argue with. Build it if hotel data is your product. Don't if hotel data is an input to your product.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/lergassy/agoda-reviews-scraper" rel="noopener noreferrer"&gt;apify.com/lergassy/agoda-reviews-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://apify.com/lergassy/tripcom-scraper" rel="noopener noreferrer"&gt;Trip.com Scraper&lt;/a&gt; for dated hotel prices and ratings, &lt;a href="https://apify.com/lergassy/google-flights-scraper" rel="noopener noreferrer"&gt;Google Flights Scraper&lt;/a&gt; for airfare on the same trip.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>api</category>
      <category>data</category>
      <category>travel</category>
    </item>
    <item>
      <title>Google Flights Data Without an API — What You Can Actually Get in 2026</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:32:43 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/google-flights-data-without-an-api-what-you-can-actually-get-in-2026-52c3</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/google-flights-data-without-an-api-what-you-can-actually-get-in-2026-52c3</guid>
      <description>&lt;p&gt;Google Flights is the best flight search on the internet, and one of the few major Google products with no self-serve API attached to it. QPX Express, the public tier that used to do this, was withdrawn on 10 April 2018. The engine itself never went away — ITA's QPX Enterprise is still sold to airlines — but nothing replaced the tier a developer could just sign up for.&lt;/p&gt;

&lt;p&gt;What's left is the page. It's worth more than people assume, because it carries something the airlines themselves don't publish.&lt;/p&gt;

&lt;p&gt;Here is a real run, Bali to Singapore, departing 15 October 2026, pulled this month:&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;"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;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DPS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"departureDate"&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-10-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tripType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"one_way"&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;"flightsFound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lowestPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"typical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typicalPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typicalPriceLow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typicalPriceHigh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceHistory"&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="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-05"&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;54&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;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-06"&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;58&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;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-07"&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;58&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;Look at &lt;code&gt;priceLevel&lt;/code&gt;, &lt;code&gt;typicalPrice&lt;/code&gt; and &lt;code&gt;priceHistory&lt;/code&gt;. Google tracks what this route normally costs and tells you where today sits in that band. $82 against a typical $68, in a normal range of $55 to $110. It's "typical", not a deal.&lt;/p&gt;

&lt;p&gt;No airline gives you that. No aggregator gives you that for free. It's the single most useful field on the page and it exists because Google has been recording every route's fares for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there's no API
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The data isn't simply Google's to hand over.&lt;/strong&gt; By QPX's own documentation the inputs are schedules from OAG, published fares from ATPCO, and availability supplied directly or indirectly by the carriers. Google can compute and display itineraries from those inputs. Reselling the underlying feeds is a different question with a different set of licensors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The industry alternatives are expensive.&lt;/strong&gt; Amadeus and Sabre sell developer access, with per-call pricing, contracts and certification. Duffel and Kiwi are friendlier but priced for booking flows, not research: they expect you to sell tickets, not analyse fares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The page is a heavy client-side app.&lt;/strong&gt; Google Flights renders results in the browser from an internal protocol buffer payload. There's no HTML table of flights to parse, which is why most naive attempts return an empty list and conclude the data isn't there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results are personalised and regional.&lt;/strong&gt; Currency, available carriers and sometimes price depend on where the request comes from. A fare pulled from a US datacenter is not necessarily the fare a user in Jakarta sees, and if you're doing price comparison research that difference is your entire result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get per flight
&lt;/h2&gt;

&lt;p&gt;Each flight in the results is a full itinerary, not a price tag:&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;"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;"flight"&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;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isBestFlight"&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;"airline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scoot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flightNumbers"&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;"TR289"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stops"&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;"departAirport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DPS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"departAt"&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-10-15T14:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arriveAirport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arriveAt"&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-10-15T16:55"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalDurationMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&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 hr 50 min"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"legs"&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;"flightNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TR289"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"airline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scoot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"airlineCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DPS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fromName"&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 Gusti Ngurah Rai International Airport"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"toName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Singapore Changi Airport"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"departAt"&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-10-15T14:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"arriveAt"&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-10-15T16:55"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"durationMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&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;Multi-leg itineraries come back with every leg, its operating carrier, and the layover between them — which matters, because a $40 saving that costs a nine-hour layover in Kuala Lumpur isn't a saving.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;isBestFlight&lt;/code&gt; is Google's own flag combining price, duration and stops. Useful as a shortcut, and useful to &lt;em&gt;ignore&lt;/em&gt; deliberately when you're studying what a route actually costs rather than what Google recommends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's genuinely good for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price monitoring on routes you care about.&lt;/strong&gt; Run the same route daily, keep the results, and &lt;code&gt;priceLevel&lt;/code&gt; plus your own history tells you when to buy. This is the use case that pays for itself in one trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route research.&lt;/strong&gt; Which carriers fly a city pair, how often, at what times. For anyone planning capacity, a travel business, or a relocation, that's a survey nobody publishes in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fare benchmarking for travel agencies.&lt;/strong&gt; Comparing your GDS fare against the public best price on the same route, daily, tells you where your inventory is uncompetitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seasonality studies.&lt;/strong&gt; Run a route across a range of departure dates and the shape of the year appears — the shoulder seasons, the holiday spikes, the exact week a route gets expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visa and immigration paperwork.&lt;/strong&gt; Several consulates want proof of onward travel. Knowing the real price band of the cheapest exit flight, for the right date, is the difference between a $60 ticket and a $300 one.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://apify.com/lergassy/google-flights-scraper" rel="noopener noreferrer"&gt;Google Flights Scraper&lt;/a&gt; takes IATA codes and dates:&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;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DPS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"departureDate"&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-10-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"adults"&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;Round trips take &lt;code&gt;returnDate&lt;/code&gt;. Several routes in one run go in &lt;code&gt;routes&lt;/code&gt;. Open-jaw and multi-city itineraries go in &lt;code&gt;multiCityLegs&lt;/code&gt;. Cabin class, passenger mix and currency are all inputs.&lt;/p&gt;

&lt;p&gt;From the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/lergassy~google-flights-scraper/runs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"origin":"DPS","destination":"SIN","departureDate":"2026-10-15","adults":1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is one &lt;code&gt;search&lt;/code&gt; row per query — with the price band, the history and the totals — followed by one &lt;code&gt;flight&lt;/code&gt; row per itinerary. The search row alone answers "is this cheap right now", which for monitoring is the only row you need to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't get
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No booking.&lt;/strong&gt; This is research data. Booking requires an airline or an OTA, and the fare you saw may be gone by the time you get there, which is normal for airline pricing rather than a defect in the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No seat availability or fare rules.&lt;/strong&gt; Change fees, baggage allowance and refundability aren't on the results page. They live in the fare rules, which are a GDS product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prices are indicative.&lt;/strong&gt; Google shows fares from its partners, and the same flight can be cheaper direct or on a regional OTA. Treat the number as a benchmark, not a quote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personalisation is real.&lt;/strong&gt; If you need the fare a specific market sees, the request has to originate in that market. Comparing "the DPS–SIN price" across regions requires deciding which region you mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest build-versus-buy note
&lt;/h2&gt;

&lt;p&gt;Parsing Google Flights is not a weekend project. The payload is a nested protocol buffer with no field names, positional rather than keyed, and the positions move when Google ships a change. Every published parser is a set of magic array indices that somebody re-derived by hand after the last break.&lt;/p&gt;

&lt;p&gt;If flight data is your core product, you'll end up owning that maintenance regardless. If it's one input among many, someone else's parser is the right answer — this one is currently free to run, with only Apify's platform usage to pay — and the two hours you'd spend on the first version are better spent on what you do with the numbers.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/lergassy/google-flights-scraper" rel="noopener noreferrer"&gt;apify.com/lergassy/google-flights-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://apify.com/lergassy/agoda-reviews-scraper" rel="noopener noreferrer"&gt;Agoda Reviews Scraper&lt;/a&gt; and &lt;a href="https://apify.com/lergassy/tripcom-scraper" rel="noopener noreferrer"&gt;Trip.com Scraper&lt;/a&gt; for the hotel half of the same trip.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>travel</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Every US Company That Registered This Week Is Public Data. Here's How to Read It</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:32:36 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/every-us-company-that-registered-this-week-is-public-data-heres-how-to-read-it-304o</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/every-us-company-that-registered-this-week-is-public-data-heres-how-to-read-it-304o</guid>
      <description>&lt;p&gt;A company that registered on Monday has no accountant, no insurance broker, no payroll provider, no bank relationship, no website and no marketing agency. Four weeks later it has most of those, chosen under time pressure from whoever reached it first.&lt;/p&gt;

&lt;p&gt;That window is the most valuable one in B2B sales, and the list of who's in it is public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three feeds, three different kinds of company
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;State registries.&lt;/strong&gt; Every US state maintains a business registry, though not always under a Secretary of State — Arizona runs it through its Corporation Commission, Michigan through LARA, Virginia through the State Corporation Commission, and Alaska, Hawaii and Utah have no Secretary of State at all. Many publish the registry as open data on a Socrata portal, the platform behind a large share of municipal open-data sites. New LLCs and corporations appear within days of filing. Connecticut is the standout: nearly every new company comes with the email address the founder gave the state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Municipal licence feeds.&lt;/strong&gt; Cities publish business licences as they're issued. This catches a different population — the restaurant, the salon, the contractor, businesses that need a licence to open rather than a corporate entity to exist. Owner names and phone numbers appear here far more often than in state data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEC EDGAR Form D.&lt;/strong&gt; When a company raises private capital under Regulation D, it files a notice with the SEC within fifteen days of the first sale. That filing is public and structured, and it names the amount raised, the date of first sale, the number of investors, and the executive officers with a company phone number.&lt;/p&gt;

&lt;p&gt;Form D is the highest-intent feed of the three. A company that just closed a round is about to hire, buy software and outsource functions, and it has money on the day you call.&lt;/p&gt;

&lt;p&gt;Here's an actual Form D record from a run this month:&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;"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;"signal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FUNDING_ROUND"&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;"SEC EDGAR Form D"&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;"AI Planning Logic 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;"cik"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2090528"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corporation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jurisdictionOfInc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DELAWARE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"yearOfIncorporation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"incorporatedWithinFiveYears"&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;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"16192 COASTAL HIGHWAY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LEWES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"postalCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19958"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contactPhone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+1707888XXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"industry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Other Technology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"industryTags"&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;"Technology &amp;amp; Software"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateOfFirstSale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-12-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fundingAmountSold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"investorCount"&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="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;$240,000 raised from three investors, a Delaware corporation under five years old, in technology, with a phone number on the filing. That's a qualified lead assembled entirely from a government record. (The phone is masked here; it's public in EDGAR.)&lt;/p&gt;

&lt;p&gt;One caveat visible in that very record: &lt;code&gt;dateOfFirstSale&lt;/code&gt; is December 2025, while the filing itself is recent. Form D is filed within fifteen days of the &lt;em&gt;first&lt;/em&gt; sale, but amendments and late filings both exist, so read the sale date rather than assuming the filing date is the event date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't as easy as "the data is public"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every source has a different schema.&lt;/strong&gt; Connecticut's field names have nothing to do with Chicago's, which have nothing to do with EDGAR's XML. Normalising forty sources into one row shape is the actual work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Registries are full of things that aren't new businesses.&lt;/strong&gt; Biennial statements, renewals, amendments, name changes, dissolutions and reinstatements sit in the same feed as formations. A naive pull of "recent filings in New York" returns mostly companies that have existed for a decade. The filing type has to be filtered, and the vocabulary differs by state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same business appears more than once.&lt;/strong&gt; A company files with the state and then takes a city licence. Two rows, one business. Deduplication has to survive punctuation, "LLC" versus "L.L.C.", and suite numbers written four ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dates lie.&lt;/strong&gt; Future-dated filings, typo'd years, and registries that publish the &lt;em&gt;processing&lt;/em&gt; date rather than the &lt;em&gt;filing&lt;/em&gt; date. If freshness is your entire value proposition, you have to defend it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contact fields are unverified.&lt;/strong&gt; Registries publish what the filer typed. Nobody checks it. A raw registry feed contains dead numbers and misspelled email domains at a meaningful rate, and each one costs you sender reputation or a wasted dial.&lt;/p&gt;

&lt;p&gt;That last one is worth solving properly: every phone in this feed is validated against the North American Numbering Plan and every email domain gets a live MX lookup plus disposable and role-address flags, before the row reaches you. A number in a reserved exchange or a domain with no mail server is labelled, not sold to you as a lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Volumes, measured
&lt;/h2&gt;

&lt;p&gt;Actual output over a seven-day window:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feed&lt;/th&gt;
&lt;th&gt;Volume&lt;/th&gt;
&lt;th&gt;Contact coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New local businesses, 9 cities&lt;/td&gt;
&lt;td&gt;~190&lt;/td&gt;
&lt;td&gt;100% with phone, ~30% with owner name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Connecticut companies&lt;/td&gt;
&lt;td&gt;~860&lt;/td&gt;
&lt;td&gt;100% with email, 99% with NAICS code, 95% with a usable address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funded startups (Form D)&lt;/td&gt;
&lt;td&gt;~250&lt;/td&gt;
&lt;td&gt;94% with company phone, 100% with an officer's name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;About 1,300 qualified new businesses a week from three presets. Not a database of everyone — a feed of who is new.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Anyone selling to new businesses.&lt;/strong&gt; Accountants, bookkeepers, insurance brokers, payroll, POS systems, business banking, commercial cleaning, signage. The entire category of vendor a business chooses once, in its first month, and then keeps for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agencies and web developers.&lt;/strong&gt; A newly registered company with no website is the least ambiguous lead in existence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B2B SaaS selling to funded startups.&lt;/strong&gt; Form D plus a five-year incorporation flag plus an amount raised is a better qualification filter than anything in a generic lead database, and it's fresher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commercial real estate and equipment.&lt;/strong&gt; New licences in a city are the leading indicator for space and fit-out demand, by industry and by ZIP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Market researchers.&lt;/strong&gt; Formation rates by state, city and industry give you the composition behind the headline number — the Census Bureau publishes Business Formation Statistics weekly, but not broken down the way your question needs it.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://apify.com/lergassy/us-business-filings" rel="noopener noreferrer"&gt;US New Business Leads&lt;/a&gt; ships with three presets so the first run needs no configuration:&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;"preset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"funded_startups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxResultsPerState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom targeting takes states, cities, industries, a date floor, and contact-quality filters:&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;"states"&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;"CT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NY"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registeredSince"&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-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requireContact"&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;"contactQuality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deliverable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monitorMode"&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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;monitorMode&lt;/code&gt; is what turns it from a scraper into a feed: it remembers what it has already delivered and returns only businesses you haven't seen. Put it on a daily schedule and each run is that day's new companies, deduplicated against every previous run.&lt;/p&gt;

&lt;p&gt;From the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/lergassy~us-business-filings/runs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"preset":"funded_startups","maxResultsPerState":100}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row carries a lead score from 0 to 100 built from freshness, contact completeness, address quality and entity type. Sort by it, or set a floor and never look at the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't get
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not every state.&lt;/strong&gt; Coverage follows which registries publish machine-readable open data, and several large states don't. Where a state is missing, it's missing because the data isn't published, not because it's hard to scrape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No decision-maker names beyond what's filed.&lt;/strong&gt; State filings name a registered agent, sometimes an organiser. Form D names executive officers. Neither gives you a VP of Engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No revenue, headcount or tech stack.&lt;/strong&gt; This is a formation feed, not an enrichment product. It tells you a company exists and is new. What it does is a separate lookup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No email for most municipal records.&lt;/strong&gt; Cities publish phones far more reliably than emails. Connecticut is the exception that makes email-first outreach viable at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that makes it work
&lt;/h2&gt;

&lt;p&gt;Freshness is the entire product. A newly-registered-business list that's six weeks old is a list of companies that already chose a vendor, and it's worth close to nothing.&lt;/p&gt;

&lt;p&gt;Which is why this belongs on a schedule rather than in a one-off run, and why deduplication across runs matters more than volume in any single one. Reaching a business in week one is a different conversation from reaching it in week seven.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/lergassy/us-business-filings" rel="noopener noreferrer"&gt;apify.com/lergassy/us-business-filings&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://apify.com/lergassy/email-phone-verifier" rel="noopener noreferrer"&gt;Email &amp;amp; Phone Verifier&lt;/a&gt; runs the same checks over any contact list you already have, including the output of another actor.&lt;/p&gt;

</description>
      <category>api</category>
      <category>data</category>
      <category>sales</category>
      <category>python</category>
    </item>
    <item>
      <title>I Measured 900 Apify Actors and Got the Price Wrong by 100x. Here's the Corrected Data</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:32:29 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/i-measured-900-apify-actors-and-got-the-price-wrong-by-100x-heres-the-corrected-data-3418</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/i-measured-900-apify-actors-and-got-the-price-wrong-by-100x-heres-the-corrected-data-3418</guid>
      <description>&lt;p&gt;I set out to answer a boring question with data instead of opinion: on the Apify Store, who actually makes money, and from what?&lt;/p&gt;

&lt;p&gt;I got an answer, published it internally, and then discovered my central example was wrong by a factor of a hundred. This is the corrected version, including the mistake, because the mistake is the most useful thing in here for anyone doing the same measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method, and its two limits
&lt;/h2&gt;

&lt;p&gt;Two public endpoints do all the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /v2/store?limit=1000&amp;amp;offset=N&lt;/code&gt; — the catalogue&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /v2/acts/{username}~{name}&lt;/code&gt; — pricing, creation date, monthly users, monthly run counts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revenue estimate per actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runs_30d × price_per_primary_event × 0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 0.8 is the developer's share. This is a lower bound: it assumes one billable result per run, and most runs produce more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limit one: you cannot crawl the whole store.&lt;/strong&gt; The endpoint reports a &lt;code&gt;total&lt;/code&gt; of about 56,000 and Apify's own store page advertises more than 67,000 actors. But it stops returning records past an offset of roughly 16,000, and every page comes back short of the limit you asked for. Deduplicated, I ended up with &lt;strong&gt;13,190 actors&lt;/strong&gt; — under a quarter of the catalogue, and biased toward whatever the default ranking puts first. So: no store-wide survival rates from me. What follows is about the &lt;strong&gt;900 most-used actors&lt;/strong&gt;, enriched individually, which is where essentially all the revenue is anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limit two is the interesting one.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pricing trap that makes every number wrong
&lt;/h2&gt;

&lt;p&gt;Pay-per-event actors can price each event in &lt;strong&gt;tiers&lt;/strong&gt;, keyed to the customer's Apify plan. The API returns them like this:&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="nl"&gt;"eventTieredPricingUsd"&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;"FREE"&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;"tieredEventPriceUsd"&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.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;span class="nl"&gt;"BRONZE"&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;"tieredEventPriceUsd"&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.001&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;"SILVER"&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;"tieredEventPriceUsd"&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.0008&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;"GOLD"&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;"tieredEventPriceUsd"&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.0006&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;"PLATINUM"&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;"tieredEventPriceUsd"&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.0006&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;"DIAMOND"&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;"tieredEventPriceUsd"&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.0006&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;Read the first key and you conclude this actor charges &lt;strong&gt;$100 per 1,000 results&lt;/strong&gt;. It doesn't. &lt;code&gt;FREE&lt;/code&gt; is a deterrent price for people on the free plan — who, as Apify's own docs state, generate no developer revenue at all. Every paying customer is on &lt;code&gt;BRONZE&lt;/code&gt; or better: &lt;strong&gt;$1 per 1,000&lt;/strong&gt;, a hundred times less.&lt;/p&gt;

&lt;p&gt;My original write-up leaned on exactly this actor as proof that expensive actors win. I had it at $8,600 a month. Corrected, it's about $86.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;60% of the top 900 actors use tiered pricing&lt;/strong&gt;, and 467 of them have a &lt;code&gt;FREE&lt;/code&gt; tier priced above their paid tiers. If you take the first key, your revenue total comes out &lt;strong&gt;21% too high&lt;/strong&gt; and individual actors can be off by up to 100×. The fix is one line — take the maximum across the non-&lt;code&gt;FREE&lt;/code&gt; tiers — but you have to know the trap is there.&lt;/p&gt;

&lt;p&gt;Everything below is computed the corrected way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who takes the money
&lt;/h2&gt;

&lt;p&gt;Five accounts — &lt;code&gt;apify&lt;/code&gt;, &lt;code&gt;compass&lt;/code&gt;, &lt;code&gt;clockworks&lt;/code&gt;, &lt;code&gt;junglee&lt;/code&gt;, &lt;code&gt;lukaskrivka&lt;/code&gt; — are Apify's own and Apify-affiliated. Among the 900 most-used actors they hold &lt;strong&gt;48% of the users and 44% of the revenue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the single most important correction to make to any niche research. Instagram and Google Maps look enormous until you exclude those accounts, and then most of the money vanishes, because it never reached a third-party developer.&lt;/p&gt;

&lt;p&gt;Of roughly $400,000 a month across these 900 actors, about &lt;strong&gt;$224,000 is left for everyone else&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding that changed how I build
&lt;/h2&gt;

&lt;p&gt;Third-party actors only, grouped by what they charge per 1,000 results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Price per 1k&lt;/th&gt;
&lt;th&gt;Actors&lt;/th&gt;
&lt;th&gt;Total/month&lt;/th&gt;
&lt;th&gt;Median actor&lt;/th&gt;
&lt;th&gt;Median runs/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;under $1&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;td&gt;$11,409&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18,876&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$1–3&lt;/td&gt;
&lt;td&gt;267&lt;/td&gt;
&lt;td&gt;$24,940&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5,748&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$3–6&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;$82,982&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$24&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7,153&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$6–15&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;$25,018&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3,221&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;over $15&lt;/td&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;td&gt;$79,725&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$111&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,799&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the last two columns together. Actors priced over $15 per 1,000 do &lt;strong&gt;one seventh the traffic&lt;/strong&gt; of the sub-$1 group and make &lt;strong&gt;twenty-two times more money&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The cheap end is a logistics business. 193 actors grinding through nearly nineteen thousand runs a month each, to earn five dollars.&lt;/p&gt;

&lt;p&gt;The top earner in the whole sample is a LinkedIn profile search priced at &lt;strong&gt;$100 per 1,000 search-page results&lt;/strong&gt; — genuinely, at every paid tier — doing 651,000 events a month for about &lt;strong&gt;$52,000&lt;/strong&gt;. Four of the top twelve earners are LinkedIn actors, three of them third-party.&lt;/p&gt;

&lt;p&gt;The mechanism is not clever. A buyer prices your scraper against the cheapest scraper doing the same thing. A buyer prices your enrichment tool against ZoomInfo, Hunter and Snov, which charge in dollars per contact. Same engineering, different comparison set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What $500 a month actually requires
&lt;/h2&gt;

&lt;p&gt;Pure arithmetic, after the 20% platform cut:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Price per 1,000&lt;/th&gt;
&lt;th&gt;Rows needed per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;td&gt;625,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;td&gt;156,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;31,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;6,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;3,100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 200× spread in what you have to build and operate for the same money.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many actually get there
&lt;/h2&gt;

&lt;p&gt;Among the 829 third-party actors in the top 900 by usage — the most-used quarter of the store's most-used slice, so this is the optimistic end of the distribution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Actors clearing it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;any revenue at all&lt;/td&gt;
&lt;td&gt;827&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$100/month&lt;/td&gt;
&lt;td&gt;175&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$500/month&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$1,000/month&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Median monthly revenue among those that earn anything: &lt;strong&gt;$14&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Remember these are the best-used actors in the store. The tail below them is far worse, and I can't measure it, because the catalogue endpoint won't page that far.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runs per user, not users
&lt;/h2&gt;

&lt;p&gt;Median runs per user across the sample sits between 15 and 90 depending on the niche.&lt;/p&gt;

&lt;p&gt;Hundreds of runs per user means the actor is wired into a pipeline or called in a loop by an agent. Under ten means people tried it once and left, however good the user count looks on the store page. It's the cheapest health check you can compute and almost nobody looks at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanics that gate all of this
&lt;/h2&gt;

&lt;p&gt;Worth knowing before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;80% of revenue, minus platform usage costs.&lt;/strong&gt; The split is confirmed in the API as a 20% margin, but Apify's terms deduct platform costs from your share, not from the gross. Compute per run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only paying customers count.&lt;/strong&gt; Free-plan usage earns nothing, which is exactly why the &lt;code&gt;FREE&lt;/code&gt; pricing tier exists and exactly why reading it as your price is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payout minimum: $20 for PayPal and Wise, $100 for other methods.&lt;/strong&gt; Below the threshold, the balance rolls over month to month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KYC is mandatory&lt;/strong&gt; before any payout: ID, proof of address, tax data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money is slow.&lt;/strong&gt; Invoices generate on the 11th, auto-approve on the 14th, payment follows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Significant price changes take 14 days&lt;/strong&gt; to reach existing users — a switch of model, a price increase, a new paid event. Reductions apply immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rental pricing dies on 30 September 2026.&lt;/strong&gt; Anything not migrated to pay-per-event collects nothing after that date.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trend to plan against
&lt;/h2&gt;

&lt;p&gt;Apify has published its developer payouts periodically: about $563,000 a month in September 2025, and $1.5 million a month as of August 2026. That's roughly a 2.7× gain in eleven months.&lt;/p&gt;

&lt;p&gt;I'd be careful with any tighter extrapolation than that — the earlier figures I originally used came from banner text and undated tweets, and the banners sat unchanged for months at a time, so a smooth curve fitted through them is fiction. The direction is solid and the driver Apify names is explicit: actors used as tools by AI agents.&lt;/p&gt;

&lt;p&gt;Meanwhile new actors keep arriving faster than the pie grows. Which brings it back to the price table — that race isn't won on volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself at the start
&lt;/h2&gt;

&lt;p&gt;Check demand before feasibility. I built several actors for sources that were technically satisfying and commercially empty — open government data is the sharpest version of this, where the API answers instantly, there's no anti-bot, nobody has taken the niche, and the niche leader has three users.&lt;/p&gt;

&lt;p&gt;Price against the incumbent SaaS, not against the cheapest scraper.&lt;/p&gt;

&lt;p&gt;Pick one vertical and stay in it. My own profile still shows the opposite — travel, Southeast Asian marketplaces, contact verification, B2B lead feeds — which is three themes too many, and cross-linking them is a patch, not a fix.&lt;/p&gt;

&lt;p&gt;And verify your own measurement before you build a strategy on it. Mine survived, but only after the correction: the shape of the price table is the same, the magnitudes were not.&lt;/p&gt;

&lt;p&gt;The actors are at &lt;a href="https://apify.com/lergassy" rel="noopener noreferrer"&gt;apify.com/lergassy&lt;/a&gt;. The measurement is two endpoints and an afternoon — go compute it for your own niche, and take the maximum of the paid tiers.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>data</category>
      <category>startup</category>
      <category>api</category>
    </item>
    <item>
      <title>Scraping Southeast Asian Marketplaces in 2026 — Shopee and Tokopedia Without a Browser</title>
      <dc:creator>Nikita Iakovlev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:30:43 +0000</pubDate>
      <link>https://dev.to/nikita_iakovlev_415524c19/scraping-southeast-asian-marketplaces-in-2026-shopee-and-tokopedia-without-a-browser-1efk</link>
      <guid>https://dev.to/nikita_iakovlev_415524c19/scraping-southeast-asian-marketplaces-in-2026-shopee-and-tokopedia-without-a-browser-1efk</guid>
      <description>&lt;p&gt;Southeast Asia's e-commerce market runs on platforms most Western developers have never opened. Shopee is the region's largest marketplace and also operates in Taiwan and Brazil. Tokopedia is Indonesia's best-known domestic marketplace, now 75% owned by ByteDance and merged with TikTok Shop. Neither has a public product API, and both moved to client-side rendering, which is where most scraping attempts stop.&lt;/p&gt;

&lt;p&gt;They shouldn't. Here's a product pulled this month over plain HTTP, no browser, no token:&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;"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;"product"&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;"Oleh Oleh Bali - Kopi Bubuk Bali Cap Kupu Kupu Bola Dunia kemasan baru 400 Gram"&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://shopee.co.id/...-i.21681717.6213583834"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shopId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"21681717"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6213583834"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"marketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Indonesia"&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;"IDR"&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;74700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&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.9&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;"Denpasar"&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;"searchTerm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kopi bali"&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;
  
  
  The rendering problem, and the way around it
&lt;/h2&gt;

&lt;p&gt;In mid-2026 Shopee moved product pages to a client-rendered application whose internal API is guarded by a JavaScript-signed token. The token is computed in the browser. Reproducing it means either running the JavaScript or reverse-engineering a signing routine that changes.&lt;/p&gt;

&lt;p&gt;This is why most Shopee tooling today returns only what a product card shows, and marks description, brand and image gallery as "browser only". It's also why those tools are expensive per row — a headless browser costs orders of magnitude more compute than an HTTP request.&lt;/p&gt;

&lt;p&gt;The way around it is that &lt;strong&gt;Shopee still serves a server-rendered version of the same page to crawlers&lt;/strong&gt;. Search results, category pages and shop pages come back as HTML with the full product card in it: title, price, original price, discount, rating, units sold, shipping location, badges, image. No browser, no token, no login.&lt;/p&gt;

&lt;p&gt;A search for forty products finishes in under a minute on a few megabytes of traffic. The same run through a headless browser burns orders of magnitude more compute and is more likely to be blocked, because a browser fingerprint is a much larger attack surface than an HTTP GET.&lt;/p&gt;

&lt;p&gt;Tokopedia is a different shape with the same conclusion. Its review data lives behind its own data endpoint, which answers directly. Thousands of reviews come back in minutes without rendering anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight live Shopee marketplaces
&lt;/h2&gt;

&lt;p&gt;This is the part that makes Shopee worth more than a single-country scraper:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Currency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Indonesia&lt;/td&gt;
&lt;td&gt;shopee.co.id&lt;/td&gt;
&lt;td&gt;IDR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singapore&lt;/td&gt;
&lt;td&gt;shopee.sg&lt;/td&gt;
&lt;td&gt;SGD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malaysia&lt;/td&gt;
&lt;td&gt;shopee.com.my&lt;/td&gt;
&lt;td&gt;MYR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philippines&lt;/td&gt;
&lt;td&gt;shopee.ph&lt;/td&gt;
&lt;td&gt;PHP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thailand&lt;/td&gt;
&lt;td&gt;shopee.co.th&lt;/td&gt;
&lt;td&gt;THB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vietnam&lt;/td&gt;
&lt;td&gt;shopee.vn&lt;/td&gt;
&lt;td&gt;VND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taiwan&lt;/td&gt;
&lt;td&gt;shopee.tw&lt;/td&gt;
&lt;td&gt;TWD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brazil&lt;/td&gt;
&lt;td&gt;shopee.com.br&lt;/td&gt;
&lt;td&gt;BRL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shopee's Latin American footprint has been shrinking: Argentina and the Mexican local marketplace went in 2022, and &lt;strong&gt;Chile and Colombia closed on 30 October 2025&lt;/strong&gt;, leaving cross-border only. Watch out for this — &lt;code&gt;shopee.cl&lt;/code&gt; and &lt;code&gt;shopee.com.co&lt;/code&gt; still answer with HTTP 200, because a shared CDN shell responds whether or not there's a marketplace behind it. A status code is not evidence that a storefront exists; a product row is. Requesting a Chilean search through the actor now returns an upstream error, which is the honest answer.&lt;/p&gt;

&lt;p&gt;Same product, eight catalogues, eight price levels, eight sets of competitors. Every row carries &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;marketplace&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt;, so one dataset can hold several countries side by side without a currency column guessing game. Prices come back in the marketplace's own currency, never converted — conversion is a decision with a date attached and doesn't belong in extraction.&lt;/p&gt;

&lt;p&gt;The cross-market comparison is the analysis nobody does. A product selling at IDR 74,700 in Indonesia and its equivalent in Vietnam and the Philippines tells you which market a seller is underpricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tokopedia reviews look like
&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;"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;"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;"shopName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Makarizo Advisor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2289257709"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Semoga cocok yaaa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hari ini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P***w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isAnonymous"&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;"sellerReply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"productName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Makarizo Advisor Hair &amp;amp; Scalp Tonic 60mL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productUrl"&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.tokopedia.com/makarizo-advisor/..."&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;Two details worth knowing before you build on this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buyer names are masked at the source.&lt;/strong&gt; &lt;code&gt;P***w&lt;/code&gt; is what Tokopedia publishes, not what the scraper redacted. There is no personal data to collect here even if you wanted it, which makes this dataset unusually clean from a compliance standpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Times are relative.&lt;/strong&gt; &lt;code&gt;Hari ini&lt;/code&gt; means "today". &lt;code&gt;2 hari lalu&lt;/code&gt; means two days ago. If you're building a time series you have to resolve those against the scrape timestamp at collection time, because the phrase stops being meaningful the moment you store it.&lt;/p&gt;

&lt;p&gt;Review text is mostly Indonesian, which is a feature for anyone doing multilingual sentiment work and a problem for anyone assuming English.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually buys this data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sellers benchmarking price.&lt;/strong&gt; The single most common use. What are the twenty competing listings for my SKU priced at today, in each market, with what discount and what rating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brands watching unauthorised resellers.&lt;/strong&gt; Search your own brand across all eight marketplaces and see who's listing it, at what price, from which warehouse. Grey-market pricing destroys distributor relationships and is invisible from the brand's side without this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product research for sourcing.&lt;/strong&gt; Units sold, rating and review counts across a category tell you where demand actually sits, before committing inventory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Market entry research.&lt;/strong&gt; A brand deciding whether to launch in Vietnam or the Philippines wants the current price and competitor density in both. Two runs, one afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review mining.&lt;/strong&gt; Tokopedia's reviews are a large public corpus of Indonesian-language consumer opinion, attached to specific products and star ratings. For a brand selling in Indonesia, that's the actual voice of the customer, and it's not on Twitter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running them
&lt;/h2&gt;

&lt;p&gt;Shopee 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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchTerms"&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;"kopi bali"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokopedia reviews, an entire shop at a time:&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;"startUrls"&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;"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://www.tokopedia.com/makarizo-advisor"&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;"maxReviewsPerShop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewsSort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newest"&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;From the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/lergassy~shopee-scraper/runs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"mode":"search","country":"ID","searchTerms":["kopi bali"],"maxItems":50}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shopee also takes &lt;code&gt;startUrls&lt;/code&gt; for category, shop or search URLs you already have, and a &lt;code&gt;productDetail&lt;/code&gt; mode that takes &lt;code&gt;shopId/itemId&lt;/code&gt; pairs. Tokopedia takes sort order and star-rating filters, including photos-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't get
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shopee product descriptions and galleries are partial.&lt;/strong&gt; The listing pages carry the full card. The detail page carries the category path and gallery size, and the full description only when Shopee serves its structured record for that product. This is the residue of the token problem above, and any tool claiming complete detail data over HTTP for every product is claiming something I couldn't reproduce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No stock quantities.&lt;/strong&gt; Neither platform publishes real inventory, only "units sold" as a cumulative badge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No seller contact details.&lt;/strong&gt; Shop pages give a name, rating and response rate. Anything beyond that isn't published.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits are real.&lt;/strong&gt; These are large platforms with anti-bot infrastructure. Modest concurrency works; hammering a search endpoint gets you a challenge page instead of JSON, and the correct response is to slow down rather than to escalate to a browser.&lt;/p&gt;

&lt;p&gt;That last point deserves a caveat you rarely see in posts like this one: on platforms in this region, an endpoint that answers cleanly today can start returning an anti-bot page next month with no warning and no change on your side. It has happened to me on a neighbouring marketplace while writing this. Build the retry and the alert before you build the analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The niche, honestly
&lt;/h2&gt;

&lt;p&gt;E-commerce product scraping is one of the most crowded categories on the Apify Store — 147 new actors in six months, median price around $4 per 1,000 rows, and roughly $6,300 a month in total third-party revenue across the whole category. It is not where the money is.&lt;/p&gt;

&lt;p&gt;What's differentiated here isn't "scrapes an e-commerce site". It's eight marketplaces in one dataset, no browser in the pipeline, and an Indonesian-language review corpus that the English-speaking tooling ecosystem largely ignores. If you're building in this category, that's the level the difference has to exist at.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actors:&lt;/strong&gt; &lt;a href="https://apify.com/lergassy/shopee-scraper" rel="noopener noreferrer"&gt;Shopee Scraper&lt;/a&gt; · &lt;a href="https://apify.com/lergassy/tokopedia-reviews-scraper" rel="noopener noreferrer"&gt;Tokopedia Reviews Scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://apify.com/lergassy/mercadolibre-scraper" rel="noopener noreferrer"&gt;Mercado Libre Scraper&lt;/a&gt; for the same job across Latin America.&lt;/p&gt;

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