<?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: Yaobuilds</title>
    <description>The latest articles on DEV Community by Yaobuilds (@yaobuilds).</description>
    <link>https://dev.to/yaobuilds</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%2F3915397%2Fa737ab8a-b4f1-49e2-a17c-b5907ea3b64c.jpeg</url>
      <title>DEV Community: Yaobuilds</title>
      <link>https://dev.to/yaobuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yaobuilds"/>
    <language>en</language>
    <item>
      <title>Turn any newsletter into a database</title>
      <dc:creator>Yaobuilds</dc:creator>
      <pubDate>Wed, 05 Aug 2026 21:25:50 +0000</pubDate>
      <link>https://dev.to/yaobuilds/turn-any-newsletter-into-a-database-27nn</link>
      <guid>https://dev.to/yaobuilds/turn-any-newsletter-into-a-database-27nn</guid>
      <description>&lt;p&gt;Newsletters are the strangest data feed on the internet. Airlines, deal hunters, job boards, real estate brokers, security teams: thousands of organizations run pipelines that end in a beautifully formatted email, and nowhere else. There is no API. There is no RSS. There is a designer-approved HTML table in your inbox, and that is the product.&lt;/p&gt;

&lt;p&gt;Which means your inbox is quietly one of the best free data sources you have. You just cannot query it.&lt;/p&gt;

&lt;p&gt;This tutorial fixes that. By the end, a flight-deals newsletter will be landing in a Postgres-queryable event stream as typed JSON, and the whole thing takes about fifteen minutes. Disclosure up front: I built the service used in the middle step (Inbin), after running the DIY version inside my own product first. The pattern works with any extraction layer, including one you build yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Newsletter (Going, TLDR, anything)
        ↓  subscribes to
inbox address (yours-a3f2c8@in.inbin.dev)
        ↓  extraction against your schema
typed JSON event
        ↓  HMAC-signed webhook
your app / database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is the middle: instead of your personal email address, you subscribe the newsletter to a machine address, and every edition that arrives is parsed against a schema you declare once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: create an inbox
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Inbin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inbin/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inbin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Inbin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INBIN_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inboxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flight-deals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// flight-deals-a3f2c8@in.inbin.dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That address is permanent. It is also receive-only, which matters: the worst a leaked newsletter subscription can do is send you more newsletters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: declare what every edition should become
&lt;/h2&gt;

&lt;p&gt;This is the part that replaces the parser you would otherwise write. Describe the fields; do not describe how to find them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;destination_city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;origin_iata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;airline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;travel_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;hallucination_guard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design notes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arrays are the right default for newsletters.&lt;/strong&gt; One edition usually carries many records (five deals, ten stories, three listings). Declaring &lt;code&gt;deals&lt;/code&gt; as an array means one email becomes five rows, not one blob.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn the hallucination guard on.&lt;/strong&gt; Extraction here is LLM-based, and language models will confidently invent a price if you let them. The guard checks that every extracted string appears verbatim in the source email, and every number in its digits; anything it cannot back comes back null. Missing data is recoverable. Invented data is poison in a database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: subscribe the newsletter
&lt;/h2&gt;

&lt;p&gt;Go to the newsletter's signup page and enter the inbox address. That is the whole step. Some senders require confirming the subscription; the confirmation email also arrives as an event, so you can grab the confirm link from your dashboard or the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: receive rows, not emails
&lt;/h2&gt;

