<?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: Raizan</title>
    <description>The latest articles on DEV Community by Raizan (@chasebot).</description>
    <link>https://dev.to/chasebot</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3869200%2F83f3115f-a656-4200-8cea-d88dfde8350a.png</url>
      <title>DEV Community: Raizan</title>
      <link>https://dev.to/chasebot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chasebot"/>
    <language>en</language>
    <item>
      <title>Building a Crypto Price Alert Bot with n8n and CoinGecko</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Wed, 29 Apr 2026 08:00:29 +0000</pubDate>
      <link>https://dev.to/chasebot/building-a-crypto-price-alert-bot-with-n8n-and-coingecko-52fe</link>
      <guid>https://dev.to/chasebot/building-a-crypto-price-alert-bot-with-n8n-and-coingecko-52fe</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for self-hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; if setting up a domain&lt;/li&gt;
&lt;li&gt;CoinGecko API (free tier available at coingecko.com)&lt;/li&gt;
&lt;li&gt;A messaging platform (Discord, Telegram, or email)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What You'll Need&lt;/li&gt;
&lt;li&gt;Understanding the Architecture&lt;/li&gt;
&lt;li&gt;Setting Up Your First Webhook Trigger&lt;/li&gt;
&lt;li&gt;Fetching Live Crypto Prices from CoinGecko&lt;/li&gt;
&lt;li&gt;Building the Price Alert Logic&lt;/li&gt;
&lt;li&gt;Sending Notifications&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Understanding the Architecture
&lt;/h2&gt;

&lt;p&gt;I've been building crypto automation workflows for about two years now, and I can tell you: price alert bots are deceptively simple on the surface but incredibly powerful when done right. The basic architecture is straightforward: a trigger fires on a schedule, you fetch current prices, compare them against thresholds you've set, and notify the user if conditions are met.&lt;/p&gt;

&lt;p&gt;Here's what happens behind the scenes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt;: A schedule executes every N minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data fetch&lt;/strong&gt;: Call CoinGecko API for real-time prices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic&lt;/strong&gt;: Compare prices against your alert thresholds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: Send a notification if the alert fires&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The beauty of building this in &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; is that you get a visual workflow editor, built-in nodes for HTTP requests, and native integrations with Discord, Telegram, and email—no custom code required (though you can add it if you want).&lt;/p&gt;

&lt;p&gt;If you're deploying to production, you might consider &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;self-hosting n8n on a Hetzner VPS&lt;/a&gt; or learning how to &lt;a href="https://dev.to/blog/deploy-n8n-docker-vps/"&gt;deploy n8n with Docker on any VPS&lt;/a&gt;. That gives you full control over execution and cost savings at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Your First Webhook Trigger
&lt;/h2&gt;

&lt;p&gt;Log into &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; and create a new workflow. The first node will be your trigger—I'm using a Schedule node set to run every 5 minutes.&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;+&lt;/strong&gt; icon on your canvas and search for "Schedule". Add the node and configure it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trigger Type: Every X minutes
Interval: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means your workflow will execute every 5 minutes automatically. You could also use a Webhook trigger if you want external systems to kick off the workflow, but for a price alert bot, a schedule makes more sense—you want continuous monitoring, not event-driven checks.&lt;/p&gt;

&lt;p&gt;Now connect your Schedule node to your next step. We're going to fetch live price data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fetching Live Crypto Prices from CoinGecko
&lt;/h2&gt;

&lt;p&gt;The CoinGecko API is free and doesn't require authentication for basic endpoints. This is crucial because it means zero setup friction. Add an HTTP Request node right after your Schedule trigger.&lt;/p&gt;

&lt;p&gt;Configure the HTTP node like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node: HTTP Request&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
&lt;span class="na"&gt;URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.coingecko.com/api/v3/simple/price&lt;/span&gt;
&lt;span class="na"&gt;Authentication&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;None&lt;/span&gt;
&lt;span class="na"&gt;Query Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bitcoin,ethereum,cardano&lt;/span&gt;
  &lt;span class="na"&gt;vs_currencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;usd&lt;/span&gt;
  &lt;span class="na"&gt;include_market_cap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;include_24hr_vol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;include_24hr_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response will look like:&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;"bitcoin"&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;"usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_market_cap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;831000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_24h_vol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_24h_change"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ethereum"&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;"usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_market_cap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;270000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_24h_vol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_24h_change"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.8&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;"cardano"&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;"usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_market_cap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;33000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_24h_vol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;450000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_24h_change"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.5&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single API call gives you everything you need. You can request as many coins as you want in the &lt;code&gt;ids&lt;/code&gt; parameter—just separate them with commas. For a comprehensive list of supported coins, check the &lt;a href="https://dev.to/blog/best-free-apis-automation/"&gt;best free APIs for building automation workflows in 2026&lt;/a&gt; guide, which includes CoinGecko benchmarks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Building the Price Alert Logic
&lt;/h2&gt;

&lt;p&gt;Now we need to add logic. You'll want to store your alert thresholds somewhere—for this example, I'm storing them in a Function node as JavaScript, but in production, you'd use a database or external config file.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Function&lt;/strong&gt; node after your HTTP Request node:&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;priceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alerts&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;coin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bitcoin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;below&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;coin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ethereum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;below&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;coin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cardano&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;below&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;triggeredAlerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;alerts&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;currentPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;priceData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;usd&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;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;below&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;triggeredAlerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;priceData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;usd_24h_change&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;above&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;triggeredAlerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;priceData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;usd_24h_change&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;triggeredAlerts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function compares each coin's current price against your thresholds. It supports both "above" and "below" directions, so you can alert on pumps or dumps. The output is an array of triggered alerts—empty if no alerts fire.&lt;/p&gt;

&lt;p&gt;Next, add an &lt;strong&gt;IF&lt;/strong&gt; node to check if any alerts were triggered:&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;Condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triggeredAlerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this evaluates to true, proceed to your notification nodes. If false, the workflow ends silently—no spam.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sending Notifications
&lt;/h2&gt;

&lt;p&gt;Connect your IF node to a Discord node (or Telegram/email—your choice). I'll show Discord because it's the most popular for crypto communities.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Discord&lt;/strong&gt; node and configure it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node: Discord&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;Authentication: Select your Discord webhook
Message Type: Text
Text:
🚨 CRYPTO PRICE ALERT 🚨

&lt;span class="k"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nv"&gt;coin&lt;/span&gt; &lt;span class="k"&gt;}}&lt;/span&gt; just hit your target!
Current Price: $&lt;span class="k"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nv"&gt;currentPrice&lt;/span&gt; &lt;span class="k"&gt;}}&lt;/span&gt;
Target Price: $&lt;span class="k"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nv"&gt;targetPrice&lt;/span&gt; &lt;span class="k"&gt;}}&lt;/span&gt;
24h Change: &lt;span class="k"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nv"&gt;change24h&lt;/span&gt; &lt;span class="k"&gt;}}&lt;/span&gt;%

Action now or miss the move! 📈
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$json[0]&lt;/code&gt; syntax references data from the previous node. For multiple alerts in one message, you'd loop through the array. Here's a more advanced version using a Code node to format multiple alerts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node: Function (for formatting)&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🚨 **CRYPTO PRICE ALERTS** 🚨&lt;/span&gt;&lt;span class="se"&gt;\n\n&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;const&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&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;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Current: $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Target: $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetPrice&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`24h Change: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check your exchange now! ⚡&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;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then pass &lt;code&gt;{{ $json.message }}&lt;/code&gt; to your Discord node's Text field.&lt;/p&gt;

&lt;p&gt;If you want text-to-speech notifications on mobile, consider piping alerts through &lt;a href="https://dev.to/blog/edge-tts-free-guide/"&gt;Edge TTS: The Free Text-to-Speech Engine&lt;/a&gt;, which integrates beautifully with n8n for voice alerts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advanced: Multi-Exchange Price Comparison
&lt;/h2&gt;

&lt;p&gt;If you want to level up, fetch prices from multiple exchanges and alert when spreads widen. Add a second HTTP Request node that calls a different endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
&lt;span class="na"&gt;URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.coingecko.com/api/v3/simple/price&lt;/span&gt;
&lt;span class="na"&gt;Query Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bitcoin,ethereum&lt;/span&gt;
  &lt;span class="na"&gt;vs_currencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;usd&lt;/span&gt;
  &lt;span class="na"&gt;include_exchanges&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use a Function node to calculate spreads:&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;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analysis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bitcoin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bitcoin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;volatility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bitcoin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usd_24h_change&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;ethereum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethereum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;volatility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethereum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usd_24h_change&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;analysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bitcoin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volatility&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&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;alert&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bitcoin is volatile (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bitcoin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volatility&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%)`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;alert&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This triggers alerts on high volatility, not just price thresholds—much smarter for active traders.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error Handling &amp;amp; Rate Limiting
&lt;/h2&gt;

&lt;p&gt;CoinGecko's free API allows 10-50 calls per minute depending on your region. With a 5-minute schedule and a single endpoint, you're safe. But add error handling anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&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="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;bitcoin&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;API returned incomplete data&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;response&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;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;error&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="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="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;Then add an IF node that checks for &lt;code&gt;error&lt;/code&gt; in the response and sends you a Slack message if something breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Ready to build? Here's your action plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up for &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; (free tier covers most use cases)&lt;/li&gt;
&lt;li&gt;Create a new workflow&lt;/li&gt;
&lt;li&gt;Add Schedule → HTTP Request → Function → IF → Discord/Telegram node&lt;/li&gt;
&lt;li&gt;Test with manual trigger (click the play button)&lt;/li&gt;
&lt;li&gt;Adjust alert thresholds based on your trading strategy&lt;/li&gt;
&lt;li&gt;Turn on Active toggle to run your bot 24/7&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're planning to run this bot constantly without cloud costs, explore &lt;a href="https://dev.to/blog/deploy-n8n-docker-vps/"&gt;deploying n8n with Docker on a Hetzner VPS&lt;/a&gt;—you'll pay ~$3-5/month instead of n8n Cloud's subscription.&lt;/p&gt;




&lt;h3&gt;
  
  
  Outsource Your Automation
&lt;/h3&gt;

&lt;p&gt;Don't have time to build this yourself? I specialize in production-ready n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hire me on Fiverr&lt;/a&gt;—mention &lt;strong&gt;SYS3-DEVTO&lt;/strong&gt; for priority handling and 20% off. Or reach out directly at &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;chasebot.online&lt;/a&gt; and let's discuss your specific needs.&lt;/p&gt;

&lt;p&gt;I've built crypto bots that handle multi-exchange arbitrage, Discord raid alerts, and even automated position management. Whatever you're trying to automate, I can make it work.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Save your workflow&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/price-alert-bot-n8n-coingecko/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>crypto</category>
      <category>bot</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Use HuggingFace Inference API for Free Image Generation</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Tue, 28 Apr 2026 08:00:29 +0000</pubDate>
      <link>https://dev.to/chasebot/how-to-use-huggingface-inference-api-for-free-image-generation-1hkh</link>
      <guid>https://dev.to/chasebot/how-to-use-huggingface-inference-api-for-free-image-generation-1hkh</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; if domain needed&lt;/li&gt;
&lt;li&gt;A free HuggingFace account (huggingface.co)&lt;/li&gt;
&lt;li&gt;Basic understanding of API calls and JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Understanding HuggingFace Inference API&lt;/li&gt;
&lt;li&gt;Setting Up Your API Key&lt;/li&gt;
&lt;li&gt;Making Your First Free Image Generation Request&lt;/li&gt;
&lt;li&gt;Integrating with n8n for Automated Workflows&lt;/li&gt;
&lt;li&gt;Handling Rate Limits and Optimization&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding HuggingFace Inference API
&lt;/h2&gt;

&lt;p&gt;I've been working with AI model APIs for the past three years, and HuggingFace's approach is genuinely refreshing. Their Inference API lets you hit thousands of pre-trained models—including cutting-edge image generation models like Stable Diffusion—completely free. The catch? You get rate limits, but for personal projects and testing, it's more than enough.&lt;/p&gt;

&lt;p&gt;HuggingFace hosts model inference behind their API servers. You send a prompt, they run it on their GPU infrastructure, and return the generated image. No setup, no hardware needed on your end. The free tier covers text-to-image, image-to-image, image classification, and dozens of other tasks.&lt;/p&gt;

&lt;p&gt;The models you're accessing are open-source, maintained by the community. You're not locked into proprietary offerings like DALL-E. That means you can iterate, test, and deploy without vendor anxiety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your API Key
&lt;/h2&gt;

&lt;p&gt;First, head to HuggingFace and create a free account if you don't have one. Once you're logged in, navigate to your Settings page (click your profile icon in the top right).&lt;/p&gt;

&lt;p&gt;Look for "Access Tokens" in the left sidebar. Click "Create new token" and give it a name—something like "Image Generation Workflow." Set the token type to "read" (you don't need write access for inference). Copy the token immediately and store it somewhere safe. You won't see it again.&lt;/p&gt;

&lt;p&gt;Now you have your authentication. This token is what authorizes your API calls to HuggingFace's servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Your First Free Image Generation Request
&lt;/h2&gt;

&lt;p&gt;Let me show you exactly how to hit the API from the command line. This is the foundation for everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-3.5-large &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_TOKEN_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": "A serene mountain landscape at sunset with golden light reflecting off a calm lake"
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; generated_image.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_API_TOKEN_HERE&lt;/code&gt; with the token you just created. This command sends a prompt to Stable Diffusion 3.5 Large and downloads the resulting image as PNG.&lt;/p&gt;

&lt;p&gt;The first request takes 10-15 seconds because the model needs to load. Subsequent requests are faster. You'll get raw image binary back, which the &lt;code&gt;--output&lt;/code&gt; flag saves as a file.&lt;/p&gt;

&lt;p&gt;Here's a Python example if you prefer working in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;HF_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_TOKEN_HERE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stabilityai/stable-diffusion-3.5-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api-inference.huggingface.co/models/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL_ID&lt;/span&gt;&lt;span class="si"&gt;}&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;HF_API_TOKEN&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;payload&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;inputs&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;A cyberpunk city street at night with neon signs and flying cars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&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="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&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;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated_image.png&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;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="n"&gt;content&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Image saved successfully&lt;/span&gt;&lt;span class="sh"&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="nf"&gt;print&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;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&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="sh"&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;p&gt;This script does the exact same thing but lets you handle the response programmatically. You can check for errors, log responses, and chain it into larger workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating with n8n for Automated Workflows
&lt;/h2&gt;

&lt;p&gt;Now here's where it gets interesting. &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; lets you build no-code automation that can generate images on demand. Instead of running curl commands manually, you set up a workflow that generates images based on webhooks, database triggers, or scheduled intervals.&lt;/p&gt;

&lt;p&gt;Create a new workflow in n8n and add an HTTP Request node. Configure it like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP Request Node Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-3.5-large&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Authorization: Bearer YOUR_API_TOKEN_HERE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Type: application/json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In the &lt;strong&gt;Body&lt;/strong&gt; section, set it to JSON and add:&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;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $node.previous_node.json.prompt }}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This assumes you have a previous node passing a &lt;code&gt;prompt&lt;/code&gt; field. If you're triggering this with a webhook, your webhook node would receive JSON like:&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;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A minimalist Scandinavian bedroom with soft morning light"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{{ $node.previous_node.json.prompt }}&lt;/code&gt; syntax pulls that value and inserts it into the API request.&lt;/p&gt;

