<?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: Lalit Kumar Jangid</title>
    <description>The latest articles on DEV Community by Lalit Kumar Jangid (@lalitkumarjangid).</description>
    <link>https://dev.to/lalitkumarjangid</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%2F3422354%2F2cb52798-7247-4769-a31e-1513864ac683.jpg</url>
      <title>DEV Community: Lalit Kumar Jangid</title>
      <link>https://dev.to/lalitkumarjangid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lalitkumarjangid"/>
    <language>en</language>
    <item>
      <title>Your email API returned 200. Here's why the email still never arrived</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Thu, 05 Nov 2026 16:42:35 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/your-email-api-returned-200-heres-why-the-email-still-never-arrived-3kc9</link>
      <guid>https://dev.to/lalitkumarjangid/your-email-api-returned-200-heres-why-the-email-still-never-arrived-3kc9</guid>
      <description>&lt;p&gt;A user signs up. Your code calls the email API. You get &lt;code&gt;200 OK&lt;/code&gt; and an id back. The user says the verification email never came.&lt;/p&gt;

&lt;p&gt;You check the logs: success. You check your code: it's fine. You send yourself a test: it arrives. Your user still has nothing.&lt;/p&gt;

&lt;p&gt;This is one of the most common email bugs, and it hides in the gap between three different things that all get called "sent":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Accepted.&lt;/strong&gt; The email API took your request. That's what the &lt;code&gt;200&lt;/code&gt; means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivered.&lt;/strong&gt; The recipient's mail server accepted the message. Your provider learns this later, asynchronously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inboxed.&lt;/strong&gt; The message landed in the inbox, not spam, and not a tab nobody opens. &lt;strong&gt;No sender can see this.&lt;/strong&gt; Not you, not your provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;code&gt;200&lt;/code&gt; tells you about step 1 only. This guide walks the path from there to the inbox, in the order I'd debug it, with the commands to run at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find out what actually happened to that email
&lt;/h2&gt;

&lt;p&gt;Before touching DNS or rewriting copy, get the facts on that one message. Every serious email API records what happens after it accepts a request, and reading that is step one.&lt;/p&gt;

&lt;p&gt;With Cresca's API (the one I'm building, so it's the one I'll use for examples), the email object carries a &lt;code&gt;last_event&lt;/code&gt; field:&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;-s&lt;/span&gt; https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c &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;$CRESCA_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'{to, subject, last_event, created_at}'&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;"to"&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;"ana@example.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;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verify your 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;"last_event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bounced"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-04T09:14:03.120Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each status points to a different problem, so this is where the debugging forks:&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;What it means&lt;/th&gt;
&lt;th&gt;Where to look next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sending&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Still in the pipeline. Usually seconds.&lt;/td&gt;
&lt;td&gt;Wait. If it's stuck for minutes, it's a provider issue.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Handed to the mail infrastructure; no answer from the recipient's server yet.&lt;/td&gt;
&lt;td&gt;Wait a few minutes, then step 4 (greylisting, delays).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The recipient's &lt;strong&gt;server&lt;/strong&gt; accepted it.&lt;/td&gt;
&lt;td&gt;It's in their account somewhere: spam folder, a tab or a filter. Steps 2–3 and 6.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivery_delayed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The receiving server said "try later" (full mailbox, rate limiting, greylisting).&lt;/td&gt;
&lt;td&gt;Step 4: read the deferral reason.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bounced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The receiving server rejected it.&lt;/td&gt;
&lt;td&gt;Step 4: the bounce code tells you exactly why.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;complained&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The recipient marked it as spam.&lt;/td&gt;
&lt;td&gt;Step 6: why your email looked unwanted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;suppressed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Never sent, because the address is on the suppression list (it bounced or complained before).&lt;/td&gt;
&lt;td&gt;Step 5.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Couldn't be sent at all.&lt;/td&gt;
&lt;td&gt;The dashboard shows the reason. Often an unverified domain or sender.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these surprise people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;delivered&lt;/code&gt; is not "in the inbox".&lt;/strong&gt; It means the receiving server said &lt;code&gt;250 OK&lt;/code&gt;. Gmail accepts a lot of mail and then files it under Spam, and it's still &lt;code&gt;delivered&lt;/code&gt;. If your status says delivered and the user can't find it, skip to steps 2, 3 and 6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;suppressed&lt;/code&gt; emails never left.&lt;/strong&gt; If an address hard-bounced last month, or the person reported a previous email as spam, a good provider stops sending to it. That protects your reputation, but it means your &lt;code&gt;200&lt;/code&gt; was followed by nothing at all. That's step 5.&lt;/p&gt;

&lt;p&gt;If you poll instead of using webhooks, wait a few seconds after sending and check again. A tiny helper is enough:&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="c1"&gt;// wait-for-status.js (Node 18+)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.cresca.xyz/api/v1&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;FINAL&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bounced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;complained&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppressed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&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;waitForStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;everyMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/emails/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;CRESCA_API_KEY&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Status check failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;last&lt;/span&gt; &lt;span class="o"&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;last_event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FINAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;everyMs&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="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// still queued/sent/delayed: worth a look&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it in a smoke test after every deploy that touches email: send a verification email to a test inbox and fail the pipeline if the status isn't &lt;code&gt;delivered&lt;/code&gt; within two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Check that your domain is actually authenticated
&lt;/h2&gt;

&lt;p&gt;If emails are bouncing with authentication errors, or landing in spam even for you, the cause is usually DNS. Three records decide whether a mailbox provider believes the email really came from you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; lists the servers allowed to send for a domain. It's checked against the &lt;strong&gt;envelope sender&lt;/strong&gt; (the Return-Path / MAIL FROM), not the From address your user sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; signs each message with a key whose public half you publish in DNS. The signature carries a domain (&lt;code&gt;d=&lt;/code&gt;), which is the domain vouching for the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; ties them to the From address. It passes if SPF &lt;em&gt;or&lt;/em&gt; DKIM passes &lt;strong&gt;and is aligned&lt;/strong&gt;, meaning the domain they authenticated matches the domain in your From header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last word, aligned, is where most setups quietly fail. You can have a passing SPF check and a passing DKIM signature, and still fail DMARC, because both authenticated your provider's domain instead of yours.&lt;/p&gt;

&lt;p&gt;Check all three with &lt;code&gt;dig&lt;/code&gt;. Replace &lt;code&gt;acme.com&lt;/code&gt; with your domain, and &lt;code&gt;mail.acme.com&lt;/code&gt; with whatever subdomain you send from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DMARC: must exist on the From domain (or its parent)&lt;/span&gt;
dig +short TXT _dmarc.acme.com
&lt;span class="c"&gt;# "v=DMARC1; p=none; rua=mailto:dmarc@acme.com"&lt;/span&gt;

&lt;span class="c"&gt;# SPF: on the domain used as the envelope sender (often a MAIL FROM subdomain)&lt;/span&gt;
dig +short TXT bounce.mail.acme.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf
&lt;span class="c"&gt;# "v=spf1 include:amazonses.com ~all"&lt;/span&gt;

&lt;span class="c"&gt;# DKIM: the selector comes from your provider; it's in the DNS records they gave you&lt;/span&gt;
dig +short CNAME abc123._domainkey.mail.acme.com
dig +short TXT  abc123._domainkey.mail.acme.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mistakes that come up most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Two SPF records on one name.&lt;/strong&gt; SPF allows exactly one &lt;code&gt;v=spf1&lt;/code&gt; record per name. Two of them is a &lt;code&gt;permerror&lt;/code&gt;, which counts as a fail. Merge them into one record with multiple &lt;code&gt;include:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too many DNS lookups in SPF.&lt;/strong&gt; SPF stops at 10 lookups. Every &lt;code&gt;include:&lt;/code&gt; can pull in more. Once you've added a CRM, a helpdesk and two email tools, you can be over the limit without knowing. Remove services you no longer send from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No DMARC record at all.&lt;/strong&gt; Without one, Gmail and Yahoo treat bulk mail from you as unauthenticated (more on that in step 3). Start with &lt;code&gt;p=none&lt;/code&gt; and a &lt;code&gt;rua=&lt;/code&gt; address so you get reports, then tighten later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM published on the wrong name.&lt;/strong&gt; If the provider asked for &lt;code&gt;abc123._domainkey.mail.acme.com&lt;/code&gt; and your DNS host appended your domain again, you've created &lt;code&gt;abc123._domainkey.mail.acme.com.acme.com&lt;/code&gt;. Run the &lt;code&gt;dig&lt;/code&gt; and check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From address on a domain you never set up.&lt;/strong&gt; Your app sends as &lt;code&gt;noreply@acme.io&lt;/code&gt; while you authenticated &lt;code&gt;acme.com&lt;/code&gt;. Different domain, nothing aligns.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Read the verdict the receiver actually gave
&lt;/h3&gt;

&lt;p&gt;DNS tells you what you published. The receiving server tells you what it concluded. In Gmail, open the email, click &lt;strong&gt;⋮ → Show original&lt;/strong&gt;, and you'll see a summary at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPF:   PASS with IP 54.240.x.x
DKIM:  'PASS' with domain mail.acme.com
DMARC: 'PASS'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're scripting it, the same verdict is in the &lt;code&gt;Authentication-Results&lt;/code&gt; header of the raw message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication-Results: mx.google.com;
       dkim=pass header.i=@mail.acme.com header.s=abc123;
       spf=pass smtp.mailfrom=bounce.mail.acme.com;
       dmarc=pass (p=NONE) header.from=mail.acme.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you want to see is &lt;code&gt;dmarc=pass&lt;/code&gt;, and a DKIM &lt;code&gt;header.i&lt;/code&gt; or SPF &lt;code&gt;smtp.mailfrom&lt;/code&gt; whose domain matches &lt;code&gt;header.from&lt;/code&gt;. If DMARC fails while SPF and DKIM both pass, it's alignment, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Know the rules Gmail, Yahoo and Outlook now enforce
&lt;/h2&gt;