&lt;p&gt;Point your webhook at an endpoint and verify the signature against the raw body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyWebhook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inbin/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INBIN_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deal&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extracted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deals&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`
      INSERT INTO deals (city, price_usd, airline, received_at)
      VALUES (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination_city&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,
              &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;airline&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received_at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every edition of the newsletter now becomes rows in your table, minutes after it is sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: skip the table entirely (optional)
&lt;/h2&gt;

&lt;p&gt;If you do not want to run a database for this, the events are already queryable where they land:&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 https://api.inbin.dev/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$INBIN_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "flatten": "deals",
    "where": [{ "field": "price_usd", "op": "lt", "value": 300 }],
    "group_by": "destination_city",
    "aggregate": [{ "fn": "avg", "field": "price_usd", "as": "avg_price" }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"rows"&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;"destination_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;"Lisbon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"avg_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;274.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;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;Filter, sort, group, aggregate: the inbox is the database.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price watchers&lt;/strong&gt;: alert when any deal to a saved city drops under a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A personal news API&lt;/strong&gt;: three newsletters in, one deduplicated JSON feed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market history&lt;/strong&gt;: newsletters are ephemeral; a table of every deal ever sent is not. Six months of data tells you what a genuinely good price looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent food&lt;/strong&gt;: the same events are exposed over MCP, so an AI agent can query them natively. That one gets its own article.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general lesson holds beyond newsletters: any email your business receives on a schedule (invoices, shipping updates, alerts) is a feed pretending to be correspondence. Declare a schema and it stops pretending.&lt;/p&gt;

&lt;p&gt;If you want to try the hosted version, it is free while in beta at &lt;a href="https://inbin.dev" rel="noopener noreferrer"&gt;inbin.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>SES vs Mailgun vs SendGrid vs Postmark: inbound email compared</title>
      <dc:creator>Yaobuilds</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:47:58 +0000</pubDate>
      <link>https://dev.to/yaobuilds/ses-vs-mailgun-vs-sendgrid-vs-postmark-inbound-email-compared-ooi</link>
      <guid>https://dev.to/yaobuilds/ses-vs-mailgun-vs-sendgrid-vs-postmark-inbound-email-compared-ooi</guid>
      <description>&lt;p&gt;Four services will accept email for your domain and call your webhook. They differ in one dimension that matters more than any pricing table: how much of the message they digest before it reaches you, and how much is still your problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-table version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Your endpoint receives&lt;/th&gt;
&lt;th&gt;MIME parsing&lt;/th&gt;
&lt;th&gt;Webhook authentication&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS SES&lt;/td&gt;
&lt;td&gt;Raw MIME (usually via S3 + Lambda/SNS)&lt;/td&gt;
&lt;td&gt;None, you parse&lt;/td&gt;
&lt;td&gt;Standard AWS auth (IAM, SNS signatures)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailgun Routes&lt;/td&gt;
&lt;td&gt;Form fields (subject, body-plain, stripped-text...)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;HMAC signature on each POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid Inbound Parse&lt;/td&gt;
&lt;td&gt;Multipart form fields, optional raw MIME&lt;/td&gt;
&lt;td&gt;Yes (basic)&lt;/td&gt;
&lt;td&gt;None built in, secure the URL yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmark Inbound&lt;/td&gt;
&lt;td&gt;One clean JSON object&lt;/td&gt;
&lt;td&gt;Yes, the most complete&lt;/td&gt;
&lt;td&gt;Basic auth / restrict by IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  AWS SES: the raw pipe
&lt;/h2&gt;

&lt;p&gt;SES receiving is rule-based: mail for your domain matches a receipt rule, and the rule delivers the raw message to S3, invokes a Lambda, or publishes to SNS. What lands is the complete, unparsed MIME document. SES attaches spam and virus verdicts plus SPF/DKIM results, which is genuinely useful, but the message itself is yours to decode: multipart trees, base64 bodies, encoded headers, attachments.&lt;/p&gt;

&lt;p&gt;Choose SES when you are already deep in AWS, want the lowest per-message cost at volume, and are comfortable owning a parsing layer (in practice: a Lambda wrapping a MIME library, plus the maintenance that follows). Note that inbound is supported in fewer regions than outbound, so check yours first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mailgun Routes: pattern matching plus parsing
&lt;/h2&gt;

&lt;p&gt;Mailgun's inbound story is routes: expressions matched against the recipient (&lt;code&gt;match_recipient&lt;/code&gt;, catch-all patterns), with actions like forwarding to a URL. The POST your server gets is already parsed: subject, sender, &lt;code&gt;body-plain&lt;/code&gt;, &lt;code&gt;body-html&lt;/code&gt;, and the handy &lt;code&gt;stripped-text&lt;/code&gt; (the message minus quoted replies and signatures). Each POST carries an HMAC signature you verify with your signing key.&lt;/p&gt;

&lt;p&gt;Choose Mailgun when you want flexible recipient routing (many addresses, one domain, different endpoints) and are fine with form-encoded payloads. The parsing is solid; the data is still email-shaped, not application-shaped.&lt;/p&gt;

&lt;h2&gt;
  
  
  SendGrid Inbound Parse: the veteran
&lt;/h2&gt;

&lt;p&gt;Point MX records at &lt;code&gt;mx.sendgrid.net&lt;/code&gt;, register a hostname and URL, and SendGrid POSTs each message as a multipart form: headers, text, HTML, attachments, plus SPF results and a spam score. A checkbox switches to raw MIME delivery if you would rather parse yourself.&lt;/p&gt;

&lt;p&gt;The sharp edge: inbound parse POSTs are not signed. The standard mitigations are an unguessable URL, basic auth embedded in it, or an allowlist, all of which are on you. It works, it has worked for a decade, but the DX shows its age.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postmark Inbound: the best mail-pipe DX
&lt;/h2&gt;

&lt;p&gt;Postmark gives you an inbound address (and optionally your own domain) and delivers each message to your webhook as a single JSON document: sender, recipients, subject, &lt;code&gt;TextBody&lt;/code&gt;, &lt;code&gt;HtmlBody&lt;/code&gt;, headers, and attachments as base64. Of the four, it is the one where you can go from zero to a working handler in an afternoon without reading a MIME RFC.&lt;/p&gt;

&lt;p&gt;Choose Postmark when developer experience matters more than squeezing cost at volume and you want JSON rather than form fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step none of them do
&lt;/h2&gt;

&lt;p&gt;All four stop at the same line: they hand you the email. If your application needs &lt;em&gt;the data inside it&lt;/em&gt;, the vendor invoice total, the listing price, the deal in a newsletter, you are writing the extraction layer yourself: regex against text bodies, DOM scraping against HTML, and a quiet breakage every time a sender redesigns their template. That layer is where inbound email projects actually rot.&lt;/p&gt;

&lt;p&gt;Full disclosure: that layer is the product I build. &lt;a href="https://inbin.dev" rel="noopener noreferrer"&gt;Inbin&lt;/a&gt; sits where your webhook handler would: you declare a schema (the fields you want), point any of your email at an inbox address, and receive typed JSON with a guard that drops any value the extractor cannot find literally in the source email. If you already run SES or Mailgun happily and your parsing is stable, you do not need it. If you are about to write the third regex for the same newsletter, you might.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already on AWS, high volume, own the parsing:&lt;/strong&gt; SES.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many addresses routed to different endpoints:&lt;/strong&gt; Mailgun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already a SendGrid customer for outbound:&lt;/strong&gt; Inbound Parse is fine; secure the URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastest path to a clean JSON webhook:&lt;/strong&gt; Postmark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the data, not the email:&lt;/strong&gt; an extraction layer, ours or your own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mechanics above reflect each provider's documented behavior at the time of writing; pricing changes often enough that I deliberately left it out. Corrections welcome.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>aws</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