&lt;p&gt;After the HTTP Request node, add a &lt;strong&gt;Binary File&lt;/strong&gt; node set to "Write Binary File" to save the image:&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;"fileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $execution.id }}_{{ Date.now() }}.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"directoryPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/tmp/generated_images"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a unique filename for each generated image using the execution ID and timestamp.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To trigger this workflow from external tools, add a Webhook trigger node at the start. Configure it to POST and copy the webhook URL. Now you can send image generation requests from any application.&lt;/p&gt;

&lt;p&gt;Here's a complete request you could send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://YOUR_N8N_WEBHOOK_URL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "prompt": "An underwater coral reef with bioluminescent creatures"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your n8n workflow receives the prompt, hits HuggingFace, gets the image, saves it, and returns a success response.&lt;/p&gt;

&lt;p&gt;If you're comparing automation platforms, you should know that &lt;a href="https://dev.to/blog/n8n-vs-make-vs-zapier-2026/"&gt;n8n vs Make vs Zapier: Honest Comparison for 2026&lt;/a&gt; shows n8n's HTTP nodes are more flexible for API work like this. Make.com requires API extensions for binary handling, while n8n handles it natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Rate Limits and Optimization
&lt;/h2&gt;

&lt;p&gt;HuggingFace's free tier doesn't publish exact rate limits, but I've consistently hit around 5 requests per second without throttling. Beyond that, you'll get 503 Service Unavailable responses.&lt;/p&gt;

&lt;p&gt;Here's how to handle rate limiting gracefully in n8n:&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Wait&lt;/strong&gt; node after your HTTP Request node with this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"fixed_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"waitDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds a 200ms delay between requests, keeping you well under the 5 RPS limit. If you're generating multiple images in a loop, this is crucial.&lt;/p&gt;