&lt;p&gt;Since February 2024, Gmail and Yahoo have enforced sender requirements, and Microsoft added similar rules for Outlook.com consumer mailboxes in 2025. The short version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For everyone sending to these mailboxes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authenticate with SPF or DKIM (do both).&lt;/li&gt;
&lt;li&gt;Have valid forward and reverse DNS on the sending IPs. Your provider handles this if you use one.&lt;/li&gt;
&lt;li&gt;Send over TLS.&lt;/li&gt;
&lt;li&gt;Keep the spam complaint rate low. Google's line is below 0.3%, and it recommends staying under 0.1%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For bulk senders&lt;/strong&gt; (Google's threshold is around 5,000 messages a day to Gmail addresses):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPF &lt;strong&gt;and&lt;/strong&gt; DKIM &lt;strong&gt;and&lt;/strong&gt; a DMARC record (at least &lt;code&gt;p=none&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;DMARC alignment: the From domain must match the SPF or DKIM domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click unsubscribe&lt;/strong&gt; on marketing and subscription email, using the &lt;code&gt;List-Unsubscribe&lt;/code&gt; and &lt;code&gt;List-Unsubscribe-Post&lt;/code&gt; headers (RFC 8058), and honoring unsubscribes within two days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those unsubscribe headers look like this on a marketing email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List-Unsubscribe: &amp;lt;https://acme.com/unsubscribe?u=8f3c...&amp;gt;, &amp;lt;mailto:unsubscribe@acme.com?subject=unsubscribe&amp;gt;
List-Unsubscribe-Post: List-Unsubscribe=One-Click
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transactional email (receipts, password resets, verification codes) doesn't need an unsubscribe link, and you shouldn't add one. But it's checked against the same authentication rules, and it shares your domain's reputation, which is why the next point matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send app email and marketing from different subdomains.&lt;/strong&gt; If &lt;code&gt;acme.com&lt;/code&gt; sends a newsletter that gets complaints, you don't want password resets from the same domain paying for it. A common split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mail.acme.com     → transactional: resets, receipts, verification
news.acme.com     → marketing: newsletters, announcements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mailbox providers track reputation per domain, so this keeps one stream's bad week from hurting the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Read the bounce or deferral message
&lt;/h2&gt;

&lt;p&gt;If the status is &lt;code&gt;bounced&lt;/code&gt; or &lt;code&gt;delivery_delayed&lt;/code&gt;, the receiving server told your provider why, as an SMTP reply. Look at the message in your provider's dashboard or event log. The codes are more useful than they look.&lt;/p&gt;

&lt;p&gt;The first digit tells you the category:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;5xx&lt;/code&gt;: permanent.&lt;/strong&gt; Don't retry. Your provider should suppress the address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;4xx&lt;/code&gt;: temporary.&lt;/strong&gt; The provider retries on its own; you'll see &lt;code&gt;delivery_delayed&lt;/code&gt; meanwhile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real detail is in the enhanced status code (&lt;code&gt;5.1.1&lt;/code&gt;, &lt;code&gt;5.7.26&lt;/code&gt; and so on) and the text after it. The ones that cover most cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reply&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;550 5.1.1 The email account that you tried to reach does not exist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Typo or deleted address.&lt;/td&gt;
&lt;td&gt;Nothing to fix on your side. Validate addresses at signup (a confirm-your-email step catches typos like &lt;code&gt;gmial.com&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;552 5.2.2 Mailbox full&lt;/code&gt; (often sent as a &lt;code&gt;4xx&lt;/code&gt; first)&lt;/td&gt;
&lt;td&gt;The recipient's quota is full.&lt;/td&gt;
&lt;td&gt;Nothing. It'll retry, then bounce.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;550 5.7.26 ... unauthenticated&lt;/code&gt; (Gmail)&lt;/td&gt;
&lt;td&gt;Failed DMARC, or no SPF/DKIM at all.&lt;/td&gt;
&lt;td&gt;Step 2. This is an alignment or DNS problem.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;421 4.7.28 ... unusual rate&lt;/code&gt; (Gmail)&lt;/td&gt;
&lt;td&gt;You're sending faster than your reputation supports.&lt;/td&gt;
&lt;td&gt;Slow down: warm up a new domain gradually.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;554 5.7.1 ... blocked&lt;/code&gt; / mentions a blocklist&lt;/td&gt;
&lt;td&gt;Your IP or domain is on a blocklist.&lt;/td&gt;
&lt;td&gt;Check which list the message names; with a shared provider, contact them.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 5: Check whether the address is suppressed
&lt;/h2&gt;

&lt;p&gt;If an address bounced hard once, sending to it again is a signal to mailbox providers that you don't maintain your list. So providers keep a &lt;strong&gt;suppression list&lt;/strong&gt; and skip those addresses on later sends. Many also suppress anyone who marked a previous email as spam.&lt;/p&gt;

&lt;p&gt;In practice this produces the most confusing bug report of all: &lt;em&gt;"I signed up again with the same address and still got nothing."&lt;/em&gt; The first signup attempt had a typo, or their mailbox was full for long enough to hard-bounce, and the address has been suppressed ever since.&lt;/p&gt;

&lt;p&gt;What to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look up the address in your provider's suppression list. In Cresca, the email's status itself says &lt;code&gt;suppressed&lt;/code&gt;, and the delivery logs show which recipient was skipped.&lt;/li&gt;
&lt;li&gt;If the original reason is gone (the person fixed their mailbox, or the bounce was a misconfiguration on your side), get the address removed from the list and resend. Some providers let you do that yourself; others, Cresca included, have support remove bounce and complaint suppressions after checking, so one mistaken unsuppress can't hurt your domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't build a "retry everything" job that ignores suppression.&lt;/strong&gt; It's the fastest way to turn a few bad addresses into a reputation problem for every email you send.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule worth copying: &lt;strong&gt;a marketing unsubscribe must never block transactional email.&lt;/strong&gt; Someone who unsubscribed from your newsletter still needs their password reset. Hard bounces and spam complaints are different: those addresses shouldn't get anything from you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: If it's delivered but nobody sees it, look at the email itself
&lt;/h2&gt;

&lt;p&gt;If authentication passes and the status is &lt;code&gt;delivered&lt;/code&gt;, the message reached the account. When it lands in spam or a tab instead of the inbox, the content and the sending pattern are what's left. For transactional email, the usual suspects are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No plain-text part.&lt;/strong&gt; Send &lt;code&gt;multipart/alternative&lt;/code&gt; with both HTML and text. Most email APIs generate the text version from your HTML if you don't provide one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A From address nobody recognises.&lt;/strong&gt; &lt;code&gt;noreply@acme-transactional-svc.io&lt;/code&gt; reads like phishing. Use your product's name and domain: &lt;code&gt;Acme &amp;lt;hello@mail.acme.com&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link mismatch.&lt;/strong&gt; The visible link text says &lt;code&gt;acme.com&lt;/code&gt; but the URL goes through a shortener or a tracking domain. Link to your own domain, and if you use click tracking, use a tracking subdomain you own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing inside transactional.&lt;/strong&gt; A receipt with three promotional banners gets treated like a promotion. Keep transactional email about the transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A brand-new domain sending a lot, fast.&lt;/strong&gt; New domains have no reputation. Start with your own team and real users, and ramp up over a couple of weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist I run, in order
&lt;/h2&gt;

&lt;p&gt;When a user says "I never got the email":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look up the status&lt;/strong&gt; for that email id: queued, sent, delivered, bounced, suppressed or failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If bounced or delayed:&lt;/strong&gt; read the SMTP reply and use the table in step 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If suppressed:&lt;/strong&gt; check why, and only get it removed if the reason is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If delivered:&lt;/strong&gt; ask them to check spam and other tabs, and look at your own copy's &lt;strong&gt;Show original&lt;/strong&gt; for SPF/DKIM/DMARC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check DNS&lt;/strong&gt; with &lt;code&gt;dig&lt;/code&gt;: one SPF record, DKIM on the right name, a DMARC record, and alignment with your From domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the bulk-sender rules&lt;/strong&gt; if you send marketing: one-click unsubscribe headers and a low complaint rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate streams&lt;/strong&gt;: app email and marketing on different subdomains.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Almost every "it said sent but never arrived" bug sits somewhere in that list. The &lt;code&gt;200&lt;/code&gt; was true. It just wasn't the whole story.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;Cresca&lt;/a&gt;, an email platform with a transactional email API and AI-designed marketing campaigns in one account. Every API email has a status you can read through the API or in the dashboard timeline, and the &lt;a href="https://www.cresca.xyz/docs/email-lifecycle" rel="noopener noreferrer"&gt;email lifecycle docs&lt;/a&gt; list every status used above. What's the strangest delivery bug you've chased? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>email</category>
    </item>
    <item>
      <title>Your AI agent will send that email twice. Here's how to stop it</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Wed, 04 Nov 2026 16:57:17 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/your-ai-agent-will-send-that-email-twice-heres-how-to-stop-it-4ljl</link>
      <guid>https://dev.to/lalitkumarjangid/your-ai-agent-will-send-that-email-twice-heres-how-to-stop-it-4ljl</guid>
      <description>&lt;p&gt;&lt;em&gt;Updated November 5: added Rule 6 (your own send record) and a dedicated branch for the reused-key 409, after a sharp comment from a reader about the 24-hour window. Thank you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Give an AI agent a &lt;code&gt;send_email&lt;/code&gt; tool and, sooner or later, a customer gets the same receipt twice.&lt;/p&gt;

&lt;p&gt;Nothing is broken when it happens. Every piece works the way it was built to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The HTTP call to your email API times out after 10 seconds. The email was actually accepted at second 9. Your tool returns an error, so the agent tries again.&lt;/li&gt;
&lt;li&gt;The model sees a tool error and re-plans. Its second attempt rewrites the subject line "to be safe" and calls the tool again.&lt;/li&gt;
&lt;li&gt;Your agent framework retries failed tool calls automatically. So does your HTTP client. So does the queue worker that runs the agent. Three layers of retries, none of them aware of the others.&lt;/li&gt;
&lt;li&gt;A long-running agent loses its context, restarts the task from its notes, and finds "send confirmation to Ana" still on the to-do list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a reasonable design choice on its own. Together they turn "exactly once" into "at least once", and with email, "at least once" means your customer sees it twice. Unlike a duplicated database row, you can't delete a sent email.&lt;/p&gt;

&lt;p&gt;This post is the pattern I use to make email sends safe for agents: &lt;strong&gt;idempotency keys derived from the event, a strict retry policy, narrow tools, and an undo window for anything that isn't transactional.&lt;/strong&gt; The code uses the Cresca email API (I'm building it, so that's the one I know best), but every idea works with any email API that accepts an &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just hash the arguments" doesn't work
&lt;/h2&gt;

&lt;p&gt;The first fix everyone reaches for is to hash the tool's arguments and use that as the idempotency key. Same arguments, same hash, so the API ignores the duplicate. Clean.&lt;/p&gt;

&lt;p&gt;It breaks the moment the agent is involved:&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="c1"&gt;// Attempt 1, written by the model&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempt1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ana@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your order #1042 is confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Hi Ana, ...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Attempt 2, after a timeout. The model "improved" it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempt2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ana@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order #1042 confirmed!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Hello Ana, ...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different arguments, different hash, two emails. A language model almost never produces byte-identical output twice, and when it re-plans after an error it is actively trying to do something different.&lt;/p&gt;

&lt;p&gt;Hashing also fails in the other direction. If Ana places two orders that produce the same text, the hash matches and the second confirmation is silently dropped.&lt;/p&gt;

&lt;p&gt;The key has to identify &lt;strong&gt;the thing that happened in the world&lt;/strong&gt;, not the request that describes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: derive the key from the event
&lt;/h2&gt;

&lt;p&gt;An idempotency key answers one question: &lt;em&gt;which real-world event is this email about?&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order-confirmation/1042
password-reset/7f3c9a      ← the id of this reset request, not of the user
invoice-paid/inv_2026_0042
weekly-digest/user_881/2026-W45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is &lt;code&gt;&amp;lt;event&amp;gt;/&amp;lt;id&amp;gt;&lt;/code&gt;. Three properties matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stable across retries.&lt;/strong&gt; Every attempt to send the confirmation for order 1042 produces the same key, whoever makes the attempt: your code, the framework or the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New when the event is new.&lt;/strong&gt; If Ana asks for a password reset twice, those are two reset requests with two ids, and she should get two emails. Key on the request id, not on her user id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never time-based or random.&lt;/strong&gt; &lt;code&gt;Date.now()&lt;/code&gt; or a fresh UUID per attempt defeats the whole point, because each retry gets a new key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With Cresca, a request with a key the API has already seen (with the same body, within 24 hours) returns the original response instead of sending again, and adds the header &lt;code&gt;idempotent-replayed: true&lt;/code&gt;. Two other cases are worth knowing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same key, same body, within 24 hours&lt;/td&gt;
&lt;td&gt;The first response again, with &lt;code&gt;idempotent-replayed: true&lt;/code&gt;. Nothing is sent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same key, &lt;strong&gt;different&lt;/strong&gt; body&lt;/td&gt;
&lt;td&gt;&lt;code&gt;409 invalid_idempotent_request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same key while the first request is still being processed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;409 concurrent_idempotent_requests&lt;/code&gt;. Retry shortly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More than 24 hours later&lt;/td&gt;
&lt;td&gt;The key has expired, and the request is treated as new.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That second row is exactly the "model rewrote the subject" case. You get a loud 409 instead of a quiet duplicate, which is what you want, as long as your code reads that 409 as a decision rather than an error (Rule 3 shows how).&lt;/p&gt;

&lt;p&gt;And look at the last row. The API remembers a key for 24 hours. That makes the key a &lt;strong&gt;cache, not a record&lt;/strong&gt;: it stops retries that happen within a day, but an agent that picks a task back up tomorrow gets a fresh send. Rule 6 covers the part that has to live on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: don't let the model write transactional email
&lt;/h2&gt;

&lt;p&gt;That 409 is useful, but you should rarely see it, because the model shouldn't be composing the receipt in the first place.&lt;/p&gt;

&lt;p&gt;For transactional email (receipts, resets, verification codes, alerts), give the agent a tool that takes &lt;strong&gt;ids, not content&lt;/strong&gt;:&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="c1"&gt;// tools.js. The shape the model sees. (Field names differ slightly between&lt;/span&gt;
&lt;span class="c1"&gt;// SDKs: some call the schema `parameters`, others `input_schema`.)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&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="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;send_order_confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Send the order confirmation email for one order. Safe to call more than once: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the same order never gets two confirmations. Use only after the order is paid.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;schema&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;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;order_id&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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The order id, e.g. 1042&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="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your code looks up the order, builds the email from a template and owns the key. The model decides &lt;em&gt;whether&lt;/em&gt; and &lt;em&gt;when&lt;/em&gt; to send; it never decides &lt;em&gt;what&lt;/em&gt; the email says or how it's identified.&lt;/p&gt;

&lt;p&gt;This does three things at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency stops depending on the model.&lt;/strong&gt; The key comes from the order id, which your code controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucinated content is impossible.&lt;/strong&gt; The model can't invent a discount, a wrong total or a link to the wrong account, because it never writes the body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool description can promise safety.&lt;/strong&gt; "Safe to call more than once" is now true, and models do read tool descriptions. It reduces the nervous re-calling you see when a tool has failed before.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule 3: retry the right failures, and only those
&lt;/h2&gt;

&lt;p&gt;Here's the send function behind that tool. It's plain Node 18+ with &lt;code&gt;fetch&lt;/code&gt;, no SDK:&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="c1"&gt;// send-email.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.cresca.xyz/api/v1&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;KEY&lt;/span&gt; &lt;span class="o"&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;CRESCA_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// a "sending access" key: it can send, nothing else&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&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;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idempotencyKey is required for every send&lt;/span&gt;&lt;span class="dl"&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;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/emails`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;KEY&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Network error or timeout: the email may or may not have been accepted.&lt;/span&gt;
      &lt;span class="c1"&gt;// Retrying with the SAME key is safe, so this is exactly when to retry.&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;body&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replayed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idempotent-replayed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;

    &lt;span class="c1"&gt;// In flight elsewhere (another retry layer got there first): wait and ask again.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;concurrent_idempotent_requests&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Same key, different body: an email for this event already went out (the&lt;/span&gt;
    &lt;span class="c1"&gt;// model probably rewrote the content). That's a decision, not an error:&lt;/span&gt;
    &lt;span class="c1"&gt;// report it as already sent so the agent stops instead of re-planning.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_idempotent_request&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replayed&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;conflict&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;span class="c1"&gt;// Rate limited: the server tells us how long to wait.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limit_exceeded&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry-after&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Server-side trouble: back off and retry.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Everything else is a bug in the request or a used-up monthly quota.&lt;/span&gt;
    &lt;span class="c1"&gt;// Retrying can't fix either. Fail loudly.&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email not sent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email not sent after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; attempts`&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;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;250&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;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 1s, 2s, 4s, 8s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the tool handler that ties it to the order:&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="c1"&gt;// handlers.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&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;./send-email.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getOrder&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;./orders.js&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;send_order_confirmation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;order_id&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;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order_id&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not paid yet`&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;orders@mail.acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order-confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// a published template, by alias&lt;/span&gt;
        &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ORDER_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;TOTAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalFormatted&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&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;category&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_confirmation&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="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`order-confirmation/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="c1"&gt;// Tell the model plainly what happened, including "you already did this".&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conflict&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ok&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;done&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;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`A confirmation for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was already sent. Nothing new was sent. This task is complete; do not retry.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replayed&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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;done&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;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Confirmation for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was already sent earlier (id &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). Nothing new was sent.`&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;ok&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;done&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;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Confirmation for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; sent (id &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last &lt;code&gt;return&lt;/code&gt; matters more than it looks. When the agent calls the tool a second time, it gets a clear answer, "already sent earlier", instead of a generic success it might second-guess. A clear answer like that is what stops the retry loop at the model level: there's nothing left to fix.&lt;/p&gt;

&lt;p&gt;Four details in the retry policy are deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network errors and timeouts are retried.&lt;/strong&gt; They're the dangerous case: the email might have gone out. The idempotency key is what makes retrying them safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retry-after&lt;/code&gt; is respected on 429.&lt;/strong&gt; Retrying a rate limit immediately just burns your next request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reused-key 409 is a decision, not an error.&lt;/strong&gt; Most agent frameworks and HTTP clients sort 4xx responses into "retryable" or "not retryable" and nothing else. If &lt;code&gt;invalid_idempotent_request&lt;/code&gt; reaches the model as a tool error, the model re-plans and you're back to the rewritten subject line, arriving by a different route. So it gets its own branch that ends the task with "already sent".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other 4xx responses are never retried.&lt;/strong&gt; A validation error or an exhausted quota will fail the same way every time, and a retry loop here is how you end up with a stuck agent and a page of logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule 4: give non-transactional email an undo window
&lt;/h2&gt;

&lt;p&gt;Receipts are easy because the content is fixed. The hard case is the email the agent &lt;em&gt;does&lt;/em&gt; compose: "draft a follow-up to everyone who attended Tuesday's webinar and send it."&lt;/p&gt;

&lt;p&gt;You can't key that on an order id, and you probably don't want a model hitting "send" on a message to 300 people with no human in the loop. The pattern I like is to &lt;strong&gt;schedule instead of sending&lt;/strong&gt;, then let a person cancel:&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="c1"&gt;// The agent's "send" tool for composed email schedules 15 minutes ahead.&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;send_followup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;campaign_id&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;sendAt&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scheduled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sendAt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`followup/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;campaign_id&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;to&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;await&lt;/span&gt; &lt;span class="nf"&gt;notifyHuman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`The agent scheduled "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sendAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="s2"&gt;`Cancel within 15 minutes: POST /emails/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cancel`&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Scheduled for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sendAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. A human can cancel it before then.`&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;Cresca accepts &lt;code&gt;scheduled_at&lt;/code&gt; up to 30 days ahead, and &lt;code&gt;POST /emails/:id/cancel&lt;/code&gt; works while an email is still scheduled or queued; once sending has started it returns &lt;code&gt;422&lt;/code&gt;. A canceled email doesn't count toward your monthly allowance, so a canceled mistake costs nothing.&lt;/p&gt;

&lt;p&gt;Fifteen minutes is arbitrary. Pick a window that matches how fast someone on your team actually looks at notifications. The point is that the agent's action becomes reversible for a while, which is the property email normally lacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: give the agent a key that can only send
&lt;/h2&gt;

&lt;p&gt;The agent runs with whatever credentials you give its tools. For email, that should be a key that can send and nothing else.&lt;/p&gt;

&lt;p&gt;Cresca's &lt;strong&gt;sending access&lt;/strong&gt; keys can call &lt;code&gt;POST /emails&lt;/code&gt; and &lt;code&gt;POST /emails/batch&lt;/code&gt;, but can't read your email logs or change templates. Your undo-window tool needs a full-access key to cancel, so keep that on the human side, in the notification handler, not in the agent's tools.&lt;/p&gt;

&lt;p&gt;The general rule: &lt;strong&gt;the agent's credentials should match the agent's job, not your admin's.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 6: keep your own record of what was sent
&lt;/h2&gt;

&lt;p&gt;The idempotency key protects you for 24 hours. Agents don't always finish within 24 hours. A long-running agent that loses its context and rebuilds its to-do list from notes the next day will find "send confirmation to Ana" still open, and by then the key has expired. Every piece behaves correctly and Ana still gets two receipts.&lt;/p&gt;

&lt;p&gt;The fix is a record on your side that outlives the provider's window. Write it in the &lt;strong&gt;same database transaction&lt;/strong&gt; as the state change that triggers the email, so the two can never disagree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;email_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_key&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- e.g. 'order-confirmation/1042'&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- pending | sent&lt;/span&gt;
  &lt;span class="n"&gt;email_id&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;sent_at&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When the order is marked paid, record that its confirmation is owed, in the&lt;/span&gt;
&lt;span class="c1"&gt;// same transaction. If the transaction rolls back, no email is owed.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE orders SET status = 'paid' WHERE id = $1&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="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO email_outbox (event_key) VALUES ($1) ON CONFLICT DO NOTHING&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="s2"&gt;`order-confirmation/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// The tool (or a worker) checks the record before it ever calls the email API.&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;send_order_confirmation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;order_id&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`order-confirmation/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order_id&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;oneOrNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT status, email_id FROM email_outbox WHERE event_key = $1&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="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No confirmation is owed for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order_id&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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;done&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;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Already sent (id &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). This task is complete; do not retry.`&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE email_outbox SET status = 'sent', email_id = $2, sent_at = now() WHERE event_key = $1&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="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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;done&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;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Confirmation for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; sent.`&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;The two layers cover different gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your outbox row&lt;/strong&gt; answers "was this ever sent?", for as long as you keep it. A restart a week later reads &lt;code&gt;sent&lt;/code&gt; and stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The idempotency key&lt;/strong&gt; covers the seconds between calling the API and writing &lt;code&gt;sent&lt;/code&gt;: if the process crashes after the email goes out but before the update, the retry reuses the same key and the API returns the original result instead of sending again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the outbox pattern payment systems use for the same reason: the provider's deduplication window is a convenience, and your own ledger is the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same pattern in Python
&lt;/h2&gt;

&lt;p&gt;If your agent runs on a Python stack, here's the core of &lt;code&gt;send_email&lt;/code&gt; with &lt;code&gt;httpx&lt;/code&gt;:&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;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.cresca.xyz/api/v1&lt;/span&gt;&lt;span class="sh"&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;Authorization&lt;/span&gt;&lt;span class="sh"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CRESCA_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/emails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransportError&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;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replayed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotent-replayed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concurrent_idempotent_requests&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_idempotent_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&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="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;replayed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# already sent: stop, don't re-plan
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate_limit_exceeded&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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;Email not sent: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &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="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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;Email not sent after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to test that it actually works
&lt;/h2&gt;

&lt;p&gt;Don't trust the pattern until you've watched it refuse to send twice. Three tests worth automating:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The replay test.&lt;/strong&gt; Call your tool twice in a row for the same order and assert one email:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;send_order_confirmation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-1042&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;send_order_confirmation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-1042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;already sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The timeout test.&lt;/strong&gt; Point &lt;code&gt;sendEmail&lt;/code&gt; at a proxy that accepts the request, waits past your 10-second timeout, then lets it through. Your code sees a timeout and retries; you should still end up with one email and &lt;code&gt;replayed: true&lt;/code&gt; on the retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The model test.&lt;/strong&gt; Run the agent with a tool that fails the first call on purpose ("network error"), and log every tool call it makes after that. You're checking two things: that the model's retries land on the same key, and that the "already sent" note stops it from calling a third time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before you hand an agent a tool that sends email:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every send carries an idempotency key built from the &lt;strong&gt;event&lt;/strong&gt; (&lt;code&gt;&amp;lt;event&amp;gt;/&amp;lt;id&amp;gt;&lt;/code&gt;), never from time, randomness or a hash of model output.&lt;/li&gt;
&lt;li&gt;Transactional tools take &lt;strong&gt;ids&lt;/strong&gt;, and your code builds the content from templates.&lt;/li&gt;
&lt;li&gt;The retry policy retries network errors, timeouts, 429 (after &lt;code&gt;retry-after&lt;/code&gt;) and 5xx, and &lt;strong&gt;nothing else&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A replayed send &lt;strong&gt;and&lt;/strong&gt; a reused-key 409 both return a clear "already sent, task complete" result to the model, never an error.&lt;/li&gt;
&lt;li&gt;Your own outbox row records each send, written in the same transaction as the state change, so a restart days later still knows it was sent.&lt;/li&gt;
&lt;li&gt;Composed or bulk email is &lt;strong&gt;scheduled, not sent&lt;/strong&gt;, with a cancel window a human can use.&lt;/li&gt;
&lt;li&gt;The agent's key can send, and only send.&lt;/li&gt;
&lt;li&gt;You've watched the replay, timeout and model tests pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is new. Payment APIs have worked this way for years, because charging a card twice is the same kind of mistake as emailing someone twice. And that's the real argument for narrow tools: most mistakes in agent systems can be rolled back, but &lt;strong&gt;you can't delete a sent email&lt;/strong&gt;. What's new is that the thing calling your API is now a model that retries creatively. Design the tool so that creativity can't cost your customer a duplicate email.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;Cresca&lt;/a&gt;, an email platform with a transactional email API (idempotency keys, scheduling and cancel included) and AI-designed marketing campaigns in the same account. The &lt;a href="https://www.cresca.xyz/docs/idempotency" rel="noopener noreferrer"&gt;idempotency docs&lt;/a&gt; cover the exact behaviour used above. If you've hit a duplicate-send bug with an agent, I'd like to hear how it happened in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Which emails should an unsubscribe stop? A suppression rulebook for your app</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:23:53 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/which-emails-should-an-unsubscribe-stop-a-suppression-rulebook-for-your-app-10ie</link>
      <guid>https://dev.to/lalitkumarjangid/which-emails-should-an-unsubscribe-stop-a-suppression-rulebook-for-your-app-10ie</guid>
      <description>&lt;p&gt;After my &lt;a href="https://dev.to/lalitkumarjangid/transactional-email-checklist-8-things-to-get-right-before-your-app-sends-a-password-reset-2g66"&gt;transactional email checklist&lt;/a&gt;, a reader left a comment I keep thinking about. They had to decide what happens when they reply to someone's support message, and that person has switched all email off. Their answer: the reply still goes out, because the person wrote first. A bounced address still stops it.&lt;/p&gt;

&lt;p&gt;That's the right call, and it points at something most apps get wrong. They keep one "do not email" list and check it before every send. But an unsubscribe, a bounce and a spam complaint don't mean the same thing, so they shouldn't stop the same emails.&lt;/p&gt;

&lt;p&gt;Here's the rulebook I'd use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three kinds of email
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; newsletters, launches, promotions. You decide to send it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactional:&lt;/strong&gt; password resets, receipts, verification codes. The person's action triggers it, and they're waiting for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversational:&lt;/strong&gt; replies to a message the person sent you, such as a support ticket. They started the conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Five signals
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;th&gt;Marketing&lt;/th&gt;
&lt;th&gt;Transactional&lt;/th&gt;
&lt;th&gt;Reply to their message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unsubscribe&lt;/td&gt;
&lt;td&gt;"Stop sending me marketing"&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Send&lt;/td&gt;
&lt;td&gt;Send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard bounce&lt;/td&gt;
&lt;td&gt;The address doesn't exist&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeated soft bounces&lt;/td&gt;
&lt;td&gt;The address keeps failing&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spam complaint&lt;/td&gt;
&lt;td&gt;"I don't want email from you"&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual block&lt;/td&gt;
&lt;td&gt;You, or they, asked for it&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single soft bounce, such as a full mailbox, isn't on the list. Retry it. Only suppress the address when it keeps failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: an unsubscribe is about the content, not the address
&lt;/h2&gt;

&lt;p&gt;When someone clicks unsubscribe on your newsletter, they're saying "stop sending me this kind of email". They aren't saying "never let me reset my password".&lt;/p&gt;

&lt;p&gt;If an unsubscribe blocks transactional email, the person who unsubscribed last year can't log in today, and they'll think your product is broken. So check unsubscribes only before marketing sends.&lt;/p&gt;

&lt;p&gt;The rules agree. In the US, CAN-SPAM looks at an email's &lt;a href="https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business" rel="noopener noreferrer"&gt;primary purpose&lt;/a&gt;: transactional and relationship messages aren't treated like commercial ones. And Gmail and Yahoo's bulk-sender rules ask for one-click unsubscribe on marketing and subscribed email, not on a password reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: a bounce is about the address
&lt;/h2&gt;

&lt;p&gt;A hard bounce means the mailbox doesn't exist. That's true whatever you're sending, so stop everything to that address: campaigns, receipts and replies.&lt;/p&gt;

&lt;p&gt;Sending again doesn't just fail. Mailbox providers watch how often you send to addresses that don't exist, and a high bounce rate hurts the delivery of every other email you send.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: a complaint is about you
&lt;/h2&gt;

&lt;p&gt;A spam complaint is the strongest signal you can get. It doesn't mean the address is bad. It means this person doesn't want email from &lt;em&gt;you&lt;/em&gt;. Stop everything from your sender to them, including transactional email, even though that feels harsh.&lt;/p&gt;

&lt;p&gt;If you run a platform that sends for many brands, keep complaints scoped to the brand that was reported. A complaint about one sender shouldn't block a different company's password reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: check twice
&lt;/h2&gt;

&lt;p&gt;Most apps check suppression when they queue an email. But an address can bounce or complain between queueing and sending, especially for scheduled emails. Check again right before the email goes out, and drop any recipient who was suppressed in the meantime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: don't let a transactional email turn into marketing
&lt;/h2&gt;

&lt;p&gt;This one came straight from the comment. A support reply or a receipt keeps its transactional status only while it's about the thing the person asked for. Add a newsletter footer, a "you might also like" block or a promo code, and you've made it marketing. Then the unsubscribe rules apply again, and you'll want an unsubscribe link in it.&lt;/p&gt;

&lt;p&gt;Keep transactional emails boring. That's a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  In code
&lt;/h2&gt;

&lt;p&gt;The whole rulebook fits in one small function. Store each suppression with its reason, then decide per email type:&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="c1"&gt;// reason: 'unsubscribe' | 'hard_bounce' | 'soft_bounce_repeated' | 'complaint' | 'manual'&lt;/span&gt;
&lt;span class="c1"&gt;// type:   'marketing' | 'transactional' | 'reply'&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suppression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;suppression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suppression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsubscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marketing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// bounces, complaints and manual blocks stop everything&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;canSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transactional&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsubscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// true:  the password reset goes out&lt;/span&gt;
&lt;span class="nf"&gt;canSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marketing&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsubscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;     &lt;span class="c1"&gt;// false: no newsletter&lt;/span&gt;
&lt;span class="nf"&gt;canSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reply&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hard_bounce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;         &lt;span class="c1"&gt;// false: the address doesn't exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part isn't the function. It's storing the &lt;strong&gt;reason&lt;/strong&gt; with every suppression. If your table only says "suppressed: true", you can't make any of these decisions later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we do it in Cresca
&lt;/h2&gt;

&lt;p&gt;These are the rules we follow in &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;Cresca&lt;/a&gt;. Campaigns skip every suppressed address. Email sent through the transactional API ignores marketing unsubscribes, but hard bounces, repeated soft bounces (3 in 30 days), spam complaints and manual suppressions still block it. Complaints only block the account that was reported. And we check again right before each email is sent. The &lt;a href="https://www.cresca.xyz/docs/suppression" rel="noopener noreferrer"&gt;suppression docs&lt;/a&gt; have the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Thanks again to the reader who brought up support replies: that's now rule 5. What other email in your app doesn't fit neatly into "marketing" or "transactional"? I'd like to hear the edge cases.&lt;/p&gt;

</description>
      <category>email</category>
      <category>webdev</category>
      <category>api</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Transactional Email API: The Complete Cresca Guide</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:40:47 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/transactional-email-api-the-complete-cresca-guide-3n03</link>
      <guid>https://dev.to/lalitkumarjangid/transactional-email-api-the-complete-cresca-guide-3n03</guid>
      <description>&lt;p&gt;&lt;strong&gt;Cresca's transactional email API&lt;/strong&gt; is a REST API at &lt;code&gt;https://api.cresca.xyz/api/v1&lt;/code&gt; that sends one-to-one email, such as password resets, receipts and verification codes, from your own verified domain. You authenticate with an API key in an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header, POST JSON to &lt;code&gt;/emails&lt;/code&gt;, and get back an id you can use to check delivery. This guide covers every v1 endpoint, with a full worked example and code you can copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this guide&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every endpoint at a glance&lt;/li&gt;
&lt;li&gt;Authentication: API keys and the Bearer header&lt;/li&gt;
&lt;li&gt;Worked example: your first email, end to end&lt;/li&gt;
&lt;li&gt;Emails API: send, batch, list, reschedule, cancel&lt;/li&gt;
&lt;li&gt;Templates API: create, publish and send by alias&lt;/li&gt;
&lt;li&gt;Contacts API&lt;/li&gt;
&lt;li&gt;Idempotency, lifecycle, suppression, limits and errors&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All requests go to &lt;code&gt;https://api.cresca.xyz/api/v1&lt;/code&gt; over HTTPS, and requests and responses are JSON. There is no SDK package to install: any HTTP client works, so the examples below use cURL, Node.js 18+ (built-in &lt;code&gt;fetch&lt;/code&gt;) and Python (&lt;code&gt;requests&lt;/code&gt;). Field names follow the conventions of popular email APIs, so an existing integration is mostly a change of base URL and key.&lt;/p&gt;

&lt;p&gt;The API is for transactional email: messages a person expects because of something they did. Newsletters and promotions belong in Cresca campaigns. If the line is unclear, read &lt;a href="https://www.cresca.xyz/blog/transactional-vs-marketing-email" rel="noopener noreferrer"&gt;transactional vs marketing email&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every endpoint at a glance
&lt;/h2&gt;

&lt;p&gt;Paths are relative to the base URL. "Sending" means a sending-access key works; "Full" needs a full-access key.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Docs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/emails&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send one email (up to 50 recipients)&lt;/td&gt;
&lt;td&gt;Sending&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/send-email" rel="noopener noreferrer"&gt;Send email&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/emails/batch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send up to 100 emails in one request&lt;/td&gt;
&lt;td&gt;Sending&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/send-batch" rel="noopener noreferrer"&gt;Send batch&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/emails/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve one email and its &lt;code&gt;last_event&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/retrieve-email" rel="noopener noreferrer"&gt;Retrieve email&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/emails&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List emails, newest first&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/list-emails" rel="noopener noreferrer"&gt;List emails&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/emails/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reschedule a scheduled email&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/update-email" rel="noopener noreferrer"&gt;Update email&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/emails/:id/cancel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancel a scheduled or queued email&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/cancel-email" rel="noopener noreferrer"&gt;Cancel email&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/templates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a template draft&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/create-template" rel="noopener noreferrer"&gt;Create template&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/templates/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve a template by id or alias&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/retrieve-template" rel="noopener noreferrer"&gt;Retrieve template&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/templates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List templates&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/list-templates" rel="noopener noreferrer"&gt;List templates&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/templates/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update the draft&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/update-template" rel="noopener noreferrer"&gt;Update template&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/templates/:id/publish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Publish the draft as the next version&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/publish-template" rel="noopener noreferrer"&gt;Publish template&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/templates/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a template&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/delete-template" rel="noopener noreferrer"&gt;Delete template&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/contacts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add up to 1,000 contacts to a list&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cresca.xyz/docs/api-reference/add-contacts" rel="noopener noreferrer"&gt;Add contacts&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Authentication: API keys and the Bearer header
&lt;/h2&gt;

&lt;p&gt;Create keys on the &lt;a href="https://app.cresca.xyz/api-keys" rel="noopener noreferrer"&gt;API keys&lt;/a&gt; page of the Cresca app. Keys start with &lt;code&gt;cr_live_&lt;/code&gt; and are shown once; Cresca stores only a hash. Send the key as a bearer token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Authorization: Bearer cr_live_xxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;X-API-Key&lt;/code&gt; header also works. Keep keys on your server, never in browser code, URLs or public repositories.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Can do&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sending access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Send emails and batches. Can be limited to one verified domain.&lt;/td&gt;
&lt;td&gt;Production servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Everything: send, read and cancel emails, manage templates, add contacts&lt;/td&gt;
&lt;td&gt;Scripts and internal tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Authentication errors: &lt;code&gt;401 missing_api_key&lt;/code&gt; (no key), &lt;code&gt;403 invalid_api_key&lt;/code&gt; (unknown or revoked key, or suspended account) and &lt;code&gt;401 restricted_api_key&lt;/code&gt; (a sending-access key called a full-access endpoint). Details are on the &lt;a href="https://www.cresca.xyz/docs/authentication" rel="noopener noreferrer"&gt;Authentication&lt;/a&gt; docs page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example: send your first transactional email
&lt;/h2&gt;

&lt;p&gt;This is the whole path from a fresh account to a delivered email. It mirrors the &lt;a href="https://www.cresca.xyz/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Verify a sending domain
&lt;/h3&gt;

&lt;p&gt;In the app, open Sending Domains, add your domain and publish the DNS records Cresca shows you: DKIM, a custom MAIL FROM domain and an ownership record. If your DNS is on Cloudflare, Cresca can publish them for you. The domain becomes active once the records verify. A subdomain such as &lt;code&gt;mail.acme.com&lt;/code&gt; keeps the reputation of app email separate from campaigns. See &lt;a href="https://www.cresca.xyz/sending-domains" rel="noopener noreferrer"&gt;sending domains&lt;/a&gt; and &lt;a href="https://www.cresca.xyz/blog/email-deliverability-spf-dkim-dmarc-done-right" rel="noopener noreferrer"&gt;SPF, DKIM and DMARC done right&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add a sender
&lt;/h3&gt;

&lt;p&gt;On the same page, create a sender such as &lt;code&gt;hello@acme.com&lt;/code&gt; with a display name and an optional Reply-To. Every API &lt;code&gt;from&lt;/code&gt; address must be an active sender on a verified domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create an API key
&lt;/h3&gt;

&lt;p&gt;Create a &lt;strong&gt;Sending access&lt;/strong&gt; key for your server and store it as &lt;code&gt;CRESCA_API_KEY&lt;/code&gt;. Create a separate full-access key for the status check in step 5.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Send the email
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Idempotency-Key&lt;/code&gt; header makes the request safe to retry.&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="s1"&gt;'https://api.cresca.xyz/api/v1/emails'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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="s1"&gt;'Idempotency-Key: password-reset/reset-request-9f2c'&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;'{
    "from": "Acme &amp;lt;hello@acme.com&amp;gt;",
    "to": "user@example.com",
    "subject": "Reset your password",
    "html": "&amp;lt;p&amp;gt;Click &amp;lt;a href=\"https://acme.com/reset?token=abc\"&amp;gt;here&amp;lt;/a&amp;gt; to reset your password.&amp;lt;/p&amp;gt;"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js 18+, run as an ES module for top-level await&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.cresca.xyz/api/v1/emails&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;CRESCA_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password-reset/reset-request-9f2c&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Click &amp;lt;a href="https://acme.com/reset?token=abc"&amp;gt;here&amp;lt;/a&amp;gt; to reset your password.&amp;lt;/p&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.cresca.xyz/api/v1/emails&lt;/span&gt;&lt;span class="sh"&gt;"&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;Authorization&lt;/span&gt;&lt;span class="sh"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CRESCA_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&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;Idempotency-Key&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;password-reset/reset-request-9f2c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&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;from&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;Acme &amp;lt;hello@acme.com&amp;gt;&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;to&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;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&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;Reset your password&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;html&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;&amp;lt;p&amp;gt;Click &amp;lt;a href=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://acme.com/reset?token=abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;here&amp;lt;/a&amp;gt; to reset your password.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;200&lt;/code&gt; response returns the id as soon as the email is accepted; sending happens in the background within seconds.&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4ef9a417-02e9-4d39-ad75-9611e0fcc33c"&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;h3&gt;
  
  
  Step 5: Check the status
&lt;/h3&gt;

&lt;p&gt;Retrieve the email with your full-access key, or open Transactional in the app for the full timeline.&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="s1"&gt;'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&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;"object"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4ef9a417-02e9-4d39-ad75-9611e0fcc33c"&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;"Acme &amp;lt;hello@acme.com&amp;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;"to"&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;"user@example.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;"cc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bcc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reset your password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;p&amp;gt;Click &amp;lt;a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;https://acme.com/reset?token=abc&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;here&amp;lt;/a&amp;gt; to reset your password.&amp;lt;/p&amp;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;"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;"Click here (https://acme.com/reset?token=abc) to reset your password."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-19T09:14:03.120Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheduled_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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;&lt;code&gt;last_event&lt;/code&gt; moves from &lt;code&gt;queued&lt;/code&gt; through &lt;code&gt;sending&lt;/code&gt; and &lt;code&gt;sent&lt;/code&gt; to &lt;code&gt;delivered&lt;/code&gt; (or &lt;code&gt;bounced&lt;/code&gt;). Emails are kept for 30 days; another account's id returns &lt;code&gt;404 not_found&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emails API
&lt;/h2&gt;

&lt;p&gt;The email sending API has six endpoints. &lt;code&gt;POST /emails&lt;/code&gt; takes these fields; the deep dive is in &lt;a href="https://www.cresca.xyz/blog/send-email-api" rel="noopener noreferrer"&gt;the send email API guide&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;from&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Yes*&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hello@acme.com&lt;/code&gt; or &lt;code&gt;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/code&gt;; an active sender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;to&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string or string[]&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Up to 50 recipients across to, cc and bcc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Yes*&lt;/td&gt;
&lt;td&gt;Single line, up to 998 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;html&lt;/code&gt; / &lt;code&gt;text&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;One of html, text, template&lt;/td&gt;
&lt;td&gt;Text is generated from HTML if omitted; &lt;code&gt;""&lt;/code&gt; leaves it out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cc&lt;/code&gt;, &lt;code&gt;bcc&lt;/code&gt;, &lt;code&gt;reply_to&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;string or string[]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reply_to&lt;/code&gt; defaults to the sender's Reply-To&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ "id", "variables" }&lt;/code&gt;; cannot be combined with html or text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduled_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;ISO 8601, up to 30 days ahead; a past time sends now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object[]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Up to 10 &lt;code&gt;{ "name", "value" }&lt;/code&gt; pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Up to 50 custom headers; Cresca-controlled ones are refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attachments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object[]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Up to 20, 7 MB total, base64 &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;filename&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Optional when a template supplies a default. Unknown fields are ignored. Attaching by URL (&lt;code&gt;path&lt;/code&gt;) is not supported.&lt;/p&gt;

&lt;p&gt;The shorter Node.js examples below use this helper:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cresca&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.cresca.xyz/api/v1&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;CRESCA_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send a batch
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /emails/batch&lt;/code&gt; takes a JSON array of up to 100 emails with the same fields, except &lt;code&gt;attachments&lt;/code&gt; and &lt;code&gt;scheduled_at&lt;/code&gt;. A batch is all or nothing: if one email is invalid, nothing is sent and the message names it, for example &lt;code&gt;Email 3: ...&lt;/code&gt; (zero-based).&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="s1"&gt;'https://api.cresca.xyz/api/v1/emails/batch'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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="s1"&gt;'Idempotency-Key: weekly-digest/2026-10-19'&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;'[
    { "from": "Acme &amp;lt;hello@acme.com&amp;gt;", "to": "ana@example.com",
      "subject": "Your weekly summary", "html": "&amp;lt;p&amp;gt;Hi Ana, here is your summary.&amp;lt;/p&amp;gt;" },
    { "from": "Acme &amp;lt;hello@acme.com&amp;gt;", "to": "ben@example.com",
      "subject": "Your weekly summary", "html": "&amp;lt;p&amp;gt;Hi Ben, here is your summary.&amp;lt;/p&amp;gt;" }
  ]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/emails/batch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// array of up to 100 emails&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;"data"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b6d1f2a0-5e2f-4c8e-9a51-0c8b5f1d2e33"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0f7e3c11-9b2a-4f6d-8e44-7a1c2d3e4f55"&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;h3&gt;
  
  
  List emails
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GET /emails&lt;/code&gt; accepts &lt;code&gt;limit&lt;/code&gt; (1 to 100, default 20) and either &lt;code&gt;after&lt;/code&gt; or &lt;code&gt;before&lt;/code&gt; (an email id). Items leave out &lt;code&gt;html&lt;/code&gt; and &lt;code&gt;text&lt;/code&gt;.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.cresca.xyz/api/v1/emails&lt;/span&gt;&lt;span class="sh"&gt;"&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;Authorization&lt;/span&gt;&lt;span class="sh"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CRESCA_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;params&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;limit&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="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&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;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"has_more"&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;"data"&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;"object"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4ef9a417-02e9-4d39-ad75-9611e0fcc33c"&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;"Acme &amp;lt;hello@acme.com&amp;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;"to"&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;"user@example.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;"cc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bcc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reply_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reset your password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-19T09:14:03.120Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"scheduled_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"last_event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the next page, pass the last item's id as &lt;code&gt;after&lt;/code&gt; while &lt;code&gt;has_more&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reschedule and cancel
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PATCH /emails/:id&lt;/code&gt; takes one required field, &lt;code&gt;scheduled_at&lt;/code&gt;, and only works while the status is &lt;code&gt;scheduled&lt;/code&gt;. &lt;code&gt;POST /emails/:id/cancel&lt;/code&gt; works while the email is &lt;code&gt;scheduled&lt;/code&gt; or &lt;code&gt;queued&lt;/code&gt;; a canceled email does not count toward your allowance.&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; PATCH &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{ "scheduled_at": "2026-10-21T09:00:00Z" }'&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/cancel'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`/emails/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&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="na"&gt;scheduled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-21T09:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`/emails/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cancel`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both return &lt;code&gt;{ "object": "email", "id": "4ef9a417-..." }&lt;/code&gt;. Rescheduling an email that is not scheduled, or canceling one that is already sending, returns &lt;code&gt;422 validation_error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common Emails API errors:&lt;/strong&gt; &lt;code&gt;403 invalid_from_address&lt;/code&gt; (not an active sender, or the key is limited to another domain), &lt;code&gt;422 missing_required_field&lt;/code&gt; (no &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt; or &lt;code&gt;subject&lt;/code&gt;), &lt;code&gt;422 validation_error&lt;/code&gt; (bad address, more than 50 recipients, template combined with html), &lt;code&gt;422 invalid_attachment&lt;/code&gt;, &lt;code&gt;429 monthly_quota_exceeded&lt;/code&gt; and &lt;code&gt;404 not_found&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates API
&lt;/h2&gt;

&lt;p&gt;Templates let you design an email once, publish it, and send it by alias with variables written as &lt;code&gt;{{{NAME}}}&lt;/code&gt;. Edits change the draft; sends always use the published version. All six endpoints need a full-access key, and &lt;code&gt;:id&lt;/code&gt; accepts the id or the alias. More depth: &lt;a href="https://www.cresca.xyz/blog/email-templates-api" rel="noopener noreferrer"&gt;the email templates API guide&lt;/a&gt; and &lt;a href="https://www.cresca.xyz/docs/templates" rel="noopener noreferrer"&gt;Templates and variables&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Up to 120 characters, unique in your account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Body with &lt;code&gt;{{{VARIABLE}}}&lt;/code&gt; placeholders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alias&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Lowercase letters, numbers, dashes, underscores; up to 64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;reply_to&lt;/code&gt;, &lt;code&gt;preview_text&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Defaults used at send time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;variables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Up to 50 &lt;code&gt;{ "key", "type", "fallback_value" }&lt;/code&gt;; type is string or number&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Create and publish
&lt;/h3&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="s1"&gt;'https://api.cresca.xyz/api/v1/templates'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "name": "Password reset",
    "alias": "password-reset",
    "subject": "Reset your password",
    "from": "Acme &amp;lt;hello@acme.com&amp;gt;",
    "html": "&amp;lt;p&amp;gt;Hi {{{NAME}}},&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&amp;lt;a href=\"{{{RESET_LINK}}}\"&amp;gt;Reset your password&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;",
    "variables": [
      { "key": "NAME", "type": "string", "fallback_value": "there" },
      { "key": "RESET_LINK", "type": "string" }
    ]
  }'&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/templates/password-reset/publish'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/templates&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="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="s1"&gt;Welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Hi {{{NAME}}}&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`/templates/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/publish`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create returns &lt;code&gt;201&lt;/code&gt; with &lt;code&gt;{ "object": "template", "id": "6713b1f4c2a94e0012ab34cd" }&lt;/code&gt;. Publish returns the new version number:&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;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6713b1f4c2a94e0012ab34cd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="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;h3&gt;
  
  
  Send by alias with variables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.cresca.xyz/api/v1/emails&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;CRESCA_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password-reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&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="s1"&gt;Ana&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;RESET_LINK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://acme.com/reset?token=abc&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// { id: '...' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subject and sender come from the template unless your request sets them. Values are HTML-escaped in the HTML body. A variable with no value and no fallback stops the send with &lt;code&gt;422 validation_error&lt;/code&gt; naming it; an unpublished template also returns &lt;code&gt;422&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieve, list, update and delete
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/templates/password-reset'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/templates?limit=10'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/templates/password-reset'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{ "subject": "Reset your Acme password" }'&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="s1"&gt;'https://api.cresca.xyz/api/v1/templates/password-reset'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/templates/password-reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/templates?limit=10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/templates/password-reset&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="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your Acme password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cresca&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/templates/password-reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retrieve returns the template object (list wraps the same objects in &lt;code&gt;{ "object": "list", "has_more", "data" }&lt;/code&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;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6713b1f4c2a94e0012ab34cd"&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;"Password reset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"password-reset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reset your password"&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;"Acme &amp;lt;hello@acme.com&amp;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;"reply_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preview_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;p&amp;gt;Hi {{{NAME}}},&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&amp;lt;a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{{{RESET_LINK}}}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;Reset your password&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variables"&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fallback_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"there"&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RESET_LINK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fallback_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="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;"published_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-19T08:30:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-18T16:02:11.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-19T08:30:00.000Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update returns &lt;code&gt;{ "object": "template", "id" }&lt;/code&gt; and changes only the draft; send &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;alias&lt;/code&gt; to remove it. Delete returns &lt;code&gt;"deleted": true&lt;/code&gt;, and later sends that reference the template fail with &lt;code&gt;404 not_found&lt;/code&gt;. Typical errors: &lt;code&gt;422 missing_required_field&lt;/code&gt; (no name or html) and &lt;code&gt;422 validation_error&lt;/code&gt; (duplicate name or alias, bad variable).&lt;/p&gt;

&lt;h2&gt;
  
  
  Contacts API
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /contacts&lt;/code&gt; adds up to 1,000 contacts per request to a list for your marketing campaigns, creating the list if needed. It needs a full-access key and is limited to 120 requests per 15 minutes. Full walkthrough: &lt;a href="https://www.cresca.xyz/blog/contacts-api" rel="noopener noreferrer"&gt;the contacts API guide&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Created if it does not exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contacts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(string \&lt;/td&gt;
&lt;td&gt;object)[]&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.cresca.xyz/api/v1/contacts&lt;/span&gt;&lt;span class="sh"&gt;"&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;Authorization&lt;/span&gt;&lt;span class="sh"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CRESCA_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&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;list_name&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;Newsletter signups&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;contacts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&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;ana@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_name&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;Ana&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;ben@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&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;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contact_import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"list"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6713b2a8c2a94e0012ab3511"&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;"Newsletter signups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&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;"received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"created"&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;"updated"&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;"already_present"&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;"rejected"&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;"added_to_list"&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;"limit_reached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&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;Invalid, disposable, role, suppressed and duplicate addresses are listed in &lt;code&gt;errors&lt;/code&gt; with their &lt;code&gt;index&lt;/code&gt;; the rest are still added. An inactive plan returns &lt;code&gt;403 plan_inactive&lt;/code&gt;; a missing &lt;code&gt;list_name&lt;/code&gt; returns &lt;code&gt;422 missing_required_field&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency, lifecycle, suppression, limits and errors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Idempotency
&lt;/h3&gt;

&lt;p&gt;Send an &lt;code&gt;Idempotency-Key&lt;/code&gt; (1 to 256 characters) on &lt;code&gt;POST /emails&lt;/code&gt; or &lt;code&gt;/emails/batch&lt;/code&gt;. The same key and body within 24 hours return the original response with &lt;code&gt;idempotent-replayed: true&lt;/code&gt; and nothing is sent again. The same key with a different body returns &lt;code&gt;409 invalid_idempotent_request&lt;/code&gt;; a key still in flight returns &lt;code&gt;409 concurrent_idempotent_requests&lt;/code&gt;. A failed request releases its key. Derive keys from the event, such as &lt;code&gt;invoice/inv-2026-0042&lt;/code&gt;. See &lt;a href="https://www.cresca.xyz/docs/idempotency" rel="noopener noreferrer"&gt;Idempotency&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Email lifecycle: statuses and events
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;last_event&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;queued&lt;/code&gt; / &lt;code&gt;scheduled&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Accepted, waiting to send (or to its &lt;code&gt;scheduled_at&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sending&lt;/code&gt; / &lt;code&gt;sent&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Being handed to, then accepted by, the mail infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The recipient's server accepted it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivery_delayed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delivery is being retried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bounced&lt;/code&gt; / &lt;code&gt;complained&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rejected, or marked as spam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;failed&lt;/code&gt; / &lt;code&gt;suppressed&lt;/code&gt; / &lt;code&gt;canceled&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not sent, every recipient suppressed, or canceled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Status only moves forward, while &lt;code&gt;last_event&lt;/code&gt; is the latest event. &lt;code&gt;delivered&lt;/code&gt; means the receiving server accepted it, not that it reached the inbox. Full table: &lt;a href="https://www.cresca.xyz/docs/email-lifecycle" rel="noopener noreferrer"&gt;Email lifecycle&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suppression
&lt;/h3&gt;

&lt;p&gt;Hard bounces, repeated soft bounces (3 in 30 days), spam complaints and manual suppressions block API email; suppressed recipients are skipped and the others still receive it. A marketing unsubscribe does not block a password reset. If every recipient is suppressed you still get &lt;code&gt;200&lt;/code&gt;, and the status is &lt;code&gt;suppressed&lt;/code&gt;. See &lt;a href="https://www.cresca.xyz/docs/suppression" rel="noopener noreferrer"&gt;Suppression&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limits and quotas
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Requests per second&lt;/th&gt;
&lt;th&gt;Emails per second with batches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;up to 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Professional&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;up to 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;up to 2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;up to 5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;up to 10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The limit is per account, shared by all keys, and an inactive plan gets the Free limit. Authenticated responses carry &lt;code&gt;ratelimit-limit&lt;/code&gt;, &lt;code&gt;ratelimit-remaining&lt;/code&gt; and &lt;code&gt;ratelimit-reset&lt;/code&gt;; a &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt; adds &lt;code&gt;retry-after&lt;/code&gt;. API email shares your plan's monthly email allowance with campaigns, one unit per recipient; when it runs out you get &lt;code&gt;429 monthly_quota_exceeded&lt;/code&gt;. Request bodies are capped at 10 MB. See &lt;a href="https://www.cresca.xyz/docs/rate-limits" rel="noopener noreferrer"&gt;Rate limits and quotas&lt;/a&gt; and &lt;a href="https://www.cresca.xyz/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Errors
&lt;/h3&gt;

&lt;p&gt;Every error has the same shape. Branch on &lt;code&gt;name&lt;/code&gt;, not &lt;code&gt;message&lt;/code&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;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&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;"validation_error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&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 `to` field. The email address needs to follow the `email@example.com` or `Name &amp;lt;email@example.com&amp;gt;` format."&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;Retry &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt;, &lt;code&gt;409 concurrent_idempotent_requests&lt;/code&gt;, &lt;code&gt;500 application_error&lt;/code&gt;, &lt;code&gt;503 service_unavailable&lt;/code&gt;, timeouts and network errors with exponential backoff and jitter, using an &lt;code&gt;Idempotency-Key&lt;/code&gt;. Do not retry other 4xx responses; fix the request. The full list is on &lt;a href="https://www.cresca.xyz/docs/errors" rel="noopener noreferrer"&gt;Errors&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is there an official SDK for the Cresca email API?
&lt;/h3&gt;

&lt;p&gt;No. Cresca does not publish an SDK package; the API is plain HTTPS and JSON, so cURL, Node.js &lt;code&gt;fetch&lt;/code&gt;, Python &lt;code&gt;requests&lt;/code&gt; or any HTTP client works. Field names and responses follow the conventions of popular email APIs, so moving an existing integration is mostly a change of base URL and API key. The docs show every request in cURL, Node.js and Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I send marketing email through the transactional email API?
&lt;/h3&gt;

&lt;p&gt;No. The API is for messages a person expects because of something they did, such as receipts, password resets and alerts. Newsletters and promotions belong in Cresca campaigns, which add the unsubscribe link and sender address marketing email legally requires. Keeping them apart also protects deliverability, as explained in &lt;a href="https://www.cresca.xyz/blog/transactional-vs-marketing-email" rel="noopener noreferrer"&gt;transactional vs marketing email&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know an email was delivered?
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;GET /emails/:id&lt;/code&gt; with a full-access key and read &lt;code&gt;last_event&lt;/code&gt;, or open Transactional in the Cresca app for the per-recipient timeline of sent, delivered, bounced and complained events. &lt;code&gt;delivered&lt;/code&gt; means the receiving server accepted the email. Webhooks are on the roadmap; today you read status from the API or the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does API email count toward my plan?
&lt;/h3&gt;

&lt;p&gt;Yes. API email uses the same monthly email allowance as your campaigns, and each recipient in to, cc and bcc counts as one email. Suppressed recipients, canceled emails and emails that fail before sending are given back. When the allowance is used up, sends return &lt;code&gt;429 monthly_quota_exceeded&lt;/code&gt; until the next billing period or an upgrade. See &lt;a href="https://www.cresca.xyz/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if my server retries after a timeout?
&lt;/h3&gt;

&lt;p&gt;If the request carried an &lt;code&gt;Idempotency-Key&lt;/code&gt;, the retry returns the original response with the header &lt;code&gt;idempotent-replayed: true&lt;/code&gt;, and no second email is sent. Without a key, a retry can send a duplicate. Derive the key from the event that caused the email, such as &lt;code&gt;welcome/user-123&lt;/code&gt;, so every retry produces the same key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need SPF, DKIM and DMARC to send transactional email?
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;from&lt;/code&gt; address must be on a domain verified in Cresca, which means publishing the DKIM, custom MAIL FROM and ownership records Cresca shows you. Gmail and Yahoo also expect a DMARC record from bulk senders. The &lt;a href="https://www.cresca.xyz/blog/gmail-yahoo-sender-requirements" rel="noopener noreferrer"&gt;Gmail and Yahoo sender requirements&lt;/a&gt; post lists every rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start sending
&lt;/h2&gt;

&lt;p&gt;A transactional email service should take minutes to set up: verify a domain, create a key, send one request. &lt;a href="https://app.cresca.xyz/signup" rel="noopener noreferrer"&gt;Try Cresca free for 30 days&lt;/a&gt;, then follow the &lt;a href="https://www.cresca.xyz/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; to send your first email.&lt;/p&gt;




</description>
      <category>email</category>
      <category>api</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How our AI agent turns a one-line brief into a responsive email template</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:36:33 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/how-our-ai-agent-turns-a-one-line-brief-into-a-responsive-email-template-1lg1</link>
      <guid>https://dev.to/lalitkumarjangid/how-our-ai-agent-turns-a-one-line-brief-into-a-responsive-email-template-1lg1</guid>
      <description>&lt;p&gt;Anyone who has hand-coded a marketing email knows the pain: nested tables, inline styles, buttons that break in Outlook, images that look fine on desktop and terrible on a phone. Most teams solve it by reusing the same tired template forever.&lt;/p&gt;

&lt;p&gt;We built the Cresca AI agent to fix that part of the job. You describe the email you want in plain words, and it builds a complete, editable email: layout, copy, sections and a clear call to action, ready to preview and send.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it looks like
&lt;/h3&gt;

&lt;p&gt;You type a brief into &lt;strong&gt;Message Cresca AI&lt;/strong&gt;, for example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a welcome email for new subscribers. Thank them, give the code HELLO10 for 10% off the first order, tell our story in two lines, and mention they can reply anytime for help.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few seconds later you have a full email in the &lt;strong&gt;email studio&lt;/strong&gt;: the conversation on the left, a live preview on the right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refine it like you'd brief a designer
&lt;/h3&gt;

&lt;p&gt;The first draft is a starting point, not a final answer. You keep talking to the agent in the same conversation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Make it shorter."&lt;/li&gt;
&lt;li&gt;"Use a warmer tone."&lt;/li&gt;
&lt;li&gt;"Add a section with three product picks."&lt;/li&gt;
&lt;li&gt;"Make the button say &lt;em&gt;Claim my 10%&lt;/em&gt;."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each change updates the preview, so you can see the result before you commit to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  On brand by default
&lt;/h3&gt;

&lt;p&gt;Generic AI output usually looks generic. So the agent works from a &lt;strong&gt;brand profile&lt;/strong&gt;: your brand name, website, logo and brand color, which it uses for buttons and banners. You can also drop in your own photos. Set it up once, and every email starts looking like you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built for the inbox, not just the screenshot
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop and mobile preview&lt;/strong&gt;, side by side with the conversation, so you catch layout problems before your subscribers do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio view&lt;/strong&gt; for a closer look at the full design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy HTML&lt;/strong&gt; if you want the code for another tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save as a template&lt;/strong&gt; and reuse it in campaigns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tips for better results
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One email, one goal.&lt;/strong&gt; Say what you want people to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the facts.&lt;/strong&gt; Paste prices, dates and codes exactly; the agent should never have to guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show your voice.&lt;/strong&gt; A sentence from an email you've sent before helps more than "friendly but professional".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review before you send.&lt;/strong&gt; AI drafts can get details wrong. You stay in charge of what goes out.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;The AI agent is included on every Cresca plan, and you can try it free for 30 days with no credit card: &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;cresca.xyz&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the most painful part of building emails for you? I'd love to hear in the comments.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>email</category>
    </item>
    <item>
      <title>Transactional email checklist: 8 things to get right before your app sends a password reset</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:20:42 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/transactional-email-checklist-8-things-to-get-right-before-your-app-sends-a-password-reset-2g66</link>
      <guid>https://dev.to/lalitkumarjangid/transactional-email-checklist-8-things-to-get-right-before-your-app-sends-a-password-reset-2g66</guid>
      <description>&lt;p&gt;Most apps send their first transactional email the same way: a developer adds a "Forgot password" flow at 1 a.m., pastes an HTML string into the code and moves on. It works, until a reset lands in spam, a customer gets two receipts, or someone who unsubscribed from your newsletter can't log in.&lt;/p&gt;

&lt;p&gt;Transactional email is the email a person triggers and is waiting for: password resets, verification codes, receipts, invites, booking reminders. Here's the checklist I wish I'd had, with examples using the &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;Cresca&lt;/a&gt; transactional email API. The ideas apply to any provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Send from your own domain, ideally a subdomain
&lt;/h2&gt;

&lt;p&gt;An email from &lt;code&gt;hello@yourapp.com&lt;/code&gt; with SPF and DKIM in place gets trusted. An email from a shared provider address gets questioned.&lt;/p&gt;

&lt;p&gt;Use a subdomain for app email, such as &lt;code&gt;mail.yourapp.com&lt;/code&gt;. Its reputation stays separate from your marketing campaigns, so a newsletter that gets a few spam complaints doesn't drag your password resets down with it. A DMARC record on the root domain is worth adding too.&lt;/p&gt;

&lt;p&gt;In Cresca you add the domain under Sending Domains, publish the DNS records it shows you (Cresca can publish them for you if your DNS is on Cloudflare), and add a sender like &lt;code&gt;hello@mail.yourapp.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep the API key on the server, with the least access it needs
&lt;/h2&gt;

&lt;p&gt;Your API key can send email as your company. It belongs in a server environment variable, never in browser or mobile code.&lt;/p&gt;

&lt;p&gt;Give production servers a key that can only send. Cresca has two permissions: &lt;strong&gt;sending access&lt;/strong&gt; (send emails and batches, nothing else, optionally limited to one domain) and &lt;strong&gt;full access&lt;/strong&gt; (also read emails, cancel them and manage templates). Keep the full-access key for admin scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Send the email in one request
&lt;/h2&gt;

&lt;p&gt;Here's a password reset from Node.js 18+, with no SDK:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.cresca.xyz/api/v1/emails&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;CRESCA_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YourApp &amp;lt;hello@mail.yourapp.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;&amp;lt;a href="https://yourapp.com/reset?token=abc"&amp;gt;Reset your password&amp;lt;/a&amp;gt;. This link expires in 1 hour.&amp;lt;/p&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// save it to look the email up later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns as soon as the email is accepted and sends it within seconds. Two details for the email itself: say how long the link is valid, and include a plain-text version. If you leave out &lt;code&gt;text&lt;/code&gt;, Cresca generates one from your HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use templates, not HTML strings in your code
&lt;/h2&gt;

&lt;p&gt;HTML strings scattered across a codebase are hard to review and easy to break. Design the email once as a template, mark the parts your app fills in, and send only the values:&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-receipt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&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="s1"&gt;Ana&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ORDER_NUMBER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1042&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ORDER_TOTAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$42.00&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cresca the template is designed in a visual editor (or created through the API) with placeholders like &lt;code&gt;{{{ORDER_NUMBER}}}&lt;/code&gt;, then published. Sends always use the published version, so editing a draft never changes the emails your app is sending. Values are HTML-escaped, so a customer named &lt;code&gt;&amp;lt;b&amp;gt;Ana&amp;lt;/b&amp;gt;&lt;/code&gt; can't change your markup, and a missing variable with no fallback returns an error instead of sending a broken email.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make retries safe
&lt;/h2&gt;

&lt;p&gt;Networks fail. If the connection drops before the response arrives, you can't tell whether the email was accepted, and a blind retry means two receipts.&lt;/p&gt;

&lt;p&gt;Send an &lt;code&gt;Idempotency-Key&lt;/code&gt; header built from the event that caused the email:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`order-receipt/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Cresca, the same key and body within 24 hours returns the original response with an &lt;code&gt;idempotent-replayed: true&lt;/code&gt; header, and nothing is sent again. The same key with a different body returns &lt;code&gt;409 invalid_idempotent_request&lt;/code&gt;, and if the first request failed, the key is released so your retry goes through.&lt;/p&gt;

&lt;p&gt;Don't build the key from a timestamp or a fresh UUID inside your retry loop, or every retry looks like a new email. One exception: a person who asks for a password reset twice should get two emails, so give each reset request its own key.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Retry the right errors, and back off
&lt;/h2&gt;

&lt;p&gt;Not every failure should be retried. Retry timeouts, network errors, &lt;code&gt;5xx&lt;/code&gt; responses and &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt;, waiting for the &lt;code&gt;retry-after&lt;/code&gt; header if there is one. Don't retry other &lt;code&gt;4xx&lt;/code&gt; errors: a bad address or an unverified sender won't fix itself.&lt;/p&gt;

&lt;p&gt;Every Cresca error has the same shape, so branch on &lt;code&gt;name&lt;/code&gt;, not on the message text:&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;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&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;"invalid_from_address"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&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 `from` address hello@mail.yourapp.com is not an active sender on a verified domain. Add it under Sending domains in Cresca."&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;Rate limits are per account, per second: 2 on the Free plan, up to 100 on Enterprise. If you need to send many emails at once, a batch request carries up to 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Don't let an unsubscribe block a password reset
&lt;/h2&gt;

&lt;p&gt;An unsubscribe means "stop sending me marketing". It doesn't mean "never let me log in again". If your app email and your newsletter share one suppression list, someone who unsubscribed last year can't reset their password today.&lt;/p&gt;

&lt;p&gt;Cresca's API ignores marketing unsubscribes for transactional email, while campaigns always respect them. Hard bounces and spam complaints still block an address everywhere, because those addresses really shouldn't get more email.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Know what "delivered" means, and check it
&lt;/h2&gt;

&lt;p&gt;"Delivered" means the receiving mail server accepted the email. It doesn't mean it reached the inbox rather than spam: no sender can see that. So log the email id you get back, and when a customer says "I never got it", look it up:&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="s1"&gt;'https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer cr_live_xxxxxxxxx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes &lt;code&gt;last_event&lt;/code&gt;: &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;bounced&lt;/code&gt;, &lt;code&gt;complained&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;, among others. The dashboard shows the full timeline per recipient. If it says delivered and the customer still can't find it, ask them to check spam, and look at your domain setup from point 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: scheduled emails you can cancel
&lt;/h2&gt;

&lt;p&gt;Booking reminders and trial-ending notices are transactional too. Add &lt;code&gt;scheduled_at&lt;/code&gt; (up to 30 days ahead) when you send, save the id, and cancel or move the email if the booking changes:&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YourApp &amp;lt;hello@mail.yourapp.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reminder: your call is tomorrow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your call is booked for 5 November at 10:00 UTC.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scheduled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-11-04T10:00:00Z&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;A canceled email doesn't count toward your monthly allowance.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Send from your own domain, ideally a subdomain.&lt;/li&gt;
&lt;li&gt;Keep keys on the server, sending-only in production.&lt;/li&gt;
&lt;li&gt;Say how long links last and include plain text.&lt;/li&gt;
&lt;li&gt;Use templates with variables, not HTML strings.&lt;/li&gt;
&lt;li&gt;Send an &lt;code&gt;Idempotency-Key&lt;/code&gt; built from the event.&lt;/li&gt;
&lt;li&gt;Retry only what can succeed, with backoff.&lt;/li&gt;
&lt;li&gt;Keep unsubscribes out of your transactional suppression.&lt;/li&gt;
&lt;li&gt;Log email ids and check status when someone says "I never got it".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to try this, &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;Cresca&lt;/a&gt; sends transactional email and marketing campaigns from the same account and the same domain, and the API works on the Free plan. The &lt;a href="https://www.cresca.xyz/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt; have a quickstart in cURL, Node.js and Python. Webhooks and an SMTP relay are on our list; for now, delivery status comes from the API or the dashboard.&lt;/p&gt;

&lt;p&gt;What's on your transactional email checklist that isn't on mine?&lt;/p&gt;

</description>
      <category>email</category>
      <category>api</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Estimating Gmail's Promotions tab without pretending to predict it</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:30:28 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/estimating-gmails-promotions-tab-without-pretending-to-predict-it-2d16</link>
      <guid>https://dev.to/lalitkumarjangid/estimating-gmails-promotions-tab-without-pretending-to-predict-it-2d16</guid>
      <description>&lt;p&gt;Every marketer has asked the same question: will this land in Primary or Promotions?&lt;/p&gt;

&lt;p&gt;Nobody outside Google can answer that with certainty. Gmail decides the tab using the message content, the sender's history, and how each recipient has engaged with that sender before. Two people on the same list can get the same email in different tabs. Any tool that claims to &lt;em&gt;predict&lt;/em&gt; placement is overselling.&lt;/p&gt;

&lt;p&gt;When we built the "Gmail tab estimate" in Cresca, we started from that limitation. This post covers what we built, how it works and where it falls short.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we're actually estimating
&lt;/h3&gt;

&lt;p&gt;The feature does not predict the tab. It estimates &lt;strong&gt;how promotional the email looks from its content&lt;/strong&gt;: the one input to Gmail's decision that the sender fully controls. The UI says this directly. Expand the "Why" panel and the first line reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gmail decides the tab using your content, your sending history and how each person has engaged with you before. This estimates how promotional the email looks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The output is one of three labels, never a percentage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Likely Primary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Could go either way&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Likely Promotions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We avoided showing a raw score because "73%" suggests a precision we don't have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it runs
&lt;/h3&gt;

&lt;p&gt;The estimate runs entirely in the browser on the rendered HTML, the same markup that will be sent. It's a pure function, so it re-runs whenever the email or subject line changes, inside the AI conversation view, the design editor and the template preview:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;analyzeInboxPlacement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&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;Running it on the client made sense. It's quick, it updates as you edit, and it needs no network call. It also keeps us honest, because it's a small set of readable rules rather than a black box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Ignore what every email shares
&lt;/h3&gt;

&lt;p&gt;We parse the HTML with &lt;code&gt;DOMParser&lt;/code&gt; and remove nodes that would add noise:&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="nx"&gt;doc&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cresca-footer], style, script, title, [style*="display:none"], [style*="display: none"]&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Cresca email gets an automatic compliance footer (unsubscribe link and address), and many have a hidden preheader. Since those are the same on every email, counting them would add noise and hide what the sender can change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Measure the signals
&lt;/h3&gt;

&lt;p&gt;We measure the features that make an email look like a newsletter or campaign rather than a message from a person:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;, ignoring very small ones (48px wide or less, such as social icons), since a logo-sized icon isn't the same signal as a hero banner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinct links&lt;/strong&gt;, excluding &lt;code&gt;mailto:&lt;/code&gt;, &lt;code&gt;tel:&lt;/code&gt; and in-page anchors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons&lt;/strong&gt;: links styled as buttons, counted once each even if several selectors match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coloured blocks&lt;/strong&gt;: elements with a &lt;code&gt;bgcolor&lt;/code&gt; that isn't white or a light grey. We use a simple colour test: a colour is neutral if its RGB channels sit within 24 of each other and all are above 200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-column layout&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sale wording&lt;/strong&gt; in the subject and body: "% off", "discount", "promo code", "shop now", "limited time", "last chance", "ends tonight" and similar phrases, found with one regex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text volume&lt;/strong&gt;: a word count, so we can flag an email that is mostly images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal markers&lt;/strong&gt;: a &lt;code&gt;{{first_name}}&lt;/code&gt; token or a greeting like "Hi" or "Hello" at the start.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Score it, simply
&lt;/h3&gt;

&lt;p&gt;The score starts at 100. Each signal subtracts a fixed penalty, with heavier penalties for stronger signals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Penalty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3+ images / 1–2 images&lt;/td&gt;
&lt;td&gt;25 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6+ links / 3–5 links&lt;/td&gt;
&lt;td&gt;20 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2+ buttons / 1 button&lt;/td&gt;
&lt;td&gt;15 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coloured banners or blocks&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-column layout&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3+ sale phrases / 1–2&lt;/td&gt;
&lt;td&gt;25 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Under 60 words &lt;em&gt;and&lt;/em&gt; at least one image&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal greeting or first-name token&lt;/td&gt;
&lt;td&gt;+5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result is clamped to 0–100. A score of 70 or more is "Likely Primary", 45–69 is "Could go either way", and anything lower is "Likely Promotions".&lt;/p&gt;