&lt;p&gt;For production workflows, add retry logic. In the HTTP Request node, set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry on fail:&lt;/strong&gt; Enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max retries:&lt;/strong&gt; 3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry delay:&lt;/strong&gt; 1000ms (1 second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This handles temporary API hiccups without breaking your workflow.&lt;/p&gt;

&lt;p&gt;If you need multiple images or advanced processing, consider piping generated images through &lt;a href="https://dev.to/blog/ffmpeg-content-creators-cheatsheet/"&gt;ffmpeg for Content Creators: Essential Commands Cheat Sheet&lt;/a&gt; to resize, compress, or batch-process them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model Selection Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Different models have different speed/quality tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stabilityai/stable-diffusion-3.5-large&lt;/strong&gt;: Best quality, slower, great for high-detail prompts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stabilityai/stable-diffusion-xl-base-1.0&lt;/strong&gt;: Faster, excellent quality, best overall balance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;black-forest-labs/FLUX.1-schnell&lt;/strong&gt;: Fastest inference, sharp results, smaller memory footprint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use FLUX.1-schnell for rapid prototyping and SD 3.5 Large when quality matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Set up your HuggingFace API key today, test a single curl request, then move it into &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt;. Start with a webhook-triggered workflow that generates one image per request. Scale from there.&lt;/p&gt;

&lt;p&gt;If you're building lead capture pipelines alongside your image generation, check out &lt;a href="https://dev.to/blog/lead-capture-n8n-typeform/"&gt;Building a Lead Capture System with n8n and Typeform&lt;/a&gt; to capture requests and metadata alongside your generated images.&lt;/p&gt;

&lt;p&gt;For self-hosting, use &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; and install n8n via Docker. You can store generated images in a local directory or push them to cloud storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outsource Your Automation
&lt;/h2&gt;

&lt;p&gt;Don't have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hire me on Fiverr&lt;/a&gt; — mention &lt;strong&gt;SYS3-DEVTO&lt;/strong&gt; for priority. Or DM at &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;chasebot.online&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/huggingface-inference-api-free/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>huggingface</category>
      <category>imagegen</category>
      <category>python</category>
    </item>
    <item>
      <title>pm2 Process Manager: Keep Your Node.js Bots Running Forever</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Mon, 27 Apr 2026 08:00:27 +0000</pubDate>
      <link>https://dev.to/chasebot/pm2-process-manager-keep-your-nodejs-bots-running-forever-5g01</link>
      <guid>https://dev.to/chasebot/pm2-process-manager-keep-your-nodejs-bots-running-forever-5g01</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n (for orchestrating automated workflows)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for hosting your Node.js processes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/do?utm_source=system3_devto" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; as an alternative hosting provider&lt;/li&gt;
&lt;li&gt;Node.js 18+ installed locally and on your server&lt;/li&gt;
&lt;li&gt;SSH access to your VPS&lt;/li&gt;
&lt;li&gt;PM2 (we'll install this together)&lt;/li&gt;
&lt;li&gt;A text editor (VS Code recommended)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why PM2 Matters for Bot Automation&lt;/li&gt;
&lt;li&gt;Installation &amp;amp; Initial Setup&lt;/li&gt;
&lt;li&gt;Running Your First Process&lt;/li&gt;
&lt;li&gt;Clustering &amp;amp; Load Balancing&lt;/li&gt;
&lt;li&gt;Monitoring &amp;amp; Logs in Real-Time&lt;/li&gt;
&lt;li&gt;Auto-Restart on Server Reboot&lt;/li&gt;
&lt;li&gt;Integration with n8n Webhooks&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why PM2 Matters for Bot Automation
&lt;/h2&gt;

&lt;p&gt;I learned this the hard way. Three years ago, I deployed a Node.js bot to a VPS—no process manager. It crashed at 2 AM on a Sunday, sat dead for six hours, and tanked the entire automation pipeline. That was the day I discovered PM2.&lt;/p&gt;

&lt;p&gt;If you're running Node.js applications in production—whether it's a webhook listener for &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; automation, a Telegram bot, a Discord integration, or a custom API server—you need process management. PM2 is the industry standard because it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Restarts crashed processes automatically&lt;/strong&gt; (no manual intervention)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clusters your app across CPU cores&lt;/strong&gt; (load balancing built-in)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manages logs&lt;/strong&gt; (no more digging through syslog)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survives server reboots&lt;/strong&gt; (auto-start on startup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitors memory and CPU&lt;/strong&gt; (alerts when things go wrong)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles zero-downtime deployments&lt;/strong&gt; (reload without dropping connections)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you're building automation workflows that depend on external services, uptime isn't optional. Unlike managed platforms, a VPS doesn't come with built-in process monitoring. PM2 fills that gap for under $5/month in hosting costs (plus zero subscription fees).&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation &amp;amp; Initial Setup
&lt;/h2&gt;

&lt;p&gt;SSH into your server and install PM2 globally:&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="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see version 5.x or higher. Next, create a directory for your bot project:&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/projects/automation-bot
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/automation-bot
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Express and Axios as dependencies (we'll use these for webhook handling):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express axios dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create your first &lt;code&gt;.env&lt;/code&gt; file to store configuration:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
PORT=3001
NODE_ENV=production
N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/automation
LOG_LEVEL=info
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running Your First Process
&lt;/h2&gt;

&lt;p&gt;Let me show you a real example. This is a simple Express server that listens for webhooks and triggers actions. Create &lt;code&gt;app.js&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&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;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;PORT&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;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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;// Health check endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&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="s1"&gt;/health&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;req&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="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="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Webhook endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook/event&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;event_type&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="nx"&gt;req&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;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="s2"&gt;`[&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="s2"&gt;] Received event: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event_type&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="c1"&gt;// Send to n8n webhook&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="nx"&gt;axios&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="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;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event_type&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="na"&gt;received_at&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;server_hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;()&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="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="na"&gt;success&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Event processed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;n8n_response&lt;/span&gt;&lt;span class="p"&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;status&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;error&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[ERROR] &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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;success&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="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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Error handling middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&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="nx"&gt;req&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;next&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unhandled error:&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal server error&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&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="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="s2"&gt;`[&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="s2"&gt;] Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&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="s2"&gt;`Health check: http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/health`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Graceful shutdown&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&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="o"&gt;=&amp;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM received, shutting down gracefully&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;Now start this with PM2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start app.js &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"webhook-bot"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────┬──────────────┬─────────────┬──────┬────────┬──────────┐
│ id  │ name         │ namespace   │ mode │ status │ restart  │
├─────┼──────────────┼─────────────┼──────┼────────┼──────────┤
│ 0   │ webhook-bot  │ default     │ fork │ online │ 0        │
└─────┴──────────────┴─────────────┴──────┴────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it:&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 http://localhost:3001/webhook/event &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"event_type":"test","data":{"message":"hello"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Clustering &amp;amp; Load Balancing
&lt;/h2&gt;

&lt;p&gt;Here's where PM2 gets powerful. Instead of running a single process, you can spawn one worker per CPU core. This means if your server has 4 cores, PM2 will automatically load-balance across 4 instances of your app.&lt;/p&gt;

&lt;p&gt;Create an &lt;code&gt;ecosystem.config.js&lt;/code&gt; file in your project root:&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&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;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;webhook-bot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3001&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;merge_logs&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;autorestart&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;watch&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;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;500M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./logs/err.log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;out_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./logs/out.log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;log_date_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY-MM-DD HH:mm:ss Z&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;p&gt;Stop the current process and restart using this config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 stop webhook-bot
pm2 start ecosystem.config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you'll see multiple instances (one per core):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────┬──────────────┬─────────────┬──────────┬────────┬──────────┐
│ id  │ name         │ namespace   │ mode     │ status │ restart  │
├─────┼──────────────┼─────────────┼──────────┼────────┼──────────┤
│ 0   │ webhook-bot  │ default     │ cluster  │ online │ 0        │
│ 1   │ webhook-bot  │ default     │ cluster  │ online │ 0        │
│ 2   │ webhook-bot  │ default     │ cluster  │ online │ 0        │
│ 3   │ webhook-bot  │ default     │ cluster  │ online │ 0        │
└─────┴──────────────┴─────────────┴──────────┴──────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is key for production. If one instance crashes, PM2 auto-restarts it while the others keep handling requests. When you're comparing automation platforms—whether it's &lt;a href="https://blog/n8n-vs-make-vs-zapier-2026/" rel="noopener noreferrer"&gt;n8n vs Make vs Zapier&lt;/a&gt;—self-hosted solutions like n8n running on your own VPS require this kind of reliability layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring &amp;amp; Logs in Real-Time
&lt;/h2&gt;

&lt;p&gt;Watch all your processes in a dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 monit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows CPU, memory, requests per minute, and restart counts. Press &lt;code&gt;q&lt;/code&gt; to exit.&lt;/p&gt;

&lt;p&gt;View logs for a specific app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 logs webhook-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or tail the last 100 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 logs webhook-bot &lt;span class="nt"&gt;--lines&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a broader view of what's happening, save all logs to files. The &lt;code&gt;ecosystem.config.js&lt;/code&gt; I provided earlier already does this. Check your logs:&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="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/projects/automation-bot/logs/out.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also rotate logs automatically. Install the PM2 logrotate module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 &lt;span class="nb"&gt;install &lt;/span&gt;pm2-logrotate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure it to keep logs under 10MB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 &lt;span class="nb"&gt;set &lt;/span&gt;pm2-logrotate:max_size 10M
pm2 &lt;span class="nb"&gt;set &lt;/span&gt;pm2-logrotate:retain 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps your disk clean—critical when you're running high-volume webhook receivers on a budget VPS like &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auto-Restart on Server Reboot
&lt;/h2&gt;

&lt;p&gt;If your VPS restarts (whether planned or due to a crash), PM2 needs to resurrect your processes. We do this by creating a startup script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command outputs a line you need to run. It looks like:&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="nb"&gt;sudo env &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd &lt;span class="nt"&gt;-u&lt;/span&gt; ubuntu &lt;span class="nt"&gt;--hp&lt;/span&gt; /home/ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy and run that exact line. Then save your current process list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now test it. Reboot your server:&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="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait 30 seconds, SSH back in, and check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your processes should be running. If they're not, check the systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status pm2-ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Integration with n8n Webhooks
&lt;/h2&gt;

&lt;p&gt;This is where it gets practical. When you're running a self-hosted &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; instance, you often need a separate Node.js service to handle incoming webhooks and trigger workflows. PM2 keeps that service alive.&lt;/p&gt;

&lt;p&gt;Here's a real workflow: your PM2-managed bot listens on port 3001, receives events, and hits your n8n webhook to trigger an automation.&lt;/p&gt;

&lt;p&gt;Modify your &lt;code&gt;.env&lt;/code&gt;:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
PORT=3001
NODE_ENV=production
N8N_WEBHOOK_URL=https://your-n8n-domain.com/webhook/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/pm2-process-manager-guide/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pm2</category>
      <category>node</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Automating Social Media Posts with n8n: Complete Guide</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 08:00:31 +0000</pubDate>
      <link>https://dev.to/chasebot/automating-social-media-posts-with-n8n-complete-guide-1dgo</link>
      <guid>https://dev.to/chasebot/automating-social-media-posts-with-n8n-complete-guide-1dgo</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for self-hosted deployments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; if you're setting up a custom domain&lt;/li&gt;
&lt;li&gt;Social media platform API credentials (Twitter/X, Instagram, Facebook, LinkedIn, TikTok)&lt;/li&gt;
&lt;li&gt;Basic understanding of REST APIs and JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Automate Social Media Posts?&lt;/li&gt;
&lt;li&gt;Setting Up Your n8n Instance&lt;/li&gt;
&lt;li&gt;Connecting Your First Social Platform&lt;/li&gt;
&lt;li&gt;Building a Multi-Platform Workflow&lt;/li&gt;
&lt;li&gt;Scheduling Posts with Cron Triggers&lt;/li&gt;
&lt;li&gt;Advanced: Content Batching and Analytics&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Automate Social Media Posts?
&lt;/h2&gt;

&lt;p&gt;I've been managing social media for clients for years, and I can tell you: manually posting to six different platforms every single day is a guaranteed way to burn out. The repetition is soul-crushing. You write something once, then copy-paste it five more times with slight tweaks for platform best practices. It's mindless work—exactly what automation exists to solve.&lt;/p&gt;

&lt;p&gt;When you automate social media posting, you free up hours every week. You can batch-create content on Monday, set it to post across the entire month, and actually focus on strategy. You can also maintain consistency (posting at optimal times in different timezones), reduce human error, and scale your reach without hiring a full social media team.&lt;/p&gt;

&lt;p&gt;n8n makes this possible because it's flexible. Unlike many social media management tools that lock you into their limited feature set and charge per platform, n8n lets you connect &lt;em&gt;any&lt;/em&gt; platform's API directly, chain complex logic, and build custom workflows tailored to your exact needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your n8n Instance
&lt;/h2&gt;

&lt;p&gt;You've got two paths: cloud or self-hosted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud is fastest:&lt;/strong&gt; Sign up at &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt;, get a free tier with up to 1,000 executions/month, and start immediately. If you hit limits, you can upgrade or move to self-hosted without rewriting your workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted gives you full control:&lt;/strong&gt; Deploy on a &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; (both around $5-10/month), then use Docker to spin up n8n. You'll want to set up &lt;a href="https://dev.to/blog/nginx-reverse-proxy-ssl-setup/"&gt;an Nginx reverse proxy with SSL from Let's Encrypt&lt;/a&gt; so your instance runs securely under a custom domain.&lt;/p&gt;

&lt;p&gt;For this guide, I'll assume you're using n8n Cloud, but the workflows are identical whether you're cloud or self-hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Your First Social Platform
&lt;/h2&gt;

&lt;p&gt;Let's start with Twitter/X because its API is straightforward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Get Your Twitter/X API Credentials&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://developer.twitter.com/en/portal/dashboard" rel="noopener noreferrer"&gt;the Twitter Developer Portal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create a new app (or use an existing one)&lt;/li&gt;
&lt;li&gt;Generate API keys and access tokens under "Keys and tokens"&lt;/li&gt;
&lt;li&gt;Copy:

&lt;ul&gt;
&lt;li&gt;API Key (Consumer Key)&lt;/li&gt;
&lt;li&gt;API Secret (Consumer Secret)&lt;/li&gt;
&lt;li&gt;Access Token&lt;/li&gt;
&lt;li&gt;Access Token Secret&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create Your n8n Workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Log in to &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt;. Click "New workflow" and name it "Social Media Poster."&lt;/p&gt;

&lt;p&gt;Start with a &lt;strong&gt;Trigger&lt;/strong&gt; node. Click the "+" to add a node, then search for "Manual Trigger" and add it. This lets you test the workflow by hand before scheduling it.&lt;/p&gt;

&lt;p&gt;Now add a &lt;strong&gt;Twitter Node&lt;/strong&gt;. Click "+" again, search for "Twitter," and select it. A credential dialog will pop up.&lt;/p&gt;

&lt;p&gt;Fill in your Twitter credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Key&lt;/li&gt;
&lt;li&gt;API Secret&lt;/li&gt;
&lt;li&gt;Access Token&lt;/li&gt;
&lt;li&gt;Access Token Secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave the API Version as "v2" (the current standard).&lt;/p&gt;

&lt;p&gt;Click "Create new credential" and save.&lt;/p&gt;

&lt;p&gt;In the Twitter node itself, configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Post a Tweet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text:&lt;/strong&gt; Enter your test post, e.g., "Testing automation with n8n! 🚀"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connect the Manual Trigger to the Twitter node. Click "Execute Workflow" at the top right.&lt;/p&gt;

&lt;p&gt;If everything's set up correctly, you'll see a green checkmark and your tweet will post live to your account.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Building a Multi-Platform Workflow
&lt;/h2&gt;

&lt;p&gt;Now let's post to multiple platforms at once. I'll add Instagram, LinkedIn, and TikTok to the same workflow.&lt;/p&gt;

&lt;p&gt;First, gather credentials for each platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instagram:&lt;/strong&gt; Create a Facebook app, generate a Page Access Token, and use the Instagram Graph API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; Register an app, request the &lt;code&gt;Share on behalf of a user&lt;/code&gt; permission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TikTok:&lt;/strong&gt; Create a developer account, get your Access Token from the TikTok for Business API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Create a unified workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start with a &lt;strong&gt;Manual Trigger&lt;/strong&gt; node (same as before). Then add a &lt;strong&gt;Set&lt;/strong&gt; node to store your post content in a variable so you only write it once.&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;"postText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Excited to announce our new product feature! 🎉 Now available to all users."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hashtags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#automation #n8n #workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheduledFor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-15T14:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click the Set node, then "Add Data" and paste that JSON. This becomes &lt;code&gt;$json&lt;/code&gt; for downstream nodes.&lt;/p&gt;

&lt;p&gt;Now add your first social platform node. Right-click and duplicate it multiple times—one for each platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Twitter Node Config:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Post a Tweet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text:&lt;/strong&gt; &lt;code&gt;{{ $json.postText + " " + $json.hashtags }}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn Node Config:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Create Post&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text:&lt;/strong&gt; &lt;code&gt;{{ $json.postText + " " + $json.hashtags }}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share Publicly:&lt;/strong&gt; True&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Instagram Node Config:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Create Media&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image URL:&lt;/strong&gt; Point to an image (we'll use a placeholder for now: &lt;code&gt;https://via.placeholder.com/1080x1080&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caption:&lt;/strong&gt; &lt;code&gt;{{ $json.postText + " " + $json.hashtags }}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TikTok Node Config:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Create Post&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video URL:&lt;/strong&gt; Point to a video source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description:&lt;/strong&gt; &lt;code&gt;{{ $json.postText }}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connect all nodes &lt;strong&gt;in parallel&lt;/strong&gt; (not in sequence). Right-click the Set node, choose "Duplicate," and connect each social node to a separate duplicate of the Set node. This way, if one platform fails, the others still execute.&lt;/p&gt;

&lt;p&gt;Click "Execute Workflow." You should see posts appear across all four platforms simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling Posts with Cron Triggers
&lt;/h2&gt;

&lt;p&gt;Manually clicking "Execute" every time you want to post defeats the purpose. Let's use a &lt;strong&gt;Cron&lt;/strong&gt; trigger to automate posting on a schedule.&lt;/p&gt;

&lt;p&gt;Delete the Manual Trigger node. Click "+" and search for "Cron." Add it.&lt;/p&gt;

&lt;p&gt;Configure the Cron node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger Times:&lt;/strong&gt; Select "Every day"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; Set to 9:00 AM (or whenever you want to post)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your workflow now posts automatically every single day at 9 AM.&lt;/p&gt;

&lt;p&gt;But there's a problem: you're posting the &lt;em&gt;same content&lt;/em&gt; every day. Let's fix that with a &lt;strong&gt;Google Sheets&lt;/strong&gt; connection to store different posts for each day.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Google Sheets&lt;/strong&gt; node before your social platform nodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Read rows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spreadsheet:&lt;/strong&gt; Create a Google Sheet with columns: Date, Post Text, Hashtags, Image URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range:&lt;/strong&gt; A1:D31 (covers a month of posts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Google Sheets node outputs rows as an array. Add an &lt;strong&gt;Item Lists&lt;/strong&gt; node to extract just today's row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation:&lt;/strong&gt; Limit items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max items:&lt;/strong&gt; 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offset:&lt;/strong&gt; Current day of month (use &lt;code&gt;{{ moment().date() - 1 }}&lt;/code&gt; to zero-index)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now your Set node reads from the Google Sheet row:&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;"postText"&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[0].fields['Post Text'] }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hashtags"&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[0].fields.Hashtags }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imageUrl"&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[0].fields['Image URL'] }}"&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;Connect that to your social media nodes. Every day at 9 AM, n8n reads today's row from your spreadsheet and posts it across all platforms.&lt;/p&gt;

&lt;p&gt;You can also schedule different post times for different platforms by adding &lt;strong&gt;Delay&lt;/strong&gt; nodes:&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;"delay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That's 5 minutes in milliseconds.) This staggers posts so Twitter posts at 9 AM, Instagram at 9:05 AM, etc., which looks more natural and avoids algorithm penalties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Content Batching and Analytics
&lt;/h2&gt;

&lt;p&gt;Now let's level up. I recommend combining this with &lt;a href="https://dev.to/blog/lead-capture-n8n-typeform/"&gt;a lead capture system using Typeform&lt;/a&gt; to gather post ideas from your audience directly. Then you can auto-generate content based on what people actually want to see.&lt;/p&gt;

&lt;p&gt;You can also add analytics by connecting a &lt;strong&gt;Webhook&lt;/strong&gt; to a Google Sheet or database. After posting, grab the post ID from each platform, then periodically query their APIs for engagement metrics (likes, shares, comments).&lt;/p&gt;

&lt;p&gt;Here's a webhook config to log post results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postId"&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.id }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"twitter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ now().toISO() }}"&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;"{{ $json.postText }}"&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;"success"&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;Send this to a &lt;strong&gt;Webhook by Zapier&lt;/strong&gt; or a custom webhook endpoint. Store the data in Google Sheets or a database, and after 7 days, query back with a &lt;strong&gt;HTTP Request&lt;/strong&gt; node to fetch performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.twitter.com/2/tweets/{{ $json.postId }}/metrics?metrics=public_metrics
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you build a real-time dashboard showing which posts performed best, so you can tailor future content.&lt;/p&gt;

&lt;p&gt;If you're managing multiple accounts or clients, you might want to add a &lt;strong&gt;Split In Batches&lt;/strong&gt; node to loop through different credential sets, so each client's posts go to their own accounts without mixing data. Similar logic applies if you're &lt;a href="https://dev.to/blog/email-newsletter-automation-n8n/"&gt;automating an email newsletter&lt;/a&gt;—you can trigger sends based on the same schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Next steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up for &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; (free tier available)&lt;/li&gt;
&lt;li&gt;Get API credentials from your social platforms&lt;/li&gt;
&lt;li&gt;Build your first single-platform workflow (start with Twitter/X)&lt;/li&gt;
&lt;li&gt;Add a second platform and test parallel execution&lt;/li&gt;
&lt;li&gt;Replace the Manual Trigger with a Cron trigger&lt;/li&gt;
&lt;li&gt;Connect Google Sheets to vary content daily&lt;/li&gt;
&lt;li&gt;Add analytics webhooks to measure performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're self-hosting, grab a VPS from &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo&lt;/a&gt;, install n8n via Docker, and secure it with Nginx reverse proxy (I have a full guide on that setup). Then deploy this same workflow—it'll work identically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Outsource Your Automation
&lt;/h3&gt;

&lt;p&gt;Don't have time to configure this yourself? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hire me on Fiverr&lt;/a&gt;—mention &lt;strong&gt;SYS3-DEVTO&lt;/strong&gt; for priority. Or DM me at &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;chasebot.online&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The beauty of automation is that you set it up once and it runs forever. Every hour you save is an hour you can spend on strategy, creative thinking, or just taking a break. That's the real win.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/social-media-automation-n8n/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>socialmedia</category>
      <category>automation</category>
      <category>marketing</category>
    </item>
    <item>
      <title>How to Set Up Nginx Reverse Proxy with SSL (Let's Encrypt)</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Sat, 25 Apr 2026 08:00:26 +0000</pubDate>
      <link>https://dev.to/chasebot/how-to-set-up-nginx-reverse-proxy-with-ssl-lets-encrypt-gek</link>
      <guid>https://dev.to/chasebot/how-to-set-up-nginx-reverse-proxy-with-ssl-lets-encrypt-gek</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n (optional, for API monitoring)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for your server&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; for domain registration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/do?utm_source=system3_devto" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; as an alternative hosting option&lt;/li&gt;
&lt;li&gt;SSH access to your Linux server (Ubuntu 20.04 or later preferred)&lt;/li&gt;
&lt;li&gt;A domain name pointing to your server's IP address&lt;/li&gt;
&lt;li&gt;Basic command-line knowledge&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Understanding Nginx Reverse Proxy Architecture&lt;/li&gt;
&lt;li&gt;Installing Nginx and Certbot&lt;/li&gt;
&lt;li&gt;Configuring Your First Reverse Proxy&lt;/li&gt;
&lt;li&gt;Setting Up SSL with Let's Encrypt&lt;/li&gt;
&lt;li&gt;Securing Your Configuration&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Understanding Nginx Reverse Proxy Architecture
&lt;/h2&gt;

&lt;p&gt;I've been managing production servers for years, and one thing I learned early: a reverse proxy is your infrastructure's best friend. Unlike a forward proxy (which shields clients), a reverse proxy sits between your internet-facing traffic and your internal applications. This setup gives you load balancing, SSL termination, security hardening, and the ability to run multiple services behind a single domain.&lt;/p&gt;

&lt;p&gt;Here's why this matters: if you're running multiple backend services—say, a Node.js API on port 3000 and a Python Flask app on port 5000—a reverse proxy lets clients connect to both via &lt;code&gt;api.example.com&lt;/code&gt; and &lt;code&gt;app.example.com&lt;/code&gt; without exposing internal ports. Add Let's Encrypt SSL, and you've got enterprise-grade encryption for free.&lt;/p&gt;

&lt;p&gt;This is especially useful if you're building automation workflows. For example, if you're running a self-hosted &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; instance or managing &lt;a href="https://dev.to/blog/n8n-replace-saas-tools/"&gt;automation workflows that replace expensive SaaS tools&lt;/a&gt;, you'll want that traffic encrypted end-to-end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing Nginx and Certbot
&lt;/h2&gt;

&lt;p&gt;First, SSH into your server. I'm assuming you've already provisioned a &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; with Ubuntu 20.04 or later. Your domain should already be pointing to your server's IP address (if not, update your DNS records at &lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Update your package manager and install Nginx:&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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start Nginx and enable it to run on boot:&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="nb"&gt;sudo &lt;/span&gt;systemctl start nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that Nginx is running:&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="nb"&gt;sudo &lt;/span&gt;systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;active (running)&lt;/code&gt; in the output. Now install Certbot, the Let's Encrypt client, and the Nginx plugin:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certbot &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great. Now we're ready to configure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring Your First Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;Let's say you have a backend service running on &lt;code&gt;localhost:3000&lt;/code&gt; (maybe a Node.js app, a Python service, or anything HTTP). You want to expose it securely at &lt;code&gt;api.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Open the Nginx configuration directory:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the entire file with this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;backend_service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_redirect&lt;/span&gt; &lt;span class="no"&gt;off&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;Let me break this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;upstream backend_service&lt;/strong&gt;: Defines where traffic goes. Point this to your actual backend port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;listen 80&lt;/strong&gt;: Listen on HTTP (Certbot will upgrade this to HTTPS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;server_name api.example.com&lt;/strong&gt;: Your domain. Change this to your actual domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;proxy_pass&lt;/strong&gt;: Routes requests to your backend service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;proxy_set_header directives&lt;/strong&gt;: Preserve client information (IP, protocol, domain) so your backend app sees the real client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test the configuration syntax:&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="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;syntax is ok&lt;/code&gt;, reload Nginx:&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="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you visit &lt;code&gt;http://api.example.com&lt;/code&gt; in your browser right now, it should proxy to your backend service on port 3000. The connection is unencrypted, though—that's next.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setting Up SSL with Let's Encrypt
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. Certbot automates the entire SSL setup, including renewal. Run Certbot with the Nginx plugin:&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="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot will ask a few questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter your email address (for renewal reminders).&lt;/li&gt;
&lt;li&gt;Accept the Let's Encrypt terms of service.&lt;/li&gt;
&lt;li&gt;Optionally share your email with the EFF (their choice).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Choose &lt;strong&gt;"2"&lt;/strong&gt; when asked about redirecting HTTP to HTTPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, unless you have a specific reason otherwise.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot automatically modifies your Nginx config to enable HTTPS and redirect HTTP traffic. Here's what your config now looks like:&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="nb"&gt;sudo cat&lt;/span&gt; /etc/nginx/sites-available/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something like this (Certbot added the HTTPS server block and modified the HTTP block):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;backend_service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$server_name$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/api.example.com/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/api.example.com/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/options-ssl-nginx.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_dhparam&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/ssl-dhparams.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_redirect&lt;/span&gt; &lt;span class="no"&gt;off&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;Test the syntax:&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="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload Nginx:&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="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now visit &lt;code&gt;https://api.example.com&lt;/code&gt;. Your connection is encrypted with a valid SSL certificate—and it's completely free. Your certificate is valid for 90 days. Certbot automatically renews it before expiration via a system timer.&lt;/p&gt;

&lt;p&gt;Verify the renewal timer is active:&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="nb"&gt;sudo &lt;/span&gt;systemctl status certbot.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;active&lt;/code&gt; in the output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Securing Your Configuration
&lt;/h2&gt;

&lt;p&gt;A reverse proxy without proper security is like leaving your front door unlocked. Let's harden the setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add Security Headers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Edit your Nginx config:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add these directives inside the HTTPS server block (after &lt;code&gt;ssl_dhparam&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-XSS-Protection&lt;/span&gt; &lt;span class="s"&gt;"1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;mode=block"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These headers prevent clickjacking, MIME sniffing, and enforce HTTPS everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Limit Request Rate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're concerned about DDoS or brute-force attacks, add rate limiting. Insert this outside any server block (near the top of the file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=general:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=login:10m&lt;/span&gt; &lt;span class="s"&gt;rate=5r/m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add this inside the HTTPS server block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=general&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&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;3. Restrict File Access&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add this inside the HTTPS server block to block sensitive files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;~$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&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;&lt;strong&gt;4. Enable Gzip Compression&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Outside any server block, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;gzip_vary&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;gzip_min_length&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;gzip_types&lt;/span&gt; &lt;span class="nc"&gt;text/plain&lt;/span&gt; &lt;span class="nc"&gt;text/css&lt;/span&gt; &lt;span class="nc"&gt;text/xml&lt;/span&gt; &lt;span class="nc"&gt;text/javascript&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt; &lt;span class="nc"&gt;application/javascript&lt;/span&gt; &lt;span class="nc"&gt;application/xml&lt;/span&gt;&lt;span class="s"&gt;+rss&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;gzip_disable&lt;/span&gt; &lt;span class="s"&gt;"MSIE&lt;/span&gt; &lt;span class="s"&gt;[1-6]&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test and reload:&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="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Advanced: Multiple Backend Services
&lt;/h2&gt;

&lt;p&gt;If you're running multiple services (say, an API and a web dashboard), create separate upstream blocks and location directives. Here's an example:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;api_backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;app_backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt; &lt;span class="s"&gt;app.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$server_name$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/nginx-reverse-proxy-ssl-setup/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>ssl</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Jupiter API Guide: Free Solana Price Data for Your Apps</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Fri, 24 Apr 2026 08:00:28 +0000</pubDate>
      <link>https://dev.to/chasebot/jupiter-api-guide-free-solana-price-data-for-your-apps-2nc2</link>
      <guid>https://dev.to/chasebot/jupiter-api-guide-free-solana-price-data-for-your-apps-2nc2</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for self-hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/do?utm_source=system3_devto" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; as an alternative hosting option&lt;/li&gt;
&lt;li&gt;A Jupiter API key (free tier available)&lt;/li&gt;
&lt;li&gt;Node.js 16+ or Python 3.8+ for local testing&lt;/li&gt;
&lt;li&gt;Basic understanding of REST APIs and webhooks&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Understanding Jupiter API and Why It Matters&lt;/li&gt;
&lt;li&gt;Getting Your Free Jupiter API Key&lt;/li&gt;
&lt;li&gt;Building Your First Price Data Query&lt;/li&gt;
&lt;li&gt;Real-Time Solana Price Monitoring with n8n&lt;/li&gt;
&lt;li&gt;Storing Price Data in Google Sheets&lt;/li&gt;
&lt;li&gt;Advanced: Historical Data Analysis&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Understanding Jupiter API and Why It Matters
&lt;/h2&gt;

&lt;p&gt;I've been working with Solana ecosystem APIs for about three years now, and I can tell you that Jupiter API is one of the best-kept secrets for developers who need reliable, free Solana price data. Jupiter is the largest DEX aggregator on Solana, and their API exposes real-time pricing information that powers some of the most sophisticated trading bots and analytics platforms in the space.&lt;/p&gt;

&lt;p&gt;What makes Jupiter different from other price APIs? Speed, accuracy, and zero rate limits for reasonable usage. You're not dealing with a centralized price feed—you're tapping directly into on-chain liquidity data from Solana's most liquid markets. This means you get true price discovery rather than some aggregated guess.&lt;/p&gt;

&lt;p&gt;The use cases are endless: build portfolio dashboards, create alert systems when tokens hit certain prices, automate trading decisions, or feed price data into machine learning models. I've personally used Jupiter API to power everything from Discord bots that alert traders to automated portfolio rebalancers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Your Free Jupiter API Key
&lt;/h2&gt;

&lt;p&gt;First, head to the Jupiter API documentation. They don't require traditional API keys—the API is public and free to use. This is one of the beauties of the Solana ecosystem. However, you'll want to understand rate limiting and best practices.&lt;/p&gt;

&lt;p&gt;Here's the base endpoint you'll be working with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://price.jup.ag/v4/price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No authentication needed. Let me show you the anatomy of a real request:&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; GET &lt;span class="s2"&gt;"https://price.jup.ag/v4/price?ids=So11111111111111111111111111111111111111112&amp;amp;vsToken=EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ids&lt;/code&gt; = the token mint address you want price data for (SOL in this case)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vsToken&lt;/code&gt; = what you're pricing against (USDC in this example)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="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="nl"&gt;"So11111111111111111111111111111111111111112"&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;"So11111111111111111111111111111111111111112"&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;"derivedPrice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"175.50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"decimals"&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;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1704067200000&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;"timeTaken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0001&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;Clean, fast, and immediately actionable. You get the price, decimal precision, and timestamp.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Your First Price Data Query
&lt;/h2&gt;

&lt;p&gt;Let me walk you through a practical example. I'm going to show you how to query multiple tokens at once and parse the response in Node.js:&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;https&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&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&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;tokenMints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;SOL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;So11111111111111111111111111111111111111112&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;USDC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;USDT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenYes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;RAY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4k3Dyjzvzp8eMZWUXbBCjEvwSvsrH3xn5ZH3KD2Zcco&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;JUP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JUPyiwrYJFskUPiHa7hkeR8NqtwybKHLo3xKc4CMycJ&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;vsToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;getPrices&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;mintIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenMints&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://price.jup.ag/v4/price?ids=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;mintIds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;vsToken=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vsToken&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;return&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;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="nx"&gt;https&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&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&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;chunk&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="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&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="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&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="o"&gt;=&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&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="nf"&gt;parse&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="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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;e&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="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;displayPrices&lt;/span&gt;&lt;span class="p"&gt;()&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prices&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;getPrices&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;tokenNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenMints&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;tokenNames&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenMints&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nx"&gt;index&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;priceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prices&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;mint&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;priceData&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="s2"&gt;`&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;priceData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&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="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;error&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error fetching prices:&lt;/span&gt;&lt;span class="dl"&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;displayPrices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this as &lt;code&gt;get-prices.js&lt;/code&gt; and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node get-prices.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SOL: $175.50
USDC: $1.00
USDT: $0.99
RAY: $1.25
JUP: $0.45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty here is that you're making one request to get multiple token prices. This is efficient and respects Jupiter's infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-Time Solana Price Monitoring with n8n
&lt;/h2&gt;

&lt;p&gt;Now let's automate this. I'm going to show you how to build a workflow in &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; that queries Jupiter API every minute and stores results.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's your n8n workflow configuration (exportable JSON):&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;"nodes"&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;"parameters"&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;"rule"&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;"interval"&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;"intervalValue"&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;"intervalUnit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minutes"&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;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;"fd5a4d6c-8c2e-4bbb-9f3e-2a1b3c4d5e6f"&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;"Trigger"&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;"n8n-nodes-base.cron"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&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;"position"&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="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;300&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="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://price.jup.ag/v4/price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"specialParameters"&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;"queryParameters"&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;"ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"So11111111111111111111111111111111111111112,EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md,Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenYes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"vsToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md"&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="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;"a1b2c3d4-e5f6-4g7h-8i9j-0k1l2m3n4o5p"&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;"Fetch Jupiter Prices"&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;"n8n-nodes-base.httpRequest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;300&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="nl"&gt;"parameters"&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;"functionCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"const data = $input.first().json.data;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;const prices = {};&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;const tokenMap = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  'So11111111111111111111111111111111111111112': 'SOL',&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  'EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md': 'USDC',&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenYes': 'USDT'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;};&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Object.keys(data).forEach(mint =&amp;gt; {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  const tokenName = tokenMap[mint];&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  if (tokenName) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    prices[tokenName] = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;      price: data[mint].price,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;      timestamp: new Date(data[mint].time).toISOString()&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    };&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  }&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;});&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;return { prices, timestamp: new Date().toISOString() };"&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;"b2c3d4e5-f6g7-4h8i-9j0k-1l2m3n4o5p6q"&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;"Transform Prices"&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;"n8n-nodes-base.function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&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;"position"&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="mi"&gt;750&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;300&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="nl"&gt;"connections"&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;"Trigger"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fetch Jupiter Prices"&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;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="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;"Fetch Jupiter Prices"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transform Prices"&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;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="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;After you create this workflow in n8n, you can extend it by adding a webhook that triggers alerts when prices move by more than 5%. Here's the alert logic you'd add to the Transform Prices function:&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&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;data&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;prices&lt;/span&gt; &lt;span class="o"&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;tokenMap&lt;/span&gt; &lt;span class="o"&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;So11111111111111111111111111111111111111112&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;SOL&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;EPjFWaJgt46ggsuKpQSetg69YXiGBkSstw2HYPi81md&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;USDC&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;Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenYes&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;USDT&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;previousPrices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;SOL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;175.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;USDC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;USDT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mint&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;tokenName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mint&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;tokenName&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;currentPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&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;mint&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;price&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;previousPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previousPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tokenName&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/jupiter-api-solana-price-data/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>crypto</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Lead Capture System with n8n and Typeform</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:00:28 +0000</pubDate>
      <link>https://dev.to/chasebot/building-a-lead-capture-system-with-n8n-and-typeform-4a55</link>
      <guid>https://dev.to/chasebot/building-a-lead-capture-system-with-n8n-and-typeform-4a55</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for self-hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; if you need a custom domain&lt;/li&gt;
&lt;li&gt;Typeform account (free tier works)&lt;/li&gt;
&lt;li&gt;A spreadsheet tool like Google Sheets or Airtable (optional but recommended)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Typeform + n8n Is a Game-Changer&lt;/li&gt;
&lt;li&gt;Setting Up Your Typeform&lt;/li&gt;
&lt;li&gt;Connecting n8n to Typeform&lt;/li&gt;
&lt;li&gt;Building the Lead Capture Workflow&lt;/li&gt;
&lt;li&gt;Adding Data Validation and Filtering&lt;/li&gt;
&lt;li&gt;Storing Leads in Google Sheets&lt;/li&gt;
&lt;li&gt;Sending Automated Follow-Up Emails&lt;/li&gt;
&lt;li&gt;Testing and Deploying&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why Typeform + n8n Is a Game-Changer
&lt;/h2&gt;

&lt;p&gt;I've been building automation workflows for three years now, and the combination of Typeform and &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; is hands-down one of the most cost-effective ways to capture, validate, and nurture leads without touching a single line of code—well, almost.&lt;/p&gt;

&lt;p&gt;Here's the reality: most teams either pay $50–200/month for marketing automation platforms that feel bloated, or they cobble together Zapier, Make.com, and custom scripts that break after every update. With n8n, you own your workflow completely. No surprise pricing. No feature limits based on your plan tier.&lt;/p&gt;

&lt;p&gt;When a lead fills out your Typeform, n8n instantly captures that data, validates it, stores it wherever you want, and triggers follow-up actions—all in a single, unified workflow. If you're already looking to optimize your stack, check out our guide on &lt;a href="https://dev.to/blog/n8n-replace-saas-tools/"&gt;5 n8n Workflows That Replace $200/Month in SaaS Tools&lt;/a&gt; to see how many other platforms you can retire.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Your Typeform
&lt;/h2&gt;

&lt;p&gt;First, let's create the form that'll capture your leads. I'm going to keep it simple for this guide—name, email, phone, and a company field.&lt;/p&gt;

&lt;p&gt;Log into Typeform and click &lt;strong&gt;Create a new form&lt;/strong&gt;. Choose the &lt;strong&gt;Blank&lt;/strong&gt; template. Here's what to add:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field 1: Full Name&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question type: Short text&lt;/li&gt;
&lt;li&gt;Question: "What's your full name?"&lt;/li&gt;
&lt;li&gt;Make it required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Field 2: Email Address&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question type: Email&lt;/li&gt;
&lt;li&gt;Question: "What's your email address?"&lt;/li&gt;
&lt;li&gt;Make it required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Field 3: Phone Number&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question type: Phone number&lt;/li&gt;
&lt;li&gt;Question: "What's your phone number?"&lt;/li&gt;
&lt;li&gt;Optional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Field 4: Company&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question type: Short text&lt;/li&gt;
&lt;li&gt;Question: "What company are you with?"&lt;/li&gt;
&lt;li&gt;Optional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Field 5: How did you hear about us?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question type: Multiple choice&lt;/li&gt;
&lt;li&gt;Options: Google, LinkedIn, Referral, Other&lt;/li&gt;
&lt;li&gt;Make it required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you've built the form, click &lt;strong&gt;Share&lt;/strong&gt; and grab the public link. You'll also need your Typeform API token, which you can get from your account settings under &lt;strong&gt;API &amp;amp; Apps&lt;/strong&gt; → &lt;strong&gt;Personal tokens&lt;/strong&gt;. Create a new token and copy it—you'll need this in n8n.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting n8n to Typeform
&lt;/h2&gt;

&lt;p&gt;Now let's set up &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; to listen for new Typeform responses. If you're self-hosting, the steps are identical—just deploy n8n on a &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Log into n8n and create a new workflow. Start by dragging in a &lt;strong&gt;Webhook&lt;/strong&gt; node—this is how n8n will receive data from Typeform in real-time.&lt;/p&gt;

&lt;p&gt;In the Webhook node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;HTTP Method&lt;/strong&gt; to POST&lt;/li&gt;
&lt;li&gt;Copy the webhook URL (you'll need this in Typeform)&lt;/li&gt;
&lt;li&gt;Leave &lt;strong&gt;Authentication&lt;/strong&gt; as None for now (we'll secure it later if needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, head back to Typeform. Go to your form's &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Integrations&lt;/strong&gt; → &lt;strong&gt;Webhooks&lt;/strong&gt;. Paste your n8n webhook URL and select which events to trigger on—choose &lt;strong&gt;Response completed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Next, add a &lt;strong&gt;Typeform&lt;/strong&gt; node in n8n to pull historical responses. Click the &lt;strong&gt;+&lt;/strong&gt; icon to add a new node, search for Typeform, and select the &lt;strong&gt;Typeform&lt;/strong&gt; trigger node.&lt;/p&gt;

&lt;p&gt;Create a new Typeform credential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste your API token&lt;/li&gt;
&lt;li&gt;Test the connection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Select your form from the dropdown. This node will trigger whenever a new response is submitted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Lead Capture Workflow
&lt;/h2&gt;

&lt;p&gt;Here's the actual workflow we're going to build. I'll walk you through each node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 1: Typeform Trigger&lt;/strong&gt;&lt;br&gt;
This is your entry point. It fires whenever someone submits the form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 2: Parse the Response&lt;/strong&gt;&lt;br&gt;
Add a &lt;strong&gt;Set&lt;/strong&gt; node to extract and structure the data:&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;"full_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;"{{ $json.form_response.answers[0].text }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.form_response.answers[1].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;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.form_response.answers[2].phone_number }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.form_response.answers[3].text }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.form_response.answers[4].choice.label }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"submission_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;"{{ $json.form_response.token }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"submitted_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;"{{ $json.form_response.submitted_at }}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This restructures the Typeform response into clean, usable data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Adding Data Validation and Filtering
&lt;/h2&gt;

&lt;p&gt;Not every submission is a qualified lead. Let's add validation to filter out bad data.&lt;/p&gt;

&lt;p&gt;Add an &lt;strong&gt;If&lt;/strong&gt; node after your &lt;strong&gt;Set&lt;/strong&gt; node:&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;// Check if email exists and is valid&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;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;email&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="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// AND check if name is at least 2 characters&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&amp;amp;&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;full_name&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;full_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the condition is true, the workflow continues. If false, it stops—you can optionally send a data quality error notification.&lt;/p&gt;

&lt;p&gt;For additional filtering, you might want to check against your existing customer database. Use another &lt;strong&gt;If&lt;/strong&gt; node to query a database and mark duplicates:&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: Skip if email already in database&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;existing_customer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're building more complex workflows like this, exploring &lt;a href="https://dev.to/blog/best-free-apis-automation/"&gt;The Best Free APIs for Building Automation Workflows in 2026&lt;/a&gt; can help you integrate additional validation services without cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Storing Leads in Google Sheets
&lt;/h2&gt;

&lt;p&gt;Now let's save every valid lead to a spreadsheet. Add a &lt;strong&gt;Google Sheets&lt;/strong&gt; node.&lt;/p&gt;

&lt;p&gt;Set up your Google credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Authenticate&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select your Google account&lt;/li&gt;
&lt;li&gt;Choose or create a new spreadsheet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the Google Sheets node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document ID&lt;/strong&gt;: Paste your spreadsheet ID (from the URL)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sheet&lt;/strong&gt;: Select the sheet name (e.g., "Leads")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columns&lt;/strong&gt;: Map each field:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Column A: full_name → {{ $json.full_name }}
Column B: email → {{ $json.email }}
Column C: phone → {{ $json.phone }}
Column D: company → {{ $json.company }}
Column E: source → {{ $json.source }}
Column F: submitted_at → {{ $json.submitted_at }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a permanent record of every lead your form captures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sending Automated Follow-Up Emails
&lt;/h2&gt;

&lt;p&gt;The real power of this system is automation. Let's send a personalized welcome email the moment someone becomes a lead.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Send Email&lt;/strong&gt; node (or use &lt;strong&gt;Gmail&lt;/strong&gt; if you want to send from your Gmail account):&lt;/p&gt;

&lt;p&gt;Configure it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;To&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; {{ $json.email }}&lt;/span&gt;
&lt;span class="nt"&gt;From&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; noreply@yourcompany.com&lt;/span&gt;
&lt;span class="nt"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Welcome, {{ $json.full_name }}! Here's what's next&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the email body, use HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Hey {{ $json.full_name }},&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Thanks for filling out our form! We saw you're with &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ $json.company }}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; and heard about us through &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ $json.source }}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Here's what happens next:&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Our team will review your information&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;You'll get a personalized demo within 24 hours&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;We'll answer any questions you have&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;In the meantime, &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://yourcompany.com/resources"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;check out our resources&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Cheers,&lt;span class="nt"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;The Team&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to take this further and automate email sequences, our guide on &lt;a href="https://dev.to/blog/email-newsletter-automation-n8n/"&gt;Automate Your Email Newsletter with n8n and Claude&lt;/a&gt; shows how to generate personalized content at scale using AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing and Deploying
&lt;/h2&gt;

&lt;p&gt;Before you go live, test the entire workflow. Click &lt;strong&gt;Execute Workflow&lt;/strong&gt; and fill out your Typeform manually. Watch each node execute in real-time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Typeform trigger fires&lt;/li&gt;
&lt;li&gt;Data is parsed and structured&lt;/li&gt;
&lt;li&gt;Validation checks pass&lt;/li&gt;
&lt;li&gt;Lead is added to Google Sheets&lt;/li&gt;
&lt;li&gt;Welcome email is sent&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any step fails, n8n will show you the exact error. Common issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invalid email format&lt;/strong&gt; → Typeform should validate this, but n8n double-checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Sheets authentication expired&lt;/strong&gt; → Re-authenticate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email sending fails&lt;/strong&gt; → Check your email provider's SMTP credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once everything works, click &lt;strong&gt;Activate&lt;/strong&gt; to turn on the workflow. It's now live and listening for new Typeform submissions 24/7.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;You're ready to capture leads like a pro. Here's your checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create your Typeform with the fields you need&lt;/li&gt;
&lt;li&gt;Set up &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; (or self-host on &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Connect Typeform's API to n8n&lt;/li&gt;
&lt;li&gt;Build the workflow following the steps above&lt;/li&gt;
&lt;li&gt;Test thoroughly before activating&lt;/li&gt;
&lt;li&gt;Monitor your Google Sheets for leads rolling in&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From here, you can expand: add Slack notifications, integrate with your CRM, score leads based on company size, or trigger different workflows based on which "source" they selected.&lt;/p&gt;




&lt;h3&gt;
  
  
  Outsource Your Automation
&lt;/h3&gt;

&lt;p&gt;Don't have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hire me on Fiverr&lt;/a&gt; — mention &lt;strong&gt;SYS3-DEVTO&lt;/strong&gt; for priority. Or DM at &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;chasebot.online&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/lead-capture-n8n-typeform/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>leadgen</category>
      <category>typeform</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Deploy n8n with Docker on Any VPS (2026 Guide)</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Wed, 22 Apr 2026 08:00:22 +0000</pubDate>
      <link>https://dev.to/chasebot/how-to-deploy-n8n-with-docker-on-any-vps-2026-guide-5fn6</link>
      <guid>https://dev.to/chasebot/how-to-deploy-n8n-with-docker-on-any-vps-2026-guide-5fn6</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n (we're covering self-hosted)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt;, &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt;, or &lt;a href="https://chasebot.online/go/do?utm_source=system3_devto" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A domain name (optional, but recommended—grab one at &lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Basic command-line comfort&lt;/li&gt;
&lt;li&gt;2GB+ RAM and 20GB+ storage on your VPS&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Self-Host n8n on a VPS?&lt;/li&gt;
&lt;li&gt;Setting Up Your VPS&lt;/li&gt;
&lt;li&gt;Installing Docker&lt;/li&gt;
&lt;li&gt;Deploying n8n with Docker Compose&lt;/li&gt;
&lt;li&gt;Configuring SSL and Your Domain&lt;/li&gt;
&lt;li&gt;Running Your First Workflow&lt;/li&gt;
&lt;li&gt;Getting Started with Production&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why Self-Host n8n on a VPS?
&lt;/h2&gt;

&lt;p&gt;I made the switch to self-hosted &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; about two years ago, and honestly, it changed how I build automation. You get complete control over your workflows, no per-execution pricing limits, and the ability to run custom code without restrictions. Plus, if you're already paying for a &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;VPS for automation&lt;/a&gt;, you can run n8n alongside other services—databases, APIs, webhooks—all in one place.&lt;/p&gt;

&lt;p&gt;The deployment is surprisingly straightforward with Docker. I'll walk you through it step-by-step so you can have a production-ready n8n instance running in under an hour.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Your VPS
&lt;/h2&gt;

&lt;p&gt;Start with a fresh Ubuntu 22.04 LTS instance. If you're comparing hosting options, I've got a detailed breakdown of &lt;a href="https://chasebot.online/go/vps?utm_source=system3_devto" rel="noopener noreferrer"&gt;VPS choices for automation (Hetzner vs Contabo vs Railway)&lt;/a&gt; that might help you decide. For this guide, I'm assuming you've already provisioned your server and can SSH into it.&lt;/p&gt;

&lt;p&gt;Connect to your VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@your_vps_ip_address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your system packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a non-root user for security (optional but recommended):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser n8nuser
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;n8nuser
su - n8nuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Installing Docker
&lt;/h2&gt;

&lt;p&gt;Docker is the easiest way to run n8n without dependency hell. Install the official Docker repository:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg lsb-release
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-cs&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Docker and Docker Compose:&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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your user to the Docker group so you don't need &lt;code&gt;sudo&lt;/code&gt; every time:&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="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
newgrp docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deploying n8n with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Create a project directory:&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/n8n-deployment
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/n8n-deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file for configuration:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
N8N_HOST=your-domain.com
N8N_PORT=5678
N8N_PROTOCOL=https
NODE_ENV=production
N8N_SECURE_COOKIE=true
N8N_ENCRYPTION_KEY=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
GENERIC_TIMEZONE=UTC
DB_TYPE=postgres
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
WEBHOOK_URL=https://your-domain.com/webhook
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your-domain.com&lt;/code&gt; with your actual domain. If you don't have a domain yet, &lt;a href="https://chasebot.online/go/namecheap?utm_source=system3_devto" rel="noopener noreferrer"&gt;grab one from Namecheap&lt;/a&gt; for cheap.&lt;/p&gt;

&lt;p&gt;Now create the &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker-compose.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: n8n-postgres
    environment:
      POSTGRES_USER: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      POSTGRES_PASSWORD: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      POSTGRES_DB: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - n8n-net
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"]
      interval: 10s
      timeout: 5s
      retries: 5

  n8n:
    image: n8nio/n8n:latest
    container_name: n8n-app
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - DB_TYPE=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_TYPE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - DB_POSTGRESDB_HOST=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - DB_POSTGRESDB_PORT=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - DB_POSTGRESDB_DATABASE=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - DB_POSTGRESDB_USER=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - DB_POSTGRESDB_PASSWORD=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - N8N_HOST=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;N8N_HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - N8N_PORT=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;N8N_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - N8N_PROTOCOL=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;N8N_PROTOCOL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - NODE_ENV=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - N8N_SECURE_COOKIE=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;N8N_SECURE_COOKIE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - N8N_ENCRYPTION_KEY=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - GENERIC_TIMEZONE=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
      - WEBHOOK_URL=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
    volumes:
      - n8n_data:/home/node/.n8n
    networks:
      - n8n-net
    depends_on:
      postgres:
        condition: service_healthy

  nginx:
    image: nginx:alpine
    container_name: n8n-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    networks:
      - n8n-net
    depends_on:
      - n8n

volumes:
  postgres_data:
  n8n_data:

networks:
  n8n-net:
    driver: bridge
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the Nginx configuration file:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nginx.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="sh"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$remote_user&lt;/span&gt;&lt;span class="sh"&gt; [&lt;/span&gt;&lt;span class="nv"&gt;$time_local&lt;/span&gt;&lt;span class="sh"&gt;] "&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="sh"&gt;" '
                    '&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="sh"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$body_bytes_sent&lt;/span&gt;&lt;span class="sh"&gt; "&lt;/span&gt;&lt;span class="nv"&gt;$http_referer&lt;/span&gt;&lt;span class="sh"&gt;" '
                    '"&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$http_x_forwarded_for&lt;/span&gt;&lt;span class="sh"&gt;"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    gzip on;

    upstream n8n {
        server n8n:5678;
    }

    server {
        listen 80;
        server_name your-domain.com;
        return 301 https://&lt;/span&gt;&lt;span class="nv"&gt;$server_name$request_uri&lt;/span&gt;&lt;span class="sh"&gt;;
    }

    server {
        listen 443 ssl http2;
        server_name your-domain.com;

        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;

        client_max_body_size 50M;

        location / {
            proxy_pass http://n8n;
            proxy_http_version 1.1;
            proxy_set_header Upgrade &lt;/span&gt;&lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="sh"&gt;;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="sh"&gt;;
            proxy_set_header X-Real-IP &lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="sh"&gt;;
            proxy_set_header X-Forwarded-For &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="sh"&gt;;
            proxy_set_header X-Forwarded-Proto &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="sh"&gt;;
            proxy_cache_bypass &lt;/span&gt;&lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="sh"&gt;;
        }
    }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Configuring SSL and Your Domain
&lt;/h2&gt;

&lt;p&gt;First, point your domain to your VPS IP address in your DNS provider. Create an SSL certificate using Let's Encrypt:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-nginx
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ssl
&lt;span class="nb"&gt;cd &lt;/span&gt;ssl
&lt;span class="nb"&gt;sudo &lt;/span&gt;openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="nt"&gt;-keyout&lt;/span&gt; key.pem &lt;span class="nt"&gt;-out&lt;/span&gt; cert.pem &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=your-domain.com"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, use Let's Encrypt instead:&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="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; your-domain.com &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; &lt;span class="nt"&gt;--agree-tos&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; your-email@example.com
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/letsencrypt/live/your-domain.com/fullchain.pem ssl/cert.pem
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/letsencrypt/live/your-domain.com/privkey.pem ssl/key.pem
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:&lt;span class="nv"&gt;$USER&lt;/span&gt; ssl/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your &lt;code&gt;nginx.conf&lt;/code&gt; with your actual domain name (replace &lt;code&gt;your-domain.com&lt;/code&gt; twice in the file).&lt;/p&gt;

&lt;p&gt;Start the deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that all containers are running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see three services: &lt;code&gt;n8n-postgres&lt;/code&gt;, &lt;code&gt;n8n-app&lt;/code&gt;, and &lt;code&gt;n8n-nginx&lt;/code&gt;. View logs if anything fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; n8n-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running Your First Workflow
&lt;/h2&gt;

&lt;p&gt;Open your browser and go to &lt;code&gt;https://your-domain.com&lt;/code&gt;. You'll see the n8n setup screen. Create your admin account and log in.&lt;/p&gt;

&lt;p&gt;Once inside, create a simple test workflow to verify everything works. Click the &lt;strong&gt;+&lt;/strong&gt; button to add a trigger node. Select &lt;strong&gt;Webhook&lt;/strong&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webhook/test"&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;Add an &lt;strong&gt;HTTP Request&lt;/strong&gt; node:&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/deploy-n8n-docker-vps/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>docker</category>
      <category>vps</category>
      <category>devops</category>
    </item>
    <item>
      <title>ffmpeg for Content Creators: Essential Commands Cheat Sheet</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Tue, 21 Apr 2026 08:00:28 +0000</pubDate>
      <link>https://dev.to/chasebot/ffmpeg-for-content-creators-essential-commands-cheat-sheet-4ebc</link>
      <guid>https://dev.to/chasebot/ffmpeg-for-content-creators-essential-commands-cheat-sheet-4ebc</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A computer with FFmpeg installed (Windows, Mac, or Linux)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; if you're encoding videos at scale&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/do?utm_source=system3_devto" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; as an alternative cloud option&lt;/li&gt;
&lt;li&gt;Basic command-line comfort&lt;/li&gt;
&lt;li&gt;Sample media files (video or audio) to practice with&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why FFmpeg Matters for Content Creators&lt;/li&gt;
&lt;li&gt;Video Conversion &amp;amp; Compression&lt;/li&gt;
&lt;li&gt;Audio Extraction &amp;amp; Processing&lt;/li&gt;
&lt;li&gt;Batch Processing Multiple Files&lt;/li&gt;
&lt;li&gt;Creating Animated GIFs &amp;amp; Thumbnails&lt;/li&gt;
&lt;li&gt;Advanced Streaming &amp;amp; Watermarking&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why FFmpeg Matters for Content Creators
&lt;/h2&gt;

&lt;p&gt;I've been automating content workflows for years, and FFmpeg is the backbone of almost every video pipeline I build. It's free, it's powerful, and it runs on literally any server. If you're producing YouTube videos, Shorts, TikToks, or streaming content, you &lt;em&gt;need&lt;/em&gt; to understand at least the basics.&lt;/p&gt;

&lt;p&gt;The problem? FFmpeg has a reputation for being intimidating. The command syntax looks like ancient incantations. But once you learn the structure, you'll realize it's just a series of logical parameters: input → filters → codec → output.&lt;/p&gt;

&lt;p&gt;When I automate video processing at scale, I often use &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; to orchestrate FFmpeg jobs across multiple files. If you're looking to build a fully automated content production system, understanding FFmpeg pairs perfectly with workflow automation tools.&lt;/p&gt;

&lt;p&gt;Let me give you the commands you'll actually use every single day.&lt;/p&gt;




&lt;h2&gt;
  
  
  Video Conversion &amp;amp; Compression
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Basic MP4 Conversion
&lt;/h3&gt;

&lt;p&gt;This is the most common task. You've got a video file in some weird format, and you need it as MP4 for YouTube or web delivery.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mov &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-i input.mov&lt;/code&gt; — your input file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c:v libx264&lt;/code&gt; — use H.264 video codec (compatible everywhere)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-preset fast&lt;/code&gt; — encoding speed (fast, medium, slow; slower = better quality/bigger file)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-crf 23&lt;/code&gt; — quality (0-51; lower is better, 18-28 is typical)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c:a aac&lt;/code&gt; — audio codec&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-b:a 192k&lt;/code&gt; — audio bitrate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output.mp4&lt;/code&gt; — destination&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4K to 1080p Downscaling
&lt;/h3&gt;

&lt;p&gt;YouTube creators often need multiple resolutions. Here's how to downscale 4K to 1080p while maintaining quality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input_4k.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="nv"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1920:1080 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium &lt;span class="nt"&gt;-crf&lt;/span&gt; 21 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k output_1080p.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-vf scale=1920:1080&lt;/code&gt; filter resizes while maintaining aspect ratio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Highly Compressed Version for Mobile
&lt;/h3&gt;

&lt;p&gt;For social media where file size matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-crf&lt;/span&gt; 28 &lt;span class="nt"&gt;-s&lt;/span&gt; 1280x720 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k output_mobile.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s 1280x720&lt;/code&gt; flag sets resolution explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebM for Web Delivery
&lt;/h3&gt;

&lt;p&gt;WebM files are smaller than MP4 and great for embedding in websites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 1000k &lt;span class="nt"&gt;-c&lt;/span&gt;:a libopus &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k output.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VP9 takes longer to encode but creates tiny, high-quality files.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Audio Extraction &amp;amp; Processing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Extract Audio as MP3
&lt;/h3&gt;

&lt;p&gt;You have a video but only need the audio—common for podcasts or music content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-q&lt;/span&gt;:a 0 &lt;span class="nt"&gt;-map&lt;/span&gt; a output.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-q:a 0&lt;/code&gt; flag extracts the highest quality audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract Audio as WAV (Lossless)
&lt;/h3&gt;

&lt;p&gt;For professional audio work or archival:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:a pcm_s16le output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compress Audio Only
&lt;/h3&gt;

&lt;p&gt;Reduce file size without re-encoding video. This is useful when you're working with existing MP4s:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a libmp3lame &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-c:v copy&lt;/code&gt; flag skips video re-encoding entirely—instant processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Audio Track to Video (Without Audio)
&lt;/h3&gt;

&lt;p&gt;You've got a silent video and a separate audio file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; video_silent.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; audio.mp3 &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-map&lt;/span&gt; 0:v:0 &lt;span class="nt"&gt;-map&lt;/span&gt; 1:a:0 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps the video from file 0 and audio from file 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Normalize Audio Loudness
&lt;/h3&gt;

&lt;p&gt;If your audio is too quiet or too loud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="nv"&gt;loudnorm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;I&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-16&lt;/span&gt;:TP&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;.5:LRA&lt;span class="o"&gt;=&lt;/span&gt;11 &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loudnorm filter adjusts to broadcast standard loudness.&lt;/p&gt;




&lt;h2&gt;
  
  
  Batch Processing Multiple Files
&lt;/h2&gt;

&lt;p&gt;Here's where FFmpeg becomes truly powerful. Let's say you have 50 videos that need conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bash Script for Batch Conversion (Linux/Mac)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.mov&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;%.mov&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.mp4"&lt;/span&gt;
  ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this as &lt;code&gt;convert_all.sh&lt;/code&gt;, then run &lt;code&gt;chmod +x convert_all.sh &amp;amp;&amp;amp; ./convert_all.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows Batch Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;@echo &lt;span class="na"&gt;off&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="vm"&gt;%%f&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;(*&lt;/span&gt;.mov&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;ffmpeg&lt;/span&gt; &lt;span class="na"&gt;-i &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="vm"&gt;%%f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="na"&gt;-c&lt;/span&gt;&lt;span class="nl"&gt;:v&lt;/span&gt; &lt;span class="kd"&gt;libx264&lt;/span&gt; &lt;span class="na"&gt;-preset &lt;/span&gt;&lt;span class="kd"&gt;fast&lt;/span&gt; &lt;span class="na"&gt;-crf &lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="na"&gt;-c&lt;/span&gt;&lt;span class="nl"&gt;:a&lt;/span&gt; &lt;span class="kd"&gt;aac&lt;/span&gt; &lt;span class="na"&gt;-b&lt;/span&gt;&lt;span class="nl"&gt;:a&lt;/span&gt; &lt;span class="m"&gt;192&lt;/span&gt;&lt;span class="kd"&gt;k&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="vm"&gt;%%~nf&lt;/span&gt;&lt;span class="s2"&gt;.mp4"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;pause&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save as &lt;code&gt;convert_all.bat&lt;/code&gt; and double-click.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel Processing on a VPS
&lt;/h3&gt;

&lt;p&gt;If you're running this on a &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; with multiple CPU cores, use GNU Parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.mov"&lt;/span&gt; | parallel ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k &lt;span class="o"&gt;{&lt;/span&gt;.&lt;span class="o"&gt;}&lt;/span&gt;.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This processes multiple files simultaneously, cutting your total time dramatically.&lt;/p&gt;

&lt;p&gt;For automated batch processing at enterprise scale, I'd recommend integrating FFmpeg with &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; to trigger conversions based on file uploads or events. This is especially useful if you're running &lt;a href="https://dev.to/blog/vps-monitoring-n8n/"&gt;a system to monitor your VPS with n8n health check workflows&lt;/a&gt; since you can automatically alert when encoding finishes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating Animated GIFs &amp;amp; Thumbnails
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Convert Video to Animated GIF
&lt;/h3&gt;

&lt;p&gt;Perfect for social media previews:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=10,scale=320:-1:flags=lanczos"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v gif output.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fps=10&lt;/code&gt; — 10 frames per second (lower = smaller file)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scale=320:-1&lt;/code&gt; — scale to 320px wide, maintain aspect ratio&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flags=lanczos&lt;/code&gt; — high-quality scaling algorithm&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Extract a Single Frame as Thumbnail
&lt;/h3&gt;

&lt;p&gt;Get a frame at exactly 15 seconds in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 00:00:15 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="nv"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1280:720 &lt;span class="nt"&gt;-q&lt;/span&gt;:v 2 thumbnail.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-ss 00:00:15&lt;/code&gt; flag seeks to 15 seconds. Adjust timing to capture your best moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract Multiple Frames
&lt;/h3&gt;

&lt;p&gt;Create a grid of thumbnails from your video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=1/10,scale=320:240"&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;:v 2 frame_%04d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extracts one frame every 10 seconds. Perfect for creating preview grids.&lt;/p&gt;

&lt;h3&gt;
  
  
  High-Quality Thumbnail
&lt;/h3&gt;

&lt;p&gt;YouTube-optimized thumbnail (1280x720):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 00:00:30 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="nv"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1280:720 &lt;span class="nt"&gt;-q&lt;/span&gt;:v 1 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 thumbnail_hq.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Advanced Streaming &amp;amp; Watermarking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add a Text Watermark
&lt;/h3&gt;

&lt;p&gt;Overlay text on your video (useful for branding):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"drawtext=text='YourName':fontfile=/path/to/font.ttf:fontsize=24:fontcolor=white:x=10:y=10"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Mac, use &lt;code&gt;/Library/Fonts/Arial.ttf&lt;/code&gt;. On Linux, try &lt;code&gt;/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add an Image Watermark
&lt;/h3&gt;

&lt;p&gt;Overlay a logo or watermark image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; logo.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v][1:v] overlay=W-w-10:H-h-10:enable='between(t,0,duration)'"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This places the logo 10px from the bottom-right corner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create DASH Streaming Files
&lt;/h3&gt;

&lt;p&gt;For adaptive bitrate streaming (HLS/DASH):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-s&lt;/span&gt; 1920x1080 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 5000k &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k stream_1080p.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-s&lt;/span&gt; 1280x720 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 2500k &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k stream_720p.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-s&lt;/span&gt; 854x480 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 1000k &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 96k stream_480p.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates three quality tiers from a single source—essential for professional streaming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Segment Video into Chunks
&lt;/h3&gt;

&lt;p&gt;Break a long video into smaller files (useful for uploading or processing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 00:05:00 &lt;span class="nt"&gt;-f&lt;/span&gt; segment segment_%03d.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates 5-minute chunks: &lt;code&gt;segment_000.mp4&lt;/code&gt;, &lt;code&gt;segment_001.mp4&lt;/code&gt;, etc.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mac:&lt;/strong&gt; &lt;code&gt;brew install ffmpeg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux (Ubuntu/Debian):&lt;/strong&gt; &lt;code&gt;sudo apt-get install ffmpeg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; Download from &lt;a href="https://ffmpeg.org/download.html" rel="noopener noreferrer"&gt;ffmpeg.org&lt;/a&gt; or use &lt;code&gt;choco install ffmpeg&lt;/code&gt; if you have Chocolatey&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hosting your FFmpeg jobs:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're encoding at scale, rent a &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; (CPU-optimized instance) or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; with multiple cores. A single good server can encode 10+ videos simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automating FFmpeg:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The real power emerges when you integrate FFmpeg into your workflows. If you're&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/ffmpeg-content-creators-cheatsheet/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>automation</category>
      <category>linux</category>
    </item>
    <item>
      <title>Automate Your Email Newsletter with n8n and Claude</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Mon, 20 Apr 2026 08:00:29 +0000</pubDate>
      <link>https://dev.to/chasebot/automate-your-email-newsletter-with-n8n-and-claude-2c5e</link>
      <guid>https://dev.to/chasebot/automate-your-email-newsletter-with-n8n-and-claude-2c5e</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for self-hosting&lt;/li&gt;
&lt;li&gt;Claude API key from Anthropic&lt;/li&gt;
&lt;li&gt;Gmail or SMTP credentials for sending&lt;/li&gt;
&lt;li&gt;A list of subscriber emails (spreadsheet or database)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why Automate Your Newsletter?&lt;/li&gt;
&lt;li&gt;Building the Core Workflow&lt;/li&gt;
&lt;li&gt;Integrating Claude for Content Generation&lt;/li&gt;
&lt;li&gt;Setting Up Email Distribution&lt;/li&gt;
&lt;li&gt;Testing and Scheduling&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Automate Your Newsletter?
&lt;/h2&gt;

&lt;p&gt;I've been running newsletters for three years now, and I can tell you: doing it manually kills momentum. Every week you're drafting, editing, formatting, and manually sending to your list. By week four, you've burned out. By month two, your subscribers are getting ghosted.&lt;/p&gt;

&lt;p&gt;Automation changed everything for me. Instead of spending 4-5 hours per week on my newsletter, I now spend 30 minutes setting up a workflow that does the heavy lifting. &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; handles the scheduling, &lt;a href="https://anthropic.com" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; generates smart content based on my topic prompts, and your email provider sends it out to thousands of people.&lt;/p&gt;

&lt;p&gt;The workflow I'm sharing with you today can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate newsletter content automatically using Claude's API&lt;/li&gt;
&lt;li&gt;Pull subscriber lists from Google Sheets or a database&lt;/li&gt;
&lt;li&gt;Send personalized emails at scale&lt;/li&gt;
&lt;li&gt;Log analytics so you know what worked&lt;/li&gt;
&lt;li&gt;Run on a schedule you define (weekly, daily, monthly—whatever)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Core Workflow
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; and create a new workflow. I'm calling mine "AI Newsletter Generator."&lt;/p&gt;

&lt;p&gt;The workflow structure is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt; – A schedule node fires weekly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate Content&lt;/strong&gt; – Claude API creates the newsletter body&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch Subscribers&lt;/strong&gt; – Pull your email list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send Emails&lt;/strong&gt; – Distribute to each subscriber&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log Results&lt;/strong&gt; – Track success/failures&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's start with the schedule trigger.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add a Schedule Trigger
&lt;/h3&gt;

&lt;p&gt;In n8n, click &lt;strong&gt;+&lt;/strong&gt; and search for "Schedule Trigger." Configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Every Week"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dayOfWeek"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Monday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"09:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fires every Monday at 9 AM. Adjust to your preference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Claude API Node
&lt;/h3&gt;

&lt;p&gt;Add a new node and search for &lt;strong&gt;HTTP Request&lt;/strong&gt; (we'll use it to call Claude's API directly since n8n doesn't have a native Claude node yet).&lt;/p&gt;

&lt;p&gt;Configure the HTTP node:&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.anthropic.com/v1/messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authentication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Generic Credential Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sendHeaders"&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;"headerParameters"&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;"x-api-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;"YOUR_CLAUDE_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"anthropic-version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-06-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content-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;"application/json"&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;"sendBody"&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;"bodyParameters"&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;"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;"claude-3-5-sonnet-20241022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"system"&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 newsletter writer. Create engaging, concise newsletter content based on the topic provided. Format with a catchy subject line, brief intro paragraph, 3 main points with explanations, and a call-to-action."&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;"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;"Write a newsletter about the latest trends in workflow automation and AI integration. Keep it under 300 words."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model&lt;/code&gt;: Claude 3.5 Sonnet is fast and cheap for this use case&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_tokens&lt;/code&gt;: 1024 gives you about 750 words&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;system&lt;/code&gt;: This prompt tells Claude how to format the newsletter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;messages&lt;/code&gt;: Your actual newsletter topic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response will look like:&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;"msg_123456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"message"&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="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;"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;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"Subject: Your Weekly Automation Digest&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Hi 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="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;We'll extract the &lt;code&gt;content[0].text&lt;/code&gt; in the next step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Integrating Claude for Content Generation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 3: Parse Claude Response
&lt;/h3&gt;

&lt;p&gt;Add a &lt;strong&gt;Set Node&lt;/strong&gt; to extract and format the Claude output:&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;"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="nl"&gt;"newsletterContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $nodes['HTTP Request'].data.body.content[0].text }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"generatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ now.toIso() }}"&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;"generated"&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;This pulls the text Claude generated and adds metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Split Subject and Body
&lt;/h3&gt;

&lt;p&gt;Claude will return a formatted newsletter with a subject line, but we need to split them. Add another &lt;strong&gt;Function Node&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newsletterContent&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;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&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;body&lt;/span&gt; &lt;span class="o"&gt;=&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;subjectIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subject:&lt;/span&gt;&lt;span class="dl"&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;subjectIndex&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;subjectIndex&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subject:&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subjectIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Weekly Newsletter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&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;subject&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="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="na"&gt;generatedAt&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="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;Now we have a clean subject and body ready to email.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Email Distribution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 5: Fetch Subscriber List
&lt;/h3&gt;

&lt;p&gt;This is where many people use Google Sheets as a free database. If you haven't already, &lt;a href="https://dev.to/blog/google-sheets-database-n8n/"&gt;check out how to use Google Sheets as a free database with n8n&lt;/a&gt; for more details.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Google Sheets&lt;/strong&gt; node:&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;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Get All Rows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spreadsheetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SHEET_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sheetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscribers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"headerRow"&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;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;Your sheet should have columns: &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;. Filter for &lt;code&gt;status = active&lt;/code&gt; to avoid sending to unsubscribed users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Loop and Send Emails
&lt;/h3&gt;

&lt;p&gt;Add a &lt;strong&gt;Loop&lt;/strong&gt; node to iterate through each subscriber. Inside the loop, add an &lt;strong&gt;SMTP Email&lt;/strong&gt; node (or Gmail, depending on your preference):&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SMTP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fromEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-email@yourcompany.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;"fromName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Newsletter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"toEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $item.json.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;"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;"{{ $nodes['Set'].data.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;"textMimeType"&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/html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"htmlBody"&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;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h2&amp;gt;{{ $item.json.name }}, here's this week's update:&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;{{ $nodes['Set'].data.body }}&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&amp;lt;a href='https://yourcompany.com/unsubscribe?email={{ $item.json.email }}'&amp;gt;Unsubscribe&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&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;"smtpCredentials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SMTP_CREDENTIAL"&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;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;$item.json.email&lt;/code&gt; to address each subscriber&lt;/li&gt;
&lt;li&gt;Personalize with &lt;code&gt;$item.json.name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Include an unsubscribe link (legal requirement under CAN-SPAM)&lt;/li&gt;
&lt;li&gt;Use HTML for better formatting&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 7: Error Handling
&lt;/h3&gt;

&lt;p&gt;Add an &lt;strong&gt;Error Handler&lt;/strong&gt; node to catch failed sends:&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;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Continue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"onError"&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;"errorOutput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $item.json.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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $error.message }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ now.toIso() }}"&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;This logs failures without stopping the entire workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing and Scheduling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 8: Test with One Subscriber
&lt;/h3&gt;

&lt;p&gt;Before going live, test with yourself. Modify your Google Sheet to have just one test email. Click &lt;strong&gt;Execute&lt;/strong&gt; in n8n and check your inbox.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Subject line is correct&lt;/li&gt;
&lt;li&gt;✅ Body formatting is clean&lt;/li&gt;
&lt;li&gt;✅ Links work&lt;/li&gt;
&lt;li&gt;✅ No HTML escaping issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Claude generates weird formatting, adjust the system prompt. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"system"&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 newsletter writer. Output ONLY HTML. Start with &amp;lt;h1&amp;gt;Subject: [subject]&amp;lt;/h1&amp;gt;. Do not include markdown or asterisks."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 9: Production Scheduling
&lt;/h3&gt;

&lt;p&gt;Once you're confident, update the Schedule Trigger to your real frequency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Every Week"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dayOfWeek"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Monday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"America/New_York"&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;n8n will run this automatically every Monday at 9 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10: Add Analytics (Optional but Recommended)
&lt;/h3&gt;

&lt;p&gt;Create a second Google Sheet called "newsletter_logs" and add a &lt;strong&gt;Google Sheets&lt;/strong&gt; node at the end of your workflow:&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;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Append"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spreadsheetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SHEET_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sheetName"&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_logs"&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="nl"&gt;"date_sent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ now.toIso() }}"&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;"{{ $nodes['Set'].data.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;"recipients_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $nodes['Loop'].data.length }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"failed_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $nodes['Error Handler'].data.length }}"&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;"completed"&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;This tracks every send so you can spot patterns (e.g., "Mondays at 9 AM get better open rates").&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;You're ready to launch. Here's your checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get n8n&lt;/strong&gt;: Sign up for &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-host on &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; / &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up Claude API&lt;/strong&gt;: Get your key from Anthropic (free tier gives you $5 credit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepare your subscriber list&lt;/strong&gt;: Upload to Google Sheets with email and name columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the workflow&lt;/strong&gt;: Follow the steps above&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt;: Send one email to yourself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt;: Set the schedule and let it run&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few advanced tweaks you might consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;a href="https://dev.to/blog/best-free-apis-automation/"&gt;the best free APIs for building automation workflows&lt;/a&gt; to your newsletter—pull trending tech news and include it in your Claude prompt&lt;/li&gt;
&lt;li&gt;Use Zapier or Make.com for simple integrations, but n8n is more powerful and costs less at scale&lt;/li&gt;
&lt;li&gt;If you want to add audio versions, &lt;a href="https://dev.to/blog/edge-tts-free-guide/"&gt;check out Edge TTS for free text-to-speech&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Outsource Your Automation
&lt;/h2&gt;

&lt;p&gt;Don't have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hire me on Fiverr&lt;/a&gt; — mention &lt;strong&gt;SYS3-DEVTO&lt;/strong&gt; for priority. Or DM at &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;chasebot.online&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/email-newsletter-automation-n8n/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>email</category>
      <category>newsletter</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Use Google Sheets as a Free Database with n8n</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Sun, 19 Apr 2026 08:00:28 +0000</pubDate>
      <link>https://dev.to/chasebot/how-to-use-google-sheets-as-a-free-database-with-n8n-5339</link>
      <guid>https://dev.to/chasebot/how-to-use-google-sheets-as-a-free-database-with-n8n-5339</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; for self-hosting&lt;/li&gt;
&lt;li&gt;A Google account with Google Sheets access&lt;/li&gt;
&lt;li&gt;Basic familiarity with n8n (optional—I'll walk you through it)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why Google Sheets as a Database?&lt;/li&gt;
&lt;li&gt;Setting Up Google Sheets Credentials in n8n&lt;/li&gt;
&lt;li&gt;Building Your First Read Workflow&lt;/li&gt;
&lt;li&gt;Writing Data to Sheets&lt;/li&gt;
&lt;li&gt;Advanced: Multi-Sheet Workflows and Error Handling&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Google Sheets as a Database?
&lt;/h2&gt;

&lt;p&gt;I started using Google Sheets as a database when I was bootstrapping projects and couldn't justify $50/month for a traditional database. Honestly? It works incredibly well for small to medium workflows.&lt;/p&gt;

&lt;p&gt;Here's why I love it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free and unlimited rows.&lt;/strong&gt; Google Sheets gives you enough space for thousands of records without paying a cent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built-in collaboration.&lt;/strong&gt; Multiple team members can view and comment on data in real-time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy backups.&lt;/strong&gt; Google handles versioning automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero setup.&lt;/strong&gt; No Docker containers, no PostgreSQL configuration, no DevOps headaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native n8n integration.&lt;/strong&gt; The &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;Google Sheets node in n8n&lt;/a&gt; is rock-solid and handles authentication seamlessly.&lt;/p&gt;

&lt;p&gt;The downside? It's slower than a true database at scale (10,000+ rows), but for workflows under 5,000 records, you won't notice a difference. I've actually built workflows that &lt;a href="https://dev.to/blog/n8n-replace-saas-tools/"&gt;replace $200/month in SaaS tools&lt;/a&gt; using nothing but Google Sheets and n8n.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Google Sheets Credentials in n8n
&lt;/h2&gt;

&lt;p&gt;Before you write a single workflow, you need to authenticate Google Sheets in &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create a new workflow in n8n&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Log into your n8n account and click "New Workflow."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Add a Google Sheets node&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search for "Google Sheets" in the node panel and drag it onto your canvas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Authenticate with Google&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click the "Authenticate" button (the credential field). A popup will appear asking for permission. Sign in with your Google account and authorize n8n to access your Google Sheets.&lt;/p&gt;

&lt;p&gt;Here's what the credential connection looks like in the node configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authentication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oAuth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_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;"YOUR_GOOGLE_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_GOOGLE_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://n8n.yourinstance.com/oauth-callback"&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;If you're self-hosting n8n on a &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/do?utm_source=system3_devto" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;, you'll need to set up OAuth credentials in the Google Cloud Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;console.cloud.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create a new project called "n8n Workflows"&lt;/li&gt;
&lt;li&gt;Enable the &lt;strong&gt;Google Sheets API&lt;/strong&gt; and &lt;strong&gt;Google Drive API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Create an OAuth 2.0 credential (Application type: Web application)&lt;/li&gt;
&lt;li&gt;Set authorized redirect URI to &lt;code&gt;https://your-n8n-domain.com/oauth-callback&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy the Client ID and Secret into n8n&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. You're ready to read and write data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Your First Read Workflow
&lt;/h2&gt;

&lt;p&gt;Let me show you how to read data from a Google Sheet. I'm assuming you have a spreadsheet already created—if not, create one and add some test data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sheet name: &lt;code&gt;Contacts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Columns: &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, &lt;code&gt;Phone&lt;/code&gt;, &lt;code&gt;Status&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a simple workflow that reads all rows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Add a Google Sheets node set to "Read"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the node configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operation: Read
Spreadsheet: Select your spreadsheet from the dropdown
Sheet: Contacts
Read from: A1 (top-left corner)
First Row as Headers: Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Add a function node to transform the data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This optional node cleans up the response and makes it easier to work with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;return&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;item&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;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&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;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&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;Phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&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;Status&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;&lt;strong&gt;Step 3: Test and execute&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click "Execute" and you'll see the data returned as a JSON array. Each row from your sheet becomes an object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"Alice Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@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;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"555-0101"&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;"Active"&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;"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;"Bob Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob@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;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"555-0102"&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;"Inactive"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You're now reading from Google Sheets inside n8n.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Writing Data to Sheets
&lt;/h2&gt;

&lt;p&gt;Reading is half the battle. Here's how to write new records back to your spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; You're capturing form submissions via webhook and need to log them to Google Sheets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Add a Webhook trigger&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Method: POST
Path: /form-submission
Authentication: None (for testing; add auth in production)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Add a Google Sheets node set to "Append"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configure it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operation: Append
Spreadsheet: Your spreadsheet
Sheet: Contacts
Columns to Insert: Name, Email, Phone, Status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Map the incoming webhook data to sheet columns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the "Values" section, reference the webhook body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json.body.name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;Email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json.body.email&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;Phone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json.body.phone&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Test with a POST request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use curl or Postman:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-n8n-instance.com/webhook/form-submission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "Charlie Brown",
    "email": "charlie@example.com",
    "phone": "555-0103"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your Google Sheet—the new row will appear instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to update an existing row instead of appending?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the "Update" operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operation: Update
Spreadsheet: Your spreadsheet
Sheet: Contacts
Key: Email (the column that identifies the row)
Values: Update Name, Status, Phone as needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Advanced: Multi-Sheet Workflows and Error Handling
&lt;/h2&gt;

&lt;p&gt;Once you master basic read/write, you can build more sophisticated workflows. Here's a real-world example: syncing leads from a webhook to multiple sheets based on their source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Webhook trigger&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Accepts incoming lead data:&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;"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;"Diana Prince"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"diana@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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"facebook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5000"&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;Step 2: Route based on source&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add an "If" node to branch the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Condition: source == "facebook"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Append to the appropriate sheet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If source is "facebook," append to a "Facebook Leads" sheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Append&lt;/span&gt;
&lt;span class="na"&gt;Spreadsheet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Your spreadsheet&lt;/span&gt;
&lt;span class="na"&gt;Sheet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Facebook Leads&lt;/span&gt;
&lt;span class="na"&gt;Columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Name, Email, Budget, DateReceived&lt;/span&gt;
&lt;span class="na"&gt;Values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json.body.name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;Email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json.body.body.email&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;Budget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$json.body.budget&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;DateReceived&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;new Date().toISOString()&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, append to a "Other Leads" sheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Add error handling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wrap the Google Sheets operations in a "Try-Catch" node to gracefully handle failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Try: Google Sheets - Append node
Catch: Send notification if append fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch block might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;message&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 to append lead to Google Sheets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;details&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lead&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;body&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could then send this to a Slack channel or email for manual review. If you want to monitor failures across all your workflows, I'd recommend setting up &lt;a href="https://dev.to/blog/vps-monitoring-n8n/"&gt;VPS health check workflows&lt;/a&gt; that alert you to any repeated errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advanced Tip: Filtering and Conditional Writes
&lt;/h2&gt;

&lt;p&gt;Here's a workflow pattern I use constantly: only write data to Google Sheets if it meets certain criteria.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Add a filter node after webhook trigger&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;Condition: email contains "@company.com" AND budget &amp;gt; 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures only qualified leads get recorded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Append only if condition is true&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Google Sheets node only runs if the filter passes. Non-matching records can go to a separate sheet or be logged elsewhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Issues and Solutions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Issue: "Spreadsheet not found"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure the Google account you authenticated with actually has access to the spreadsheet. Also verify the spreadsheet hasn't been moved to a shared drive (those require different permissions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue: "Sheet name invalid"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google Sheets node is case-sensitive. If your sheet is called "Contacts" but you're searching for "contacts," it will fail. Copy-paste the exact sheet name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue: Rate limiting (too many reads/writes per minute)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google Sheets has soft rate limits (~100 requests per minute per user). If you're doing bulk operations, add a 1-second delay between rows:&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;Add&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="nx"&gt;between&lt;/span&gt; &lt;span class="nx"&gt;Google&lt;/span&gt; &lt;span class="nx"&gt;Sheets&lt;/span&gt; &lt;span class="nx"&gt;reads&lt;/span&gt;
&lt;span class="nx"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="mi"&gt;1000&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;&lt;strong&gt;Issue: Need to read only specific columns?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the "Read Range" operation instead of "Read":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Range: A1:D100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only reads columns A through D, rows 1 to 100.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;You're ready to build. Here's your launch checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign up for &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt;&lt;/strong&gt; (free tier includes 1,000 executions/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create your first Google Sheet&lt;/strong&gt; and populate it with test data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a Google Sheets node&lt;/strong&gt; and authenticate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a simple read workflow&lt;/strong&gt; to verify everything works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a webhook trigger&lt;/strong&gt; and set up an append operation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with sample data&lt;/strong&gt; before connecting to production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Need hosting? &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; offer affordable servers if you want to self-host n8n instead of using the cloud version.&lt;/p&gt;




&lt;h3&gt;
  
  
  Outsource Your Automation
&lt;/h3&gt;

&lt;p&gt;Don't have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hire me on Fiverr&lt;/a&gt; — mention SYS3-DEVTO for priority. Or DM at &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;chasebot.online&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/google-sheets-database-n8n/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>googlesheets</category>
      <category>database</category>
      <category>automation</category>
    </item>
    <item>
      <title>Edge TTS: The Free Text-to-Speech Engine Nobody Talks About</title>
      <dc:creator>Raizan</dc:creator>
      <pubDate>Sat, 18 Apr 2026 08:00:39 +0000</pubDate>
      <link>https://dev.to/chasebot/edge-tts-the-free-text-to-speech-engine-nobody-talks-about-1gj6</link>
      <guid>https://dev.to/chasebot/edge-tts-the-free-text-to-speech-engine-nobody-talks-about-1gj6</guid>
      <description>&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or self-hosted n8n instance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt; or &lt;a href="https://chasebot.online/go/contabo?utm_source=system3_devto" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; if self-hosting&lt;/li&gt;
&lt;li&gt;Python 3.8+ installed locally or on your server&lt;/li&gt;
&lt;li&gt;Basic command-line familiarity&lt;/li&gt;
&lt;li&gt;A text file or API endpoint with content to synthesize&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What Is Edge TTS and Why Should You Care?&lt;/li&gt;
&lt;li&gt;Setting Up Edge TTS on Your Machine&lt;/li&gt;
&lt;li&gt;Integrating Edge TTS with n8n&lt;/li&gt;
&lt;li&gt;Building a Production Workflow&lt;/li&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is Edge TTS and Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;I discovered Edge TTS completely by accident while hunting for a free text-to-speech solution that didn't require expensive API keys or sketchy third-party services. What I found was Microsoft's hidden gem—a legitimate TTS engine that powers Microsoft Edge's built-in reader functionality.&lt;/p&gt;

&lt;p&gt;Edge TTS is the text-to-speech engine that runs inside Microsoft Edge's "Read aloud" feature. It's been available for years, but almost nobody talks about it because Microsoft doesn't officially document it as a public API. The community reverse-engineered it, wrapped it in Python libraries, and now you can use it for free without authentication, rate limits, or corporate approval.&lt;/p&gt;

&lt;p&gt;Here's what makes it genuinely special:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Natural voice quality.&lt;/strong&gt; Edge TTS produces audio that sounds like actual humans, not the robotic Stephen-Hawking-era synth voices you'd expect from free tools. I've tested dozens of voices across 50+ languages, and the clarity is genuinely impressive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No API key required.&lt;/strong&gt; Unlike Google Cloud TTS (pay-as-you-go), AWS Polly (AWS account mandatory), or OpenAI's TTS, Edge TTS asks for nothing. No authentication. No billing. No rate-limit emails at 3 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;200+ voice options.&lt;/strong&gt; Microsoft has trained models for various languages, accents, and genders. Want a British female voice? Australian male? Korean child voice? They're all there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast processing.&lt;/strong&gt; On a decent internet connection, you'll get audio files in seconds, not minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for automation.&lt;/strong&gt; This is where I use it most—building workflows that convert article text to audio, generate YouTube video voiceovers, create multilingual bot responses, or produce accessibility-friendly content automatically.&lt;/p&gt;

&lt;p&gt;The catch? It's technically against Microsoft's terms of service (they say it's for Edge's reader feature only), but the community-maintained Python libraries have been stable for three years without takedown notices. If you're building commercial products, be aware of that risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Edge TTS on Your Machine
&lt;/h2&gt;

&lt;p&gt;Let me walk you through the simplest possible setup.&lt;/p&gt;

&lt;p&gt;First, install the edge-tts Python package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;edge-tts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You're done with setup. No credentials, no config files, no environment variables to juggle.&lt;/p&gt;

&lt;p&gt;Let's test it immediately with a simple command-line call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;edge-tts &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"Hello, this is Edge TTS speaking"&lt;/span&gt; &lt;span class="nt"&gt;--write-media&lt;/span&gt; test_output.mp3 &lt;span class="nt"&gt;--voice&lt;/span&gt; en-US-AriaNeural
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Takes your text string&lt;/li&gt;
&lt;li&gt;Downloads the MP3 audio file&lt;/li&gt;
&lt;li&gt;Saves it as &lt;code&gt;test_output.mp3&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check your current directory—you'll see a fresh MP3 file. Play it. The voice quality will shock you if you've been using free TTS before.&lt;/p&gt;

&lt;p&gt;Now let's list all available voices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;edge-tts &lt;span class="nt"&gt;--list-voices&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: af-ZA-AdriNeural
Name: af-ZA-WillemNeural
Name: am-ET-AmehaNeural
Name: am-ET-MekdesNeural
Name: ar-AE-FatimaNeural
Name: ar-AE-MohamedNeural
...and 200+ more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick a voice that matches your use case. For English, I usually default to &lt;code&gt;en-US-AriaNeural&lt;/code&gt; (neutral American female) or &lt;code&gt;en-GB-SoniaNeural&lt;/code&gt; (British female), but experiment.&lt;/p&gt;

&lt;p&gt;Now let's write actual Python code to integrate this into your workflows:&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;edge_tts&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text_to_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US-AriaNeural&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&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="n"&gt;communicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;edge_tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Communicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output_file&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;text_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome to the world of automated audio generation. This is completely free and requires no API keys.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;welcome_audio.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;voice_choice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US-AriaNeural&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&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;text_to_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_choice&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Audio saved to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&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;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python tts_script.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll have a professional-quality MP3 in seconds.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;rate&lt;/code&gt; parameter controls speech speed. Use &lt;code&gt;-50&lt;/code&gt; for slower speech, &lt;code&gt;0&lt;/code&gt; for normal, &lt;code&gt;50&lt;/code&gt; for faster. Handy for accessibility or for creating background narration that matches video pacing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integrating Edge TTS with n8n
&lt;/h2&gt;

&lt;p&gt;This is where automation gets powerful. If you're running &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or a self-hosted instance on &lt;a href="https://chasebot.online/go/hetzner?utm_source=system3_devto" rel="noopener noreferrer"&gt;Hetzner VPS&lt;/a&gt;, you can trigger TTS generation from any workflow.&lt;/p&gt;

&lt;p&gt;Create a new workflow in &lt;a href="https://chasebot.online/go/n8n?utm_source=system3_devto" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; and add an "Execute Command" node (or "Run Script" if you're using the Python node in a self-hosted setup).&lt;/p&gt;

&lt;p&gt;Here's the configuration for a simple HTTP-triggered workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 1: HTTP Request trigger&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set up a webhook that accepts POST requests with a JSON body containing your text and voice preference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 2: Function node (execute Python)&lt;/strong&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;edge_tts&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;output_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;voice_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_output.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;communicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;edge_tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Communicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;voice_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;audio_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;audio_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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="n"&gt;audio_base64&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;voice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voice&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US-AriaNeural&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;audio_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generate_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&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;audio_base64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;audio_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;success&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;&lt;strong&gt;Node 3: Send the response back&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use an HTTP Response node to return the base64-encoded audio or save it to cloud storage.&lt;/p&gt;

&lt;p&gt;When you trigger this workflow via HTTP POST:&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 http://your-n8n-instance/webhook/tts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "text": "This workflow generated this audio automatically",
    "voice": "en-GB-SoniaNeural"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get back a response with the audio file encoded in base64, ready to play, embed, or store.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Fast-Track Your Project:&lt;/strong&gt; Don't want to configure this yourself? &lt;a href="https://chasebot.online?utm_source=system3_devto" rel="noopener noreferrer"&gt;I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Building a Production Workflow
&lt;/h2&gt;

&lt;p&gt;Let me show you a real-world example: a workflow that converts blog posts into audio summaries and uploads them to cloud storage.&lt;/p&gt;

&lt;p&gt;I'll build this with multiple steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Fetch blog post content (HTTP Request node)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.example.com/posts/latest
Authentication: Bearer YOUR_API_TOKEN
Response format: JSON with "title" and "content" fields
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Summarize the content (optional—use OpenAI or similar)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your blog post is 3,000 words, you might want to summarize it to 500 words before generating audio (shorter file, faster generation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Generate audio with Edge TTS (Function node)&lt;/strong&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;edge_tts&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_podcast_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_choice&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y%m%d_%H%M%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/podcast_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;full_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post_title&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;post_summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;communicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;edge_tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Communicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;voice_choice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;file_size&lt;/span&gt; &lt;span class="o"&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getsize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&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;file_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_estimate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;150&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;json&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="n"&gt;title&lt;/span&gt;
&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;json&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="n"&gt;content&lt;/span&gt;
&lt;span class="n"&gt;voice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US-AriaNeural&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_podcast_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Upload to AWS S3 (or any cloud storage)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use n8n's built-in S3 node:&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;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-bucket-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;"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;"podcasts/{{ $node['Function'].data.timestamp }}.mp3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $node['Function'].data.file_path }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"acl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public-read"&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;Step 5: Send notification (Slack, Email, or Webhook)&lt;/strong&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;"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;"New podcast audio generated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocks"&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;"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;"section"&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="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;"mrkdwn"&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;"*Blog Audio Generated*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Title:* {{ $node['HTTP Request'].data.title }}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*File Size:* {{ $node['Function'].data.file_size }} bytes&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Listen:* https://your-bucket.s3.amazonaws.com/podcasts/{{ $node['Function'].data.timestamp }}.mp3"&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;This workflow runs on a schedule or webhook trigger, converts your latest blog post to audio, uploads it, and notifies your team—all without a single manual step.&lt;/p&gt;

&lt;p&gt;If you want to go further, combine this with the techniques in &lt;a href="https://dev.to/blog/vps-monitoring-n8n/"&gt;How to Monitor Your VPS with n8n Health Check Workflows&lt;/a&gt; to ensure your audio generation service stays healthy, or use &lt;a href="https://dev.to/blog/telegram-bot-n8n-no-code/"&gt;How to Build a Telegram Bot with n8n (No Code Required)&lt;/a&gt; to let users request audio generation on-demand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advanced: Handling Large-Scale Audio Generation
&lt;/h2&gt;

&lt;p&gt;If you're generating hundreds of audio files, edge-tts can hit rate limits (Microsoft's servers will temporarily block you). Here's a production-ready approach:&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;edge_tts&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_audio_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;voice&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;delay_seconds&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;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_list&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;output_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/batch_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;communicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;edge_tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Communicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://chasebot.online/blog/edge-tts-free-guide/?utm_source=system3_devto" rel="noopener noreferrer"&gt;Automation Insider&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tts</category>
      <category>python</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