&lt;p&gt;I should be clear about where these numbers came from. &lt;strong&gt;They are hand-tuned weights, not a trained model.&lt;/strong&gt; We based them on Google's public guidance and on how personal email differs from campaign email: personal email is mostly text, with few links, no banners and one column. The weights express a judgment about which signals matter most. They are not measured from Gmail's behaviour, and we don't present them that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Explain, don't just score
&lt;/h3&gt;

&lt;p&gt;A label alone isn't useful. Every penalty also records a reason and a fix, and the "Why" panel shows them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"4 images: use one image or none, because image-heavy emails look like marketing."&lt;/li&gt;
&lt;li&gt;"Sale wording: % off, shop now: promotional wording is a strong Promotions signal."&lt;/li&gt;
&lt;li&gt;"1 button: a plain text link reads more personal than a button."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When there are no penalties, the panel says the email reads like a personal email: mostly text, few links, no banners.&lt;/p&gt;

&lt;p&gt;The panel also gives three tips that don't depend on content: send from a person's name, ask readers to reply (replies show Gmail your emails are wanted), and remember that &lt;strong&gt;sales and big launches belong in Promotions, which is normal&lt;/strong&gt;. That last tip matters. A product launch &lt;em&gt;should&lt;/em&gt; look like a promotion. We don't want people removing every image from a launch email to chase the Primary tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one-click rewrite
&lt;/h3&gt;

&lt;p&gt;In the AI conversation view, if the estimate isn't "Likely Primary", there's a &lt;strong&gt;Rewrite as a plain personal email&lt;/strong&gt; action. It sends the current email back to the AI agent with an instruction to turn it into a plain, personal-style message. The estimate then re-runs on the new HTML, so you can compare before and after.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell anyone building something similar
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name what you can't see.&lt;/strong&gt; Engagement and reputation aren't visible to a content check. Say so in the UI, not in a footnote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer labels to percentages&lt;/strong&gt; when your confidence is coarse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every penalty needs a fix.&lt;/strong&gt; A score without an action just makes people anxious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fight the user's intent.&lt;/strong&gt; Some emails are meant to be promotional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it readable.&lt;/strong&gt; A rule-based heuristic you can explain beats an opaque model you can't defend when someone asks "why?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll keep tuning the weights as we learn. The design principle stays the same: show people what they control, and be honest about what they don't.&lt;/p&gt;

&lt;p&gt;If you want to try it on your own emails, Cresca is free for 30 days at &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;https://www.cresca.xyz&lt;/a&gt;, no credit card needed. Build an email with the AI agent or a template and open the "Why" panel before you send.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lalit Kumar Jangid, founder of Cresca&lt;/em&gt;&lt;/p&gt;

</description>
      <category>email</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Estimating Gmail's Promotions tab without pretending to predict it</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Wed, 14 Oct 2026 04:55:51 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/estimating-gmails-promotions-tab-without-pretending-to-predict-it-3b0a</link>
      <guid>https://dev.to/lalitkumarjangid/estimating-gmails-promotions-tab-without-pretending-to-predict-it-3b0a</guid>
      <description>&lt;p&gt;Every marketer has asked the same question: will this land in Primary or Promotions?&lt;/p&gt;

&lt;p&gt;Nobody outside Google can answer that with certainty. Gmail decides the tab using the message content, the sender's history, and how each recipient has engaged with that sender before. Two people on the same list can get the same email in different tabs. Any tool that claims to &lt;em&gt;predict&lt;/em&gt; placement is overselling.&lt;/p&gt;

&lt;p&gt;When we built the "Gmail tab estimate" in Cresca, we started from that limitation. This post covers what we built, how it works and where it falls short.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we're actually estimating
&lt;/h3&gt;

&lt;p&gt;The feature does not predict the tab. It estimates &lt;strong&gt;how promotional the email looks from its content&lt;/strong&gt;: the one input to Gmail's decision that the sender fully controls. The UI says this directly. Expand the "Why" panel and the first line reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gmail decides the tab using your content, your sending history and how each person has engaged with you before. This estimates how promotional the email looks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The output is one of three labels, never a percentage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Likely Primary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Could go either way&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Likely Promotions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We avoided showing a raw score because "73%" suggests a precision we don't have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it runs
&lt;/h3&gt;

&lt;p&gt;The estimate runs entirely in the browser on the rendered HTML, the same markup that will be sent. It's a pure function, so it re-runs whenever the email or subject line changes, inside the AI conversation view, the design editor and the template preview:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;analyzeInboxPlacement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&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;Running it on the client made sense. It's quick, it updates as you edit, and it needs no network call. It also keeps us honest, because it's a small set of readable rules rather than a black box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Ignore what every email shares
&lt;/h3&gt;

&lt;p&gt;We parse the HTML with &lt;code&gt;DOMParser&lt;/code&gt; and remove nodes that would add noise:&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="nx"&gt;doc&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cresca-footer], style, script, title, [style*="display:none"], [style*="display: none"]&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Cresca email gets an automatic compliance footer (unsubscribe link and address), and many have a hidden preheader. Since those are the same on every email, counting them would add noise and hide what the sender can change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Measure the signals
&lt;/h3&gt;

&lt;p&gt;We measure the features that make an email look like a newsletter or campaign rather than a message from a person:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;, ignoring very small ones (48px wide or less, such as social icons), since a logo-sized icon isn't the same signal as a hero banner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinct links&lt;/strong&gt;, excluding &lt;code&gt;mailto:&lt;/code&gt;, &lt;code&gt;tel:&lt;/code&gt; and in-page anchors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons&lt;/strong&gt;: links styled as buttons, counted once each even if several selectors match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coloured blocks&lt;/strong&gt;: elements with a &lt;code&gt;bgcolor&lt;/code&gt; that isn't white or a light grey. We use a simple colour test: a colour is neutral if its RGB channels sit within 24 of each other and all are above 200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-column layout&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sale wording&lt;/strong&gt; in the subject and body: "% off", "discount", "promo code", "shop now", "limited time", "last chance", "ends tonight" and similar phrases, found with one regex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text volume&lt;/strong&gt;: a word count, so we can flag an email that is mostly images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal markers&lt;/strong&gt;: a &lt;code&gt;{{first_name}}&lt;/code&gt; token or a greeting like "Hi" or "Hello" at the start.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Score it, simply
&lt;/h3&gt;

&lt;p&gt;The score starts at 100. Each signal subtracts a fixed penalty, with heavier penalties for stronger signals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Penalty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3+ images / 1–2 images&lt;/td&gt;
&lt;td&gt;25 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6+ links / 3–5 links&lt;/td&gt;
&lt;td&gt;20 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2+ buttons / 1 button&lt;/td&gt;
&lt;td&gt;15 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coloured banners or blocks&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-column layout&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3+ sale phrases / 1–2&lt;/td&gt;
&lt;td&gt;25 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Under 60 words &lt;em&gt;and&lt;/em&gt; at least one image&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal greeting or first-name token&lt;/td&gt;
&lt;td&gt;+5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result is clamped to 0–100. A score of 70 or more is "Likely Primary", 45–69 is "Could go either way", and anything lower is "Likely Promotions".&lt;/p&gt;

&lt;p&gt;I should be clear about where these numbers came from. &lt;strong&gt;They are hand-tuned weights, not a trained model.&lt;/strong&gt; We based them on Google's public guidance and on how personal email differs from campaign email: personal email is mostly text, with few links, no banners and one column. The weights express a judgment about which signals matter most. They are not measured from Gmail's behaviour, and we don't present them that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Explain, don't just score
&lt;/h3&gt;

&lt;p&gt;A label alone isn't useful. Every penalty also records a reason and a fix, and the "Why" panel shows them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"4 images: use one image or none, because image-heavy emails look like marketing."&lt;/li&gt;
&lt;li&gt;"Sale wording: % off, shop now: promotional wording is a strong Promotions signal."&lt;/li&gt;
&lt;li&gt;"1 button: a plain text link reads more personal than a button."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When there are no penalties, the panel says the email reads like a personal email: mostly text, few links, no banners.&lt;/p&gt;

&lt;p&gt;The panel also gives three tips that don't depend on content: send from a person's name, ask readers to reply (replies show Gmail your emails are wanted), and remember that &lt;strong&gt;sales and big launches belong in Promotions, which is normal&lt;/strong&gt;. That last tip matters. A product launch &lt;em&gt;should&lt;/em&gt; look like a promotion. We don't want people removing every image from a launch email to chase the Primary tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one-click rewrite
&lt;/h3&gt;

&lt;p&gt;In the AI conversation view, if the estimate isn't "Likely Primary", there's a &lt;strong&gt;Rewrite as a plain personal email&lt;/strong&gt; action. It sends the current email back to the AI agent with an instruction to turn it into a plain, personal-style message. The estimate then re-runs on the new HTML, so you can compare before and after.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell anyone building something similar
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name what you can't see.&lt;/strong&gt; Engagement and reputation aren't visible to a content check. Say so in the UI, not in a footnote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer labels to percentages&lt;/strong&gt; when your confidence is coarse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every penalty needs a fix.&lt;/strong&gt; A score without an action just makes people anxious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fight the user's intent.&lt;/strong&gt; Some emails are meant to be promotional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it readable.&lt;/strong&gt; A rule-based heuristic you can explain beats an opaque model you can't defend when someone asks "why?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll keep tuning the weights as we learn. The design principle stays the same: show people what they control, and be honest about what they don't.&lt;/p&gt;

&lt;p&gt;If you want to try it on your own emails, Cresca is free for 30 days at &lt;a href="https://www.cresca.xyz" rel="noopener noreferrer"&gt;https://www.cresca.xyz&lt;/a&gt;, no credit card needed. Build an email with the AI agent or a template and open the "Why" panel before you send.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lalit Kumar Jangid, founder of Cresca&lt;/em&gt;&lt;/p&gt;




</description>
    </item>
    <item>
      <title>AI n8n Integrations: The Ultimate Guide to Transforming Your Workflow in 2025</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Fri, 08 Aug 2025 17:42:56 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/ai-n8n-integrations-the-ultimate-guide-to-transforming-your-workflow-in-2025-53g8</link>
      <guid>https://dev.to/lalitkumarjangid/ai-n8n-integrations-the-ultimate-guide-to-transforming-your-workflow-in-2025-53g8</guid>
      <description>&lt;h1&gt;
  
  
  🚀 AI n8n Integrations: The Ultimate Guide to Transforming Your Workflow in 2025
&lt;/h1&gt;

&lt;p&gt;Ever wondered how the world's most productive teams automate their workflows while staying ahead of the AI curve? The answer lies in the powerful combination of n8n (pronounced "no-code") automation and cutting-edge AI technologies. In 2025, this dynamic duo isn't just changing how we work—it's revolutionizing entire industries.&lt;/p&gt;

&lt;p&gt;Whether you're a seasoned developer looking to supercharge your productivity or a business leader seeking to implement intelligent automation, this comprehensive guide will show you exactly how to harness the power of AI-driven n8n workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 What You'll Master in This Guide
&lt;/h2&gt;

&lt;p&gt;By the end of this article, you'll know how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build intelligent workflows that learn and adapt&lt;/li&gt;
&lt;li&gt;Integrate GPT-4, Claude, and other LLMs seamlessly&lt;/li&gt;
&lt;li&gt;Automate complex decision-making processes&lt;/li&gt;
&lt;li&gt;Create self-improving business workflows&lt;/li&gt;
&lt;li&gt;Deploy production-ready AI automation systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🌟 Why AI + n8n = Workflow Revolution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Perfect Storm of Automation
&lt;/h3&gt;

&lt;p&gt;n8n's visual workflow builder combined with AI's decision-making capabilities creates something magical: &lt;strong&gt;intelligent automation that thinks&lt;/strong&gt;. Unlike traditional automation that follows rigid rules, AI-powered n8n workflows can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Understand context&lt;/strong&gt;: Process natural language inputs and make contextual decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn from patterns&lt;/strong&gt;: Adapt behavior based on historical data and outcomes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle exceptions&lt;/strong&gt;: Intelligently manage edge cases without breaking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale infinitely&lt;/strong&gt;: Process thousands of requests while maintaining consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real-World Impact: The Numbers Don't Lie
&lt;/h3&gt;

&lt;p&gt;Organizations implementing AI n8n workflows report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;89% reduction&lt;/strong&gt; in manual task processing time&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;340% increase&lt;/strong&gt; in workflow accuracy&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;$2.4M average annual savings&lt;/strong&gt; per enterprise implementation&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;95% faster&lt;/strong&gt; decision-making in critical business processes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛠️ Essential AI Integrations for n8n
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. OpenAI GPT Integration
&lt;/h3&gt;

&lt;p&gt;The crown jewel of AI integrations, OpenAI's models bring natural language understanding to your workflows.&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="c1"&gt;// Example: Intelligent Email Classification&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messages&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Classify emails into: urgent, normal, or spam. Respond with category and confidence score.&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $json.email_content }}&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;temperature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pro Tip&lt;/strong&gt;: Keep temperature low (0.1-0.3) for classification tasks to ensure consistent results.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Claude Integration for Complex Reasoning
&lt;/h3&gt;

&lt;p&gt;Anthropic's Claude excels at nuanced reasoning and ethical decision-making:&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="c1"&gt;// Advanced Content Analysis Workflow&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic_version&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2023-06-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messages&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Analyze this customer feedback for sentiment, key issues, and recommended actions: {{ $json.feedback }}&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Google Gemini for Multimodal Processing
&lt;/h3&gt;

&lt;p&gt;Handle text, images, and data simultaneously:&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="c1"&gt;// Image + Text Analysis&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contents&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parts&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Analyze this product image and description for quality issues&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline_data&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mime_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $json.image_base64 }}&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="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 Top 10 Game-Changing AI n8n Workflow Templates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Intelligent Customer Support Triage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automatically categorize and route support tickets&lt;br&gt;
&lt;strong&gt;AI Model&lt;/strong&gt;: GPT-4 for understanding, sentiment analysis&lt;br&gt;
&lt;strong&gt;Workflow Steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receive ticket via webhook&lt;/li&gt;
&lt;li&gt;AI analyzes content and urgency&lt;/li&gt;
&lt;li&gt;Auto-assigns to appropriate team&lt;/li&gt;
&lt;li&gt;Generates initial response draft&lt;/li&gt;
&lt;li&gt;Tracks resolution time and satisfaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Business Impact&lt;/strong&gt;: 75% faster ticket resolution, 90% customer satisfaction&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Smart Content Generation Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Create blog posts, social media content, and marketing materials&lt;br&gt;
&lt;strong&gt;AI Model&lt;/strong&gt;: GPT-4 for writing, DALL-E for images&lt;br&gt;
&lt;strong&gt;Workflow Steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input topic and target audience&lt;/li&gt;
&lt;li&gt;Research trending keywords&lt;/li&gt;
&lt;li&gt;Generate outline and content&lt;/li&gt;
&lt;li&gt;Create accompanying visuals&lt;/li&gt;
&lt;li&gt;Schedule across platforms&lt;/li&gt;
&lt;li&gt;Track engagement metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro Tip&lt;/strong&gt;: Use temperature 0.7-0.9 for creative content generation.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Predictive Inventory Management
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Forecast stock needs and automate reordering&lt;br&gt;
&lt;strong&gt;AI Model&lt;/strong&gt;: Custom ML model + GPT for insights&lt;br&gt;
&lt;strong&gt;Workflow Steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect sales data and trends&lt;/li&gt;
&lt;li&gt;AI predicts future demand&lt;/li&gt;
&lt;li&gt;Calculates optimal reorder points&lt;/li&gt;
&lt;li&gt;Generates purchase orders&lt;/li&gt;
&lt;li&gt;Sends supplier notifications&lt;/li&gt;
&lt;li&gt;Updates inventory systems&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4. Automated Code Review Assistant
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Review pull requests and suggest improvements&lt;br&gt;
&lt;strong&gt;AI Model&lt;/strong&gt;: GPT-4 with code understanding&lt;br&gt;
&lt;strong&gt;Workflow Steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub webhook triggers review&lt;/li&gt;
&lt;li&gt;AI analyzes code changes&lt;/li&gt;
&lt;li&gt;Checks for security vulnerabilities&lt;/li&gt;
&lt;li&gt;Suggests optimizations&lt;/li&gt;
&lt;li&gt;Posts review comments&lt;/li&gt;
&lt;li&gt;Tracks code quality metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  5. Intelligent Data Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Clean, transform, and analyze incoming data streams&lt;br&gt;
&lt;strong&gt;AI Model&lt;/strong&gt;: Multiple specialized models&lt;br&gt;
&lt;strong&gt;Workflow Steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receive raw data from multiple sources&lt;/li&gt;
&lt;li&gt;AI detects data quality issues&lt;/li&gt;
&lt;li&gt;Automatically cleans and normalizes&lt;/li&gt;
&lt;li&gt;Generates insights and anomaly alerts&lt;/li&gt;
&lt;li&gt;Updates dashboards and reports&lt;/li&gt;
&lt;li&gt;Triggers actions based on findings&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  💡 Expert Tips for AI n8n Success
&lt;/h2&gt;
&lt;h3&gt;
  
  
  🔐 Security Best Practices
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API Key Management&lt;/strong&gt;: Use n8n's credential system, never hardcode keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limiting&lt;/strong&gt;: Implement exponential backoff for API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Privacy&lt;/strong&gt;: Anonymize sensitive data before AI processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Trails&lt;/strong&gt;: Log all AI decisions for compliance&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  ⚡ Performance Optimization
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Batch Processing&lt;/strong&gt;: Group similar requests to reduce API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: Store frequent AI responses to improve speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Selection&lt;/strong&gt;: Use lighter models for simple tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel Processing&lt;/strong&gt;: Run independent AI tasks simultaneously&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  📊 Monitoring and Analytics
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Workflow Performance Tracking&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workflow_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $workflow.id }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;execution_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $execution.startTime }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_model_used&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tokens_consumed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $json.usage.total_tokens }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success_rate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $json.success ? 1 : 0 }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $json.error?.type || null }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  🚨 Common Pitfalls and How to Avoid Them
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. The "AI for Everything" Trap
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Using AI unnecessarily increases costs and complexity&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Apply the "Rule of Simple First" - use traditional logic for simple decisions&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Prompt Engineering Neglect
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Poor prompts lead to inconsistent results&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;:&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="c1"&gt;// Good Prompt Structure&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are a [ROLE]. Your task is to [SPECIFIC_TASK]. Format your response as [FORMAT]. Consider [CONSTRAINTS].&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Inadequate Error Handling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: AI failures break entire workflows&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Implement comprehensive fallback mechanisms&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="c1"&gt;// Robust Error Handling&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aiResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Fallback to rule-based logic&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;defaultClassification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;aiResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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;h3&gt;
  
  
  4. Cost Spiraling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Uncontrolled AI usage leads to massive bills&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Implement usage monitoring and budget alerts&lt;/p&gt;

&lt;h2&gt;
  
  
  🔮 The Future: What's Coming Next
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI Agents in n8n
&lt;/h3&gt;

&lt;p&gt;Expect to see autonomous AI agents that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plan multi-step workflows&lt;/li&gt;
&lt;li&gt;Self-optimize based on performance&lt;/li&gt;
&lt;li&gt;Communicate with other agents&lt;/li&gt;
&lt;li&gt;Learn from user feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Advanced Multimodal Workflows
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Video processing and analysis&lt;/li&gt;
&lt;li&gt;Voice-to-workflow activation&lt;/li&gt;
&lt;li&gt;Real-time image recognition triggers&lt;/li&gt;
&lt;li&gt;Document understanding and automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Industry-Specific AI Models
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Healthcare: HIPAA-compliant patient data processing&lt;/li&gt;
&lt;li&gt;Finance: Regulatory compliance automation&lt;/li&gt;
&lt;li&gt;Legal: Contract analysis and drafting&lt;/li&gt;
&lt;li&gt;Manufacturing: Quality control automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 Action Plan: Your Next Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Week 1: Foundation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Set up n8n instance (cloud or self-hosted)&lt;/li&gt;
&lt;li&gt;[ ] Obtain API keys for OpenAI, Claude, or Gemini&lt;/li&gt;
&lt;li&gt;[ ] Create your first simple AI workflow&lt;/li&gt;
&lt;li&gt;[ ] Test basic prompt engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 2: Build Your First Production Workflow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Identify highest-impact use case in your organization&lt;/li&gt;
&lt;li&gt;[ ] Design workflow architecture&lt;/li&gt;
&lt;li&gt;[ ] Implement core AI integration&lt;/li&gt;
&lt;li&gt;[ ] Add error handling and monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 3: Scale and Optimize
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Deploy to production environment&lt;/li&gt;
&lt;li&gt;[ ] Monitor performance and costs&lt;/li&gt;
&lt;li&gt;[ ] Gather user feedback&lt;/li&gt;
&lt;li&gt;[ ] Iterate and improve&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 4: Advanced Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Implement multiple AI model integration&lt;/li&gt;
&lt;li&gt;[ ] Add advanced analytics&lt;/li&gt;
&lt;li&gt;[ ] Create custom node extensions&lt;/li&gt;
&lt;li&gt;[ ] Plan next workflow automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📚 Essential Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Documentation and Guides
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/" rel="noopener noreferrer"&gt;n8n Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs" rel="noopener noreferrer"&gt;OpenAI API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic Claude Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/docs" rel="noopener noreferrer"&gt;Google AI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Community and Support
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;n8n Community Forum&lt;/li&gt;
&lt;li&gt;Discord channels for workflow sharing&lt;/li&gt;
&lt;li&gt;GitHub repositories with template workflows&lt;/li&gt;
&lt;li&gt;YouTube channels with tutorials&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tools and Extensions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;n8n Cloud for managed hosting&lt;/li&gt;
&lt;li&gt;Custom node development framework&lt;/li&gt;
&lt;li&gt;Workflow template marketplace&lt;/li&gt;
&lt;li&gt;AI model comparison tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏆 Conclusion: Your Competitive Advantage Awaits
&lt;/h2&gt;

&lt;p&gt;The fusion of AI and n8n isn't just about automation—it's about creating intelligent systems that amplify human capabilities. Organizations that master this combination today will lead their industries tomorrow.&lt;/p&gt;

&lt;p&gt;The workflows you build now will become the competitive moats of the future. Every manual process you automate, every decision you enhance with AI, and every workflow you optimize brings you closer to operational excellence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember&lt;/strong&gt;: The best time to start was yesterday. The second-best time is now.&lt;/p&gt;

&lt;p&gt;Start with one simple workflow, learn from it, iterate, and scale. Your future self will thank you for taking the first step today.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your biggest workflow challenge? Share in the comments below, and let's solve it together with AI n8n magic! 🪄&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: #AI #n8n #Productivity #TechTrends #UltimateGuide&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow me for more cutting-edge automation content and join 50,000+ professionals transforming their workflows with AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>n8n</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Build AI-Powered Automated Workflows with n8n: Step-by-Step Guide for 2025</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Fri, 08 Aug 2025 17:39:17 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/how-to-build-ai-powered-automated-workflows-with-n8n-step-by-step-guide-for-2025-cl9</link>
      <guid>https://dev.to/lalitkumarjangid/how-to-build-ai-powered-automated-workflows-with-n8n-step-by-step-guide-for-2025-cl9</guid>
      <description>&lt;h1&gt;
  
  
  How to Build AI-Powered Automated Workflows with n8n: Step-by-Step Guide for 2025
&lt;/h1&gt;

&lt;p&gt;🤖 &lt;strong&gt;Ready to supercharge your productivity with AI-powered automation?&lt;/strong&gt; In 2025, the convergence of artificial intelligence and workflow automation has reached a tipping point. With n8n, you can build sophisticated workflows that leverage AI to handle repetitive tasks, make intelligent decisions, and scale your operations like never before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-Powered Workflows Are Game-Changers
&lt;/h2&gt;

&lt;p&gt;Traditional automation follows rigid rules: "If this, then that." But AI-powered workflows? They learn, adapt, and make contextual decisions. Imagine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📧 Email responses that understand context and sentiment&lt;/li&gt;
&lt;li&gt;📊 Data analysis that identifies patterns automatically
&lt;/li&gt;
&lt;li&gt;🎯 Content creation that matches your brand voice&lt;/li&gt;
&lt;li&gt;🔄 Workflows that optimize themselves over time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What You'll Learn
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you'll master:&lt;/p&gt;

&lt;p&gt;✅ Setting up n8n for AI integration&lt;br&gt;&lt;br&gt;
✅ Connecting popular AI services (OpenAI, Claude, Gemini)&lt;br&gt;&lt;br&gt;
✅ Building 5 real-world AI workflows&lt;br&gt;&lt;br&gt;
✅ Debugging and optimizing performance&lt;br&gt;&lt;br&gt;
✅ Scaling for production environments  &lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Basic understanding of APIs and webhooks&lt;/li&gt;
&lt;li&gt;n8n account (free tier works fine)&lt;/li&gt;
&lt;li&gt;API keys for AI services (we'll cover getting these)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Part 1: Setting Up Your AI-Ready n8n Environment
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Create Your n8n Workspace
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign up at &lt;a href="https://n8n.io" rel="noopener noreferrer"&gt;n8n.io&lt;/a&gt;&lt;/strong&gt; if you haven't already&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose your deployment method:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;☁️ &lt;strong&gt;n8n Cloud&lt;/strong&gt; (easiest, recommended for beginners)&lt;/li&gt;
&lt;li&gt;🐳 &lt;strong&gt;Docker&lt;/strong&gt; (for local development)&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Self-hosted&lt;/strong&gt; (for advanced users)
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quick Docker setup&lt;/span&gt;
docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; n8n &lt;span class="nt"&gt;-p&lt;/span&gt; 5678:5678 n8nio/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Configure AI Service Credentials
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://platform.openai.com" rel="noopener noreferrer"&gt;platform.openai.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Generate API key&lt;/li&gt;
&lt;li&gt;In n8n: Settings → Credentials → Add OpenAI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Claude (Anthropic) Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get key from &lt;a href="https://console.anthropic.com" rel="noopener noreferrer"&gt;console.anthropic.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Add as HTTP Request credential in n8n&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Google Gemini Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable Gemini API in Google Cloud Console&lt;/li&gt;
&lt;li&gt;Create service account and download JSON key&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Part 2: 5 Powerful AI Workflow Examples
&lt;/h2&gt;
&lt;h3&gt;
  
  
  🚀 Workflow 1: Smart Email Responder
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Automatically drafts personalized email responses based on incoming message context and sentiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nodes you'll use:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gmail Trigger&lt;/li&gt;
&lt;li&gt;OpenAI Chat&lt;/li&gt;
&lt;li&gt;Gmail (Send)&lt;/li&gt;
&lt;li&gt;IF conditional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step-by-step setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gmail Trigger Node:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message.received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"filters"&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;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"needs-response"&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Chat Node:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-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;"messages"&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You are a professional email assistant. Analyze the incoming email and draft a helpful, concise response that matches the sender's tone."&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Original email: {{ $json.body }}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Sender: {{ $json.from }}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Subject: {{ $json.subject }}"&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gmail Send Node:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="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;"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;"{{ $('Gmail Trigger').item.json.from }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Re: {{ $('Gmail Trigger').item.json.subject }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.choices[0].message.content }}"&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;strong&gt;💡 Pro Tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add sentiment analysis to adjust response tone&lt;/li&gt;
&lt;li&gt;Include a human review step for important emails&lt;/li&gt;
&lt;li&gt;Use templates for common response types&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🎯 Workflow 2: Intelligent Content Moderation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Automatically moderates user-generated content across platforms using AI content analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key components:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Webhook Trigger&lt;/strong&gt; (receives content from your app)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Moderation API&lt;/strong&gt; (checks for harmful content)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom decision logic&lt;/strong&gt; (flags, approves, or rejects)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack notification&lt;/strong&gt; (alerts moderators)
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OpenAI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Moderation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;configuration&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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.content }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text-moderation-latest"&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;h3&gt;
  
  
  📊 Workflow 3: AI-Powered Data Analysis Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Automatically analyzes CSV data uploads, generates insights, and creates visualizations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow structure:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Google Drive Trigger&lt;/strong&gt; → New CSV uploaded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV Parser&lt;/strong&gt; → Extract data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude AI Analysis&lt;/strong&gt; → Generate insights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Sheets&lt;/strong&gt; → Store results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack/Email&lt;/strong&gt; → Send summary report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Claude prompt for data analysis:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze this dataset and provide:
1. Key trends and patterns
2. Statistical summaries
3. Actionable insights
4. Recommendations for next steps

Data: {{ $json.csvData }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🌐 Workflow 4: Multilingual Customer Support Router
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Detects customer inquiry language, translates if needed, and routes to appropriate support agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Components:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Language detection (Google Translate API)&lt;/li&gt;
&lt;li&gt;Sentiment analysis (OpenAI)&lt;/li&gt;
&lt;li&gt;Intent classification (custom model)&lt;/li&gt;
&lt;li&gt;Auto-translation (Google Translate)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎨 Workflow 5: Dynamic Content Generation System
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Creates personalized marketing content based on user behavior data and brand guidelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User segmentation analysis&lt;/li&gt;
&lt;li&gt;Brand voice consistency&lt;/li&gt;
&lt;li&gt;A/B testing integration&lt;/li&gt;
&lt;li&gt;Multi-channel publishing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 3: Advanced Optimization Techniques
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error Handling and Retry Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Custom error handling in Function node&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;shouldRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance Optimization
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Batch Processing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process multiple items in single API calls&lt;/li&gt;
&lt;li&gt;Use n8n's batch mode for high-volume workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Caching Strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache AI responses for similar inputs&lt;/li&gt;
&lt;li&gt;Use Redis integration for persistent caching&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rate Limiting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implement delays between API calls&lt;/li&gt;
&lt;li&gt;Use queue systems for high-traffic scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Monitoring and Analytics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key metrics to track:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workflow execution time&lt;/li&gt;
&lt;li&gt;AI API usage and costs&lt;/li&gt;
&lt;li&gt;Error rates and types&lt;/li&gt;
&lt;li&gt;Business impact metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setup monitoring with:&lt;/strong&gt;&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="c1"&gt;// Function node for logging&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;json&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="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;executionTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;$workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;h2&gt;
  
  
  Part 4: Production Deployment Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Security Considerations
&lt;/h3&gt;

&lt;p&gt;🔒 &lt;strong&gt;API Key Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use environment variables&lt;/li&gt;
&lt;li&gt;Rotate keys regularly&lt;/li&gt;
&lt;li&gt;Implement least-privilege access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Data Protection:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encrypt sensitive data in transit&lt;/li&gt;
&lt;li&gt;Implement proper authentication&lt;/li&gt;
&lt;li&gt;Regular security audits&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scaling Strategies
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Horizontal Scaling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple n8n instances&lt;/li&gt;
&lt;li&gt;Load balancing&lt;/li&gt;
&lt;li&gt;Queue-based processing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vertical Scaling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increase server resources&lt;/li&gt;
&lt;li&gt;Optimize database queries&lt;/li&gt;
&lt;li&gt;Cache frequently accessed data&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cost Optimization
&lt;/h3&gt;

&lt;p&gt;💰 &lt;strong&gt;AI API Cost Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor token usage&lt;/li&gt;
&lt;li&gt;Implement request quotas&lt;/li&gt;
&lt;li&gt;Use cost-effective models when possible&lt;/li&gt;
&lt;li&gt;Cache responses to reduce API calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example cost tracking:&lt;/strong&gt;&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="c1"&gt;// Calculate OpenAI costs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt_tokens&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;outputTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completion_tokens&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;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.00001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.00003&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;json&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="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;estimatedCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cost&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;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem: AI Responses Are Inconsistent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solutions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use system prompts for consistency&lt;/li&gt;
&lt;li&gt;Implement response validation&lt;/li&gt;
&lt;li&gt;Add temperature controls&lt;/li&gt;
&lt;li&gt;Use few-shot examples&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Problem: Workflows Are Too Slow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solutions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parallel processing where possible&lt;/li&gt;
&lt;li&gt;Optimize AI prompts for faster responses&lt;/li&gt;
&lt;li&gt;Implement caching&lt;/li&gt;
&lt;li&gt;Use faster AI models for simple tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Problem: High API Costs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solutions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implement smart caching&lt;/li&gt;
&lt;li&gt;Use smaller models for simple tasks&lt;/li&gt;
&lt;li&gt;Batch similar requests&lt;/li&gt;
&lt;li&gt;Set usage limits and alerts&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Future-Proofing Your AI Workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Emerging Trends to Watch
&lt;/h3&gt;

&lt;p&gt;🔮 &lt;strong&gt;2025 Predictions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-modal AI (text + images + audio)&lt;/li&gt;
&lt;li&gt;Smaller, more efficient models&lt;/li&gt;
&lt;li&gt;Better context understanding&lt;/li&gt;
&lt;li&gt;Reduced latency and costs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Preparing for Updates
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Modular Design:&lt;/strong&gt; Build workflows that can easily swap AI providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control:&lt;/strong&gt; Track changes and enable rollbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing Frameworks:&lt;/strong&gt; Automated testing for AI outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; Real-time performance and accuracy tracking&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion: Your AI Automation Journey Starts Now
&lt;/h2&gt;

&lt;p&gt;🎉 &lt;strong&gt;Congratulations!&lt;/strong&gt; You now have the knowledge to build sophisticated AI-powered workflows with n8n. The key to success is starting small, iterating quickly, and gradually building complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with Workflow #1&lt;/strong&gt; (Smart Email Responder)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join the n8n community&lt;/strong&gt; for support and inspiration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment with different AI models&lt;/strong&gt; to find what works best&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share your creations&lt;/strong&gt; and learn from others&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Resources for Continued Learning:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📚 &lt;a href="https://docs.n8n.io/" rel="noopener noreferrer"&gt;n8n Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🤝 &lt;a href="https://community.n8n.io/" rel="noopener noreferrer"&gt;n8n Community Forum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/c/n8nio" rel="noopener noreferrer"&gt;n8n YouTube Channel&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://platform.openai.com/docs" rel="noopener noreferrer"&gt;OpenAI API Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;What AI workflow will you build first?&lt;/strong&gt; Drop a comment below and let's start a conversation about the future of intelligent automation! 🚀&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. If this guide helped you, don't forget to ⭐ star it and share with your team. Building the future, one workflow at a time! 🤖&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>n8n</category>
    </item>
    <item>
      <title>The 25 Best AI n8n Integrations: How to Automate Your Business in 2025</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Fri, 08 Aug 2025 17:36:09 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/the-25-best-ai-n8n-integrations-how-to-automate-your-business-in-2025-1om1</link>
      <guid>https://dev.to/lalitkumarjangid/the-25-best-ai-n8n-integrations-how-to-automate-your-business-in-2025-1om1</guid>
      <description>&lt;h1&gt;
  
  
  🚀 The 25 Best AI n8n Integrations: How to Automate Your Business in 2025
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction: The Future of Business Automation is Here
&lt;/h2&gt;

&lt;p&gt;In 2025, artificial intelligence isn't just changing how we work—it's revolutionizing the very fabric of business operations. &lt;strong&gt;n8n&lt;/strong&gt;, the powerful open-source workflow automation platform, has emerged as the ultimate bridge between AI capabilities and practical business automation. &lt;/p&gt;

&lt;p&gt;Whether you're a solo entrepreneur looking to streamline your daily tasks or a growing company seeking to scale efficiently, AI-powered n8n integrations offer unprecedented opportunities to automate complex workflows, enhance productivity, and drive innovation.&lt;/p&gt;

&lt;p&gt;This comprehensive guide reveals the &lt;strong&gt;25 most powerful AI n8n integrations&lt;/strong&gt; that are transforming businesses in 2025. From customer service automation to content creation, these integrations will help you build intelligent workflows that work around the clock.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Why AI + n8n = Business Transformation
&lt;/h2&gt;

&lt;p&gt;Before diving into our top 25 integrations, let's understand why this combination is so powerful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost Efficiency&lt;/strong&gt;: Reduce operational costs by up to 60%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24/7 Operations&lt;/strong&gt;: AI never sleeps, ensuring continuous business processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Handle growing workloads without proportional staff increases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy&lt;/strong&gt;: Minimize human errors in repetitive tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Innovation&lt;/strong&gt;: Free up human talent for strategic, creative work&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔥 Top 25 AI n8n Integrations for 2025
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Customer Service &amp;amp; Support Automation&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. &lt;strong&gt;OpenAI GPT-4 + Zendesk&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Intelligent ticket routing and auto-responses&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 40% reduction in response times&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Connect GPT-4 to analyze ticket content and generate contextual responses&lt;/p&gt;

&lt;h4&gt;
  
  
  2. &lt;strong&gt;Claude AI + Intercom&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Advanced chatbot conversations with human-like understanding&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 65% first-contact resolution rate&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Integrate Claude for sophisticated customer query handling&lt;/p&gt;

&lt;h4&gt;
  
  
  3. &lt;strong&gt;Google AI + Freshdesk&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Sentiment analysis and priority scoring&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 30% improvement in customer satisfaction&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Analyze customer emotions to prioritize urgent issues&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Content Creation &amp;amp; Marketing&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  4. &lt;strong&gt;OpenAI DALL-E + Canva&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated visual content generation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 80% faster design workflows&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Generate images based on text prompts and auto-design social media posts&lt;/p&gt;

&lt;h4&gt;
  
  
  5. &lt;strong&gt;Jasper AI + WordPress&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated blog post creation and publishing&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 10x content output increase&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Create SEO-optimized content based on trending keywords&lt;/p&gt;

&lt;h4&gt;
  
  
  6. &lt;strong&gt;Copy.ai + HubSpot&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Personalized email campaign generation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 45% higher open rates&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Generate personalized email content based on lead data&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Sales &amp;amp; Lead Generation&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  7. &lt;strong&gt;Salesforce Einstein + n8n&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Predictive lead scoring and pipeline management&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 25% increase in conversion rates&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Automatically score leads and trigger follow-up actions&lt;/p&gt;

&lt;h4&gt;
  
  
  8. &lt;strong&gt;LinkedIn Sales Navigator + GPT-4&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Personalized outreach message generation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 3x response rates&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Generate personalized connection requests based on prospect profiles&lt;/p&gt;

&lt;h4&gt;
  
  
  9. &lt;strong&gt;Pipedrive + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated deal analysis and forecasting&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 20% more accurate sales predictions&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Analyze deal patterns to predict closure probability&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Data Analysis &amp;amp; Business Intelligence&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  10. &lt;strong&gt;Google Analytics + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated insights and report generation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 50% faster decision making&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Generate weekly business insights from analytics data&lt;/p&gt;

&lt;h4&gt;
  
  
  11. &lt;strong&gt;Airtable + Claude AI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Intelligent data categorization and cleanup&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 70% reduction in manual data processing&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Automatically categorize and clean imported data&lt;/p&gt;

&lt;h4&gt;
  
  
  12. &lt;strong&gt;Power BI + Azure AI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Predictive analytics and trend forecasting&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 35% better strategic planning&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Integrate AI models for advanced business forecasting&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;E-commerce &amp;amp; Inventory Management&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  13. &lt;strong&gt;Shopify + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Dynamic product descriptions and pricing optimization&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 15% increase in conversion rates&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Generate compelling product descriptions and optimize pricing&lt;/p&gt;

&lt;h4&gt;
  
  
  14. &lt;strong&gt;WooCommerce + Anthropic Claude&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Intelligent inventory forecasting&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 25% reduction in stockouts&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Predict demand patterns and automate reordering&lt;/p&gt;

&lt;h4&gt;
  
  
  15. &lt;strong&gt;Amazon Seller Central + GPT-4&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated listing optimization&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 30% improvement in search rankings&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Optimize product titles, keywords, and descriptions&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Human Resources &amp;amp; Recruitment&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  16. &lt;strong&gt;BambooHR + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated resume screening and candidate matching&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 60% faster hiring process&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Screen resumes and rank candidates based on job requirements&lt;/p&gt;

&lt;h4&gt;
  
  
  17. &lt;strong&gt;Workday + Google AI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Employee sentiment analysis and retention prediction&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 40% reduction in turnover&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Analyze employee feedback to predict retention risks&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Financial Management &amp;amp; Accounting&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  18. &lt;strong&gt;QuickBooks + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated expense categorization and fraud detection&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 50% reduction in accounting errors&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Categorize expenses and flag unusual transactions&lt;/p&gt;

&lt;h4&gt;
  
  
  19. &lt;strong&gt;Xero + Claude AI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Financial forecasting and cash flow prediction&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 30% better financial planning&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Analyze financial patterns to predict future cash flows&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Project Management &amp;amp; Productivity&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  20. &lt;strong&gt;Asana + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Intelligent task prioritization and resource allocation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 25% increase in project completion rates&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Analyze project data to optimize task assignments&lt;/p&gt;

&lt;h4&gt;
  
  
  21. &lt;strong&gt;Notion + GPT-4&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated documentation and knowledge base creation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 80% faster documentation processes&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Generate comprehensive documentation from meeting notes&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Communication &amp;amp; Collaboration&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  22. &lt;strong&gt;Slack + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Intelligent message summarization and action extraction&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 35% reduction in information overload&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Summarize long conversations and extract actionable items&lt;/p&gt;

&lt;h4&gt;
  
  
  23. &lt;strong&gt;Microsoft Teams + Azure AI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Real-time meeting transcription and insight generation&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 50% better meeting follow-up&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Transcribe meetings and generate action items automatically&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Security &amp;amp; Compliance&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  24. &lt;strong&gt;Okta + Google AI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Behavioral analysis and threat detection&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 70% faster threat response&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Monitor user behavior patterns to detect security anomalies&lt;/p&gt;

&lt;h4&gt;
  
  
  25. &lt;strong&gt;AWS Security Hub + OpenAI&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;: Automated security incident analysis and response&lt;br&gt;
&lt;strong&gt;ROI&lt;/strong&gt;: 60% reduction in security response time&lt;br&gt;
&lt;strong&gt;Setup&lt;/strong&gt;: Analyze security alerts and generate response recommendations&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Implementation Strategy: Your 90-Day Automation Roadmap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Days 1-30: Foundation Phase&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Audit current workflows and identify automation opportunities&lt;/li&gt;
&lt;li&gt;Set up n8n instance and basic AI integrations&lt;/li&gt;
&lt;li&gt;Start with 3-5 high-impact, low-complexity integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Days 31-60: Expansion Phase&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Implement additional integrations based on early wins&lt;/li&gt;
&lt;li&gt;Optimize existing workflows for better performance&lt;/li&gt;
&lt;li&gt;Train team members on new automated processes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Days 61-90: Optimization Phase&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Analyze automation performance and ROI&lt;/li&gt;
&lt;li&gt;Scale successful integrations across departments&lt;/li&gt;
&lt;li&gt;Plan advanced AI implementations for next quarter&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 Pro Tips for Maximum Success
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Start Small, Think Big&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Begin with simple integrations that deliver immediate value. Success breeds success, and early wins will build momentum for larger projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Monitor and Measure Everything&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Implement proper analytics to track the performance of your AI integrations. Key metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time saved per task&lt;/li&gt;
&lt;li&gt;Error reduction rates&lt;/li&gt;
&lt;li&gt;Customer satisfaction improvements&lt;/li&gt;
&lt;li&gt;Cost savings achieved&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Invest in Training&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ensure your team understands how to work alongside AI automation. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding when to intervene in automated processes&lt;/li&gt;
&lt;li&gt;Maintaining quality control standards&lt;/li&gt;
&lt;li&gt;Leveraging automation insights for strategic decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Plan for Scalability&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Design your integrations with growth in mind. Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API rate limits and costs&lt;/li&gt;
&lt;li&gt;Data privacy and security requirements&lt;/li&gt;
&lt;li&gt;Integration maintenance and updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Stay Updated with AI Developments&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The AI landscape evolves rapidly. Regularly review and update your integrations to leverage new capabilities and maintain competitive advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔮 The Future of AI Automation
&lt;/h2&gt;

&lt;p&gt;As we look ahead, several trends will shape the future of AI-powered business automation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal AI Integration&lt;/strong&gt;: Future integrations will seamlessly combine text, voice, image, and video processing for more comprehensive automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Industry-Specific AI Models&lt;/strong&gt;: Specialized AI models will emerge for specific industries, offering more accurate and relevant automation capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autonomous Business Processes&lt;/strong&gt;: We're moving toward fully autonomous business processes that require minimal human intervention while maintaining high quality and compliance standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictive Automation&lt;/strong&gt;: AI will not just react to events but predict and prepare for future scenarios, creating truly proactive business operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Conclusion: Your Automation Journey Starts Now
&lt;/h2&gt;

&lt;p&gt;The 25 AI n8n integrations outlined in this guide represent just the beginning of what's possible in 2025. By implementing these solutions strategically, businesses can achieve unprecedented levels of efficiency, accuracy, and innovation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember&lt;/strong&gt;: The goal isn't to replace human creativity and strategic thinking—it's to amplify it. AI automation handles the routine, the repetitive, and the predictable, freeing your team to focus on what humans do best: innovate, create, and build meaningful relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your next steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose 3-5 integrations that align with your biggest pain points&lt;/li&gt;
&lt;li&gt;Set up a pilot program to test and refine your approach&lt;/li&gt;
&lt;li&gt;Measure results and scale successful implementations&lt;/li&gt;
&lt;li&gt;Continuously evolve your automation strategy as AI capabilities advance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The future of business is automated, intelligent, and incredibly exciting. The question isn't whether you should embrace AI automation—it's how quickly you can implement it to stay ahead of the competition.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;n8n Documentation&lt;/strong&gt;: Complete setup guides and tutorials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Integration Templates&lt;/strong&gt;: Ready-to-use workflow templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Forums&lt;/strong&gt;: Connect with other automation professionals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices Guide&lt;/strong&gt;: Advanced optimization techniques&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Ready to transform your business with AI automation? Start with one integration today and watch as your productivity soars to new heights.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: #AI #n8n #Automation #Business #Productivity #2025 #WorkflowAutomation #ArtificialIntelligence #BusinessTransformation #TechTrends&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>n8n</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
