<?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: Luke Sandelands</title>
    <description>The latest articles on DEV Community by Luke Sandelands (@stack_architect).</description>
    <link>https://dev.to/stack_architect</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047278%2Ff036e3b8-37c8-482e-80e3-beac221349f9.png</url>
      <title>DEV Community: Luke Sandelands</title>
      <link>https://dev.to/stack_architect</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stack_architect"/>
    <language>en</language>
    <item>
      <title>6 Reasons Your Meta Conversions API Events Don't Match (and How to Check Yours)</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Tue, 29 Sep 2026 15:12:07 +0000</pubDate>
      <link>https://dev.to/stack_architect/6-reasons-your-meta-conversions-api-events-dont-match-and-how-to-check-yours-3ldo</link>
      <guid>https://dev.to/stack_architect/6-reasons-your-meta-conversions-api-events-dont-match-and-how-to-check-yours-3ldo</guid>
      <description>&lt;p&gt;Server-side tracking is meant to fix the conversions a browser pixel loses. But a Conversions API event only helps if Meta can match it to a person, and a lot of setups send events that are accepted, counted as received, and then do much less than they should.&lt;/p&gt;

&lt;p&gt;I build server-side tracking for Shopify stores, and the same six mistakes come up again and again. Every rule below comes from Meta's own &lt;a href="https://developers.facebook.com/documentation/ads-commerce/conversions-api/parameters" rel="noopener noreferrer"&gt;Conversions API parameter documentation&lt;/a&gt;, read on 28 September 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;event_time&lt;/code&gt; is in milliseconds
&lt;/h2&gt;

&lt;p&gt;Meta wants a Unix timestamp in &lt;strong&gt;seconds&lt;/strong&gt;. JavaScript's &lt;code&gt;Date.now()&lt;/code&gt; returns &lt;strong&gt;milliseconds&lt;/strong&gt;, so it's the single most common bug:&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;event_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                    &lt;span class="c1"&gt;// wrong: 1727600000000&lt;/span&gt;
&lt;span class="nx"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// right: 1727600000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick check: a timestamp in seconds has 10 digits. Thirteen digits means milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Customer data is hashed before it's formatted
&lt;/h2&gt;

&lt;p&gt;Meta requires email, phone, name, city, state, zip and country to be SHA-256 hashed. What catches people out is that Meta matches on the hash of the &lt;strong&gt;formatted&lt;/strong&gt; value, and hashing doesn't forgive formatting:&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="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Buyer@Example.com &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;buyer@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are valid-looking 64-character hashes, so nothing flags the first one as wrong. Format first, then hash. Meta's rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Format before hashing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;em&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trim spaces, lowercase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Digits only, no leading zeros, country code included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fn&lt;/code&gt;, &lt;code&gt;ln&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lowercase, no punctuation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowercase, no punctuation, special characters or spaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;st&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowercase; US states as the 2-letter code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowercase, no spaces or dashes; first 5 digits for US zips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowercase ISO 3166-1 alpha-2, such as &lt;code&gt;gb&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The UK phone trap.&lt;/strong&gt; &lt;code&gt;+44 (0)7700 900123&lt;/code&gt; becomes &lt;code&gt;4407700900123&lt;/code&gt; if you only strip symbols and leading zeros. The zero after the country code is a national prefix that isn't dialled internationally, so the number should be &lt;code&gt;447700900123&lt;/code&gt;. If your store sells in the UK, remove it before hashing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Fields that must stay raw get hashed too
&lt;/h2&gt;

&lt;p&gt;It's tempting to hash everything "to be safe". Meta says &lt;strong&gt;not&lt;/strong&gt; to hash four fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client_ip_address&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_user_agent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fbc&lt;/code&gt; (the click ID cookie)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fbp&lt;/code&gt; (the browser ID cookie)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meta uses these as they are. A hashed IP address is just a meaningless string.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Website events are missing the page URL or user agent
&lt;/h2&gt;

&lt;p&gt;For events with &lt;code&gt;action_source: "website"&lt;/code&gt;, Meta requires both &lt;code&gt;event_source_url&lt;/code&gt; and &lt;code&gt;user_data.client_user_agent&lt;/code&gt;. Server-side setups often drop them, because the server never sees the browser. The fix is to capture both in the browser at checkout and pass them to your server with the order.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;action_source&lt;/code&gt; itself is required on every event, as is &lt;code&gt;user_data&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;event_id&lt;/code&gt; is missing, or doesn't match the Pixel
&lt;/h2&gt;

&lt;p&gt;Meta marks &lt;code&gt;event_id&lt;/code&gt; as optional, but recommends it for deduplication. If the browser Pixel and your server both send the same purchase, give them the same ID so Meta can treat them as one event:&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;// Server&lt;/span&gt;
&lt;span class="nx"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`order_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="c1"&gt;// Browser Pixel, same purchase&lt;/span&gt;
&lt;span class="nf"&gt;fbq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;track&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;Purchase&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;eventID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`order_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a shared ID, you're relying on Meta to work out that two events are the same purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Old events are retried in a batch
&lt;/h2&gt;

&lt;p&gt;If any event in a request is more than 7 days old, Meta returns an error for the &lt;strong&gt;whole request&lt;/strong&gt; and processes none of its events. A retry queue that keeps resending one stale event can take good events down with it. Drop anything older than 7 days before it reaches the batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Purchase event that gets all six right
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&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;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&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="nf"&gt;toLowerCase&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;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&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;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^0+/&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="c1"&gt;// must already include the country code&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;event_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;Purchase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;// 1. seconds&lt;/span&gt;
  &lt;span class="na"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`order_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// 5. same ID as the Pixel&lt;/span&gt;
  &lt;span class="na"&gt;action_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;website&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;event_source_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// 4. captured in the browser&lt;/span&gt;
  &lt;span class="na"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;em&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;               &lt;span class="c1"&gt;// 2. formatted, then hashed&lt;/span&gt;
    &lt;span class="na"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
    &lt;span class="na"&gt;client_ip_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientIp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// 3. raw&lt;/span&gt;
    &lt;span class="na"&gt;client_user_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// 3. raw, and 4. required&lt;/span&gt;
    &lt;span class="nx"&gt;fbp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                            &lt;span class="c1"&gt;// 3. raw&lt;/span&gt;
    &lt;span class="nx"&gt;fbc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                            &lt;span class="c1"&gt;// 3. raw&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;custom_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                     &lt;span class="c1"&gt;// required for Purchase&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// required for Purchase&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check a payload in 10 seconds
&lt;/h2&gt;

&lt;p&gt;I turned these checks into two free tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In the browser:&lt;/strong&gt; paste an event into the &lt;a href="https://stackarchitect.xyz/meta-capi-payload-validator/" rel="noopener noreferrer"&gt;Meta Conversions API payload validator&lt;/a&gt;. It runs locally, so nothing you paste is sent anywhere, and it has a formatter that applies Meta's rules and hashes the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the terminal or CI:&lt;/strong&gt; the open-source &lt;a href="https://github.com/lsb11/shopify-capi-validator" rel="noopener noreferrer"&gt;shopify-capi-validator&lt;/a&gt; package runs the same checks and exits with code 1 on any failure, so it can block a bad deploy.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shopify-capi-validator &lt;span class="nt"&gt;--payload&lt;/span&gt; ./event.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're sending Shopify orders server-side for the first time, the &lt;a href="https://stackarchitect.xyz/blog/shopify-server-side-tracking-complete-setup-guide/" rel="noopener noreferrer"&gt;server-side tracking setup guide&lt;/a&gt; covers the whole flow, and the &lt;a href="https://stackarchitect.xyz/meta-emq-score-estimator/" rel="noopener noreferrer"&gt;EMQ score estimator&lt;/a&gt; shows what the customer data you send means for match quality.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
      <category>shopify</category>
    </item>
    <item>
      <title>Shopify to GA4 without GTM: native events plus a server-side Measurement Protocol fallback</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Sat, 26 Sep 2026 10:02:43 +0000</pubDate>
      <link>https://dev.to/stack_architect/shopify-to-ga4-without-gtm-native-events-plus-a-server-side-measurement-protocol-fallback-2g25</link>
      <guid>https://dev.to/stack_architect/shopify-to-ga4-without-gtm-native-events-plus-a-server-side-measurement-protocol-fallback-2g25</guid>
      <description>&lt;p&gt;Google Analytics 4 installs on Shopify free through the official Google and YouTube sales channel, which handles browser-side event tracking without code or Google Tag Manager. Because browser events are lost to iOS and ad blockers, a Make.com webhook sending purchase events server-side completes the picture. Total setup takes under 20 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I set up Google Analytics 4 on Shopify for free?
&lt;/h3&gt;

&lt;p&gt;Install the Google &amp;amp; YouTube sales channel from the Shopify App Store (free) and connect your GA4 property during setup. Shopify automatically sends key ecommerce events (page_view, view_item, add_to_cart, begin_checkout, and purchase) to GA4 without any additional configuration. For more complete purchase data recovery, add a Make.com webhook that sends server-side purchase events to GA4's Measurement Protocol. The entire setup is free and requires no code or Google Tag Manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Google Tag Manager to connect Shopify to GA4?
&lt;/h3&gt;

&lt;p&gt;No. Shopify's native Google &amp;amp; YouTube sales channel connects directly to GA4 without Google Tag Manager. GTM is an option for stores that need custom event tracking beyond Shopify's standard ecommerce events, but for the standard purchase, add-to-cart, and checkout events that most stores need, the native integration is sufficient and requires no technical setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  What events does Shopify send to GA4 automatically?
&lt;/h3&gt;

&lt;p&gt;When connected via the Google &amp;amp; YouTube sales channel, Shopify automatically sends the following GA4 events: page_view (every page load), view_item (product page views), add_to_cart (cart additions), begin_checkout (checkout started), add_payment_info (payment step), and purchase (order completed). These cover the complete ecommerce funnel without any additional configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my GA4 showing fewer purchases than Shopify?
&lt;/h3&gt;

&lt;p&gt;GA4 purchase events are sent by the browser-based Shopify pixel, which is subject to iOS tracking restrictions, ad blockers, and cookie limitations. Stores with significant iOS traffic, users of privacy browsers, and customers using ad blockers will show fewer purchases in GA4 than Shopify actually recorded. The server-side fix is Make.com sending purchase events to GA4's Measurement Protocol directly: bypassing browser restrictions entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Google Analytics 4 free for Shopify stores?
&lt;/h3&gt;

&lt;p&gt;Yes. Google Analytics 4 is permanently free for standard use. There are no tier limits on the free version that would affect normal Shopify store analytics. Google Analytics 360 (the paid enterprise version) adds higher sampling thresholds and SLA guarantees: relevant only for extremely high-traffic stores. For the vast majority of Shopify stores, GA4 free provides complete analytics at no cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between GA4 and Google Ads conversion tracking for Shopify?
&lt;/h3&gt;

&lt;p&gt;GA4 is an analytics platform that tracks user behaviour across your entire Shopify store (sessions, page views, product views, cart events, and purchases) for analysis and reporting. Google Ads conversion tracking is specifically for attributing purchases back to Google Ads clicks to measure campaign performance. They are separate systems. GA4 can be linked to Google Ads to import key events as conversions, but setting them up separately ensures each works correctly. This guide covers GA4; the Google Ads conversion tracking setup is covered in a separate guide.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to set up GA4 on Shopify?
&lt;/h3&gt;

&lt;p&gt;The native GA4 setup via the Google &amp;amp; YouTube sales channel takes approximately 5 minutes. The additional server-side Make.com layer for complete purchase data recovery takes approximately 10 minutes. The full setup (native layer plus server-side layer plus verification) is complete in under 20 minutes with no technical skills required.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/blog/shopify-google-analytics-4-setup-free-2026/" rel="noopener noreferrer"&gt;Set Up Google Analytics 4 on Shopify Free 2026 — No Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>analytics</category>
      <category>webdev</category>
      <category>automation</category>
    </item>
    <item>
      <title>I replaced $700/month of SaaS with Google Apps Script. Here's what broke first.</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:06:55 +0000</pubDate>
      <link>https://dev.to/stack_architect/i-replaced-700month-of-saas-with-google-apps-script-heres-what-broke-first-4pa6</link>
      <guid>https://dev.to/stack_architect/i-replaced-700month-of-saas-with-google-apps-script-heres-what-broke-first-4pa6</guid>
      <description>&lt;p&gt;A while back I looked at a stack of SaaS subscriptions that were, between them, doing something fairly unglamorous: moving rows of data from one system into another and doing arithmetic on them. The combined bill was around $700 a month.&lt;/p&gt;

&lt;p&gt;So I rewrote the lot in Google Apps Script and a webhook layer. It worked immediately, which was the problem — it worked at ten events a day and quietly stopped working at four hundred, in ways that produced no error email and no obvious symptom.&lt;/p&gt;

&lt;p&gt;Here are the four things that broke, in the order they broke, and what actually fixed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Per-call service quotas, not row counts
&lt;/h2&gt;

&lt;p&gt;The first version looked like every Apps Script tutorial:&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;function&lt;/span&gt; &lt;span class="nf"&gt;logEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&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;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSheet&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;events&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;e&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;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendRow&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="nx"&gt;id&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="nx"&gt;timestamp&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="nx"&gt;amount&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="nx"&gt;source&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;This is fine for ten rows and fatal for a thousand. &lt;code&gt;appendRow&lt;/code&gt; is a service call, and Apps Script quotas count service calls, not rows. Four hundred appends is four hundred round trips to the Sheets backend, each with its own latency, and you hit &lt;code&gt;Service invoked too many times for one day&lt;/code&gt; long before you hit anything resembling a real data volume.&lt;/p&gt;

&lt;p&gt;One call instead of four hundred:&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;function&lt;/span&gt; &lt;span class="nf"&gt;logEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSheet&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;events&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;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&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="nx"&gt;amount&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="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;sheet&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLastRow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&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;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&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;Same output, one service call. This single change took a script that died daily to one that hasn't hit a quota in months.&lt;/p&gt;

&lt;p&gt;The general rule: in Apps Script, anything that touches &lt;code&gt;SpreadsheetApp&lt;/code&gt;, &lt;code&gt;DriveApp&lt;/code&gt;, &lt;code&gt;GmailApp&lt;/code&gt; or &lt;code&gt;UrlFetchApp&lt;/code&gt; inside a loop is a bug waiting for volume. Build the array in memory, write once.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Duplicate writes, because webhooks retry
&lt;/h2&gt;

&lt;p&gt;Every webhook provider retries on non-2xx, and some retry on timeout even when your handler succeeded. If your endpoint takes 30 seconds because of point 1 above, you will get the same event twice, and you will write it twice.&lt;/p&gt;

&lt;p&gt;You need idempotency, and you need it at the write layer rather than the transport layer — you cannot control how many times you get called, only what you do about it.&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;function&lt;/span&gt; &lt;span class="nf"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evt_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&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;key&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="c1"&gt;// already processed&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// 6h TTL, the cache maximum&lt;/span&gt;

  &lt;span class="nf"&gt;writeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CacheService&lt;/code&gt; is the right tool here rather than &lt;code&gt;PropertiesService&lt;/code&gt;, because it expires on its own and you are not trying to keep a permanent ledger — you are trying to survive a retry storm that lasts seconds. For anything needing a longer window, reconcile against the sheet itself on a nightly trigger and dedupe on the ID column.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing: &lt;code&gt;CacheService&lt;/code&gt; is best-effort and can evict early under pressure. It is a retry guard, not a correctness guarantee. If duplicates are genuinely unacceptable, you need the nightly reconciliation as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Concurrent executions racing for the same row
&lt;/h2&gt;

&lt;p&gt;Two webhooks arriving 200ms apart both call &lt;code&gt;getLastRow()&lt;/code&gt;, both get 4,102, and both write to row 4,103. One of them wins. The other event is gone, with no error anywhere.&lt;/p&gt;

&lt;p&gt;This is the failure mode that cost me the most time, because it produces silent, occasional data loss that you only notice weeks later when a total doesn't reconcile.&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;function&lt;/span&gt; &lt;span class="nf"&gt;writeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LockService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptLock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// throws if it can't acquire&lt;/span&gt;
    &lt;span class="nf"&gt;appendRows&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="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="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;Lock timeout, requeueing: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;queueForRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// don't silently drop it&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;releaseLock&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;&lt;code&gt;getScriptLock()&lt;/code&gt; rather than &lt;code&gt;getUserLock()&lt;/code&gt; — the lock needs to span every execution of the script, not every execution by a given user. And note the &lt;code&gt;finally&lt;/code&gt;: an early return inside the try block without releasing the lock will stall every subsequent execution for the full 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The six-minute wall
&lt;/h2&gt;

&lt;p&gt;Apps Script kills any execution at six minutes. If you are backfilling, migrating, or processing a queue that grew overnight, you will hit this, and the execution dies mid-write with no transaction to roll back.&lt;/p&gt;

&lt;p&gt;The pattern is checkpointing: process until you are near the limit, save your position, schedule a continuation, exit cleanly.&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;MAX_RUNTIME_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// leave 90s of headroom&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processQueue&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&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;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cursor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&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;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_RUNTIME_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cursor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nf"&gt;scheduleContinuation&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="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;processItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;cursor&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="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cursor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// finished cleanly&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleContinuation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processQueue&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="nf"&gt;timeBased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's more to this than one code block holds once you're processing a backlog rather than a steady queue — batch sizing, resumability after a failed continuation, and reconciling a partial run. I wrote those up separately in &lt;a href="https://stackarchitect.xyz/blog/scalable-google-sheets-automation-for-high-volume-workflows/" rel="noopener noreferrer"&gt;scalable Google Sheets automation for high-volume workflows&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two things that bite people here. The headroom matters — if your per-item work takes eight seconds, a 30-second buffer isn't enough and you'll die mid-item. And time-based triggers are themselves a quota'd resource, capped at 20 per script, so delete the trigger once it has fired or you will accumulate orphans until trigger creation starts failing.&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;function&lt;/span&gt; &lt;span class="nf"&gt;deleteTriggersFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProjectTriggers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandlerFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;handlerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ScriptApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The backoff you need for external calls
&lt;/h2&gt;

&lt;p&gt;Worth adding for anything calling an external API, since transient 429s and 503s will otherwise surface as permanent failures:&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;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWithBackoff&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;muteHttpExceptions&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResponseCode&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;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&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;res&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;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;429&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;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// real client error, don't retry&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&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="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Max retries exceeded: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jitter is not decoration. Without it, every one of your parallel executions retries at exactly the same moment and you rebuild the thundering herd you were trying to avoid.&lt;/p&gt;

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

&lt;p&gt;The SaaS tools I replaced were not charging $700 a month for the arithmetic. They were charging for the four things above — retry handling, deduplication, concurrency safety, and job continuation — which is genuinely the hard part and which is invisible until you are running at volume.&lt;/p&gt;

&lt;p&gt;That doesn't make the replacement a bad trade. It cost me maybe 40 hours of debugging spread over a few months, against roughly $8,400 a year, and I now own the thing outright and can read every line of it. But it's a real trade, not a free lunch, and anyone who tells you a Sheets-and-webhooks stack "just replaces" a SaaS subscription has not yet run it at four hundred events a day.&lt;/p&gt;

&lt;p&gt;If you're doing something similar: write these four patterns in before you have the volume that proves you needed them. The failures are silent, and by the time you notice, the missing data is already weeks old.&lt;/p&gt;




&lt;p&gt;What's the most annoying quota wall you've hit in Apps Script? I'm fairly sure I haven't found all of them yet.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>automation</category>
      <category>webdev</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>How to Recover Lost Shopify Conversions with CAPI Shield (Server-Side Tracking)</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:43:49 +0000</pubDate>
      <link>https://dev.to/stack_architect/how-to-recover-lost-shopify-conversions-with-capi-shield-server-side-tracking-1e3g</link>
      <guid>https://dev.to/stack_architect/how-to-recover-lost-shopify-conversions-with-capi-shield-server-side-tracking-1e3g</guid>
      <description>&lt;p&gt;CAPI Shield is a free Make.com scenario that takes each paid Shopify order from a webhook and sends it to Meta's Conversions API as a Purchase event, so iOS, Safari and ad blockers cannot stop it. This page is the setup. If your Meta results dropped and you do not yet know why, start with &lt;a href="https://stackarchitect.xyz/blog/shopify-meta-roas-dropped-2026-fix/" rel="noopener noreferrer"&gt;the ROAS guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Shopify CAPI?
&lt;/h3&gt;

&lt;p&gt;The Shopify CAPI refers to server-side Conversions API implementation for Shopify: sending purchase events from Shopify's servers directly to Meta CAPI, Google Enhanced Conversions, or TikTok Events API. It bypasses iOS privacy restrictions and ad blockers that block browser pixel tracking, covering purchase events that would otherwise be missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Shopify server-side tracking?
&lt;/h3&gt;

&lt;p&gt;Server-side tracking sends Shopify conversion events directly from your server to ad platforms, bypassing the browser entirely. iOS restrictions, ad blockers, and cookie expiry cannot interfere. CAPI Shield implements this free via Make.com webhooks: the webhook fires from Shopify's backend the moment an order is paid, completely independent of browser behaviour.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to set up Shopify server-side tracking free?
&lt;/h3&gt;

&lt;p&gt;Use CAPI Shield: a free Make.com scenario that routes Shopify order webhooks to Meta Conversions API. Sign up for Make.com free, create a scenario with a Webhooks trigger, add the Meta Conversions API request, create a Shopify Order Payment webhook pointing to your Make.com URL, and test with a sample order. Takes 6 minutes, no code required, free forever on Make.com's free tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Google Enhanced Conversions and how does it work with Shopify?
&lt;/h3&gt;

&lt;p&gt;Google Enhanced Conversions is Google's server-side conversion tracking system. It supplements your standard Google Ads conversion tags by sending hashed first-party customer data (email, phone, name) alongside each conversion event. Google uses this to match conversions to Google accounts with greater accuracy, improving Smart Bidding and Performance Max optimisation even when cookies and pixels are blocked by iOS or ad blockers. CAPI Shield's Google branch is not a working route to it as shipped: it uploads a click conversion keyed on a Google click ID, and Shopify's order data has no field for one. Google is also moving this upload method, offline click conversion import, out of the Google Ads API and into its Data Manager API, and Make's Google Ads module now warns that uploads may fail for developer tokens that have not recently sent click conversions. Its two consent fields must be set to match your own consent setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Meta CAPI and how does it work with Shopify?
&lt;/h3&gt;

&lt;p&gt;Meta Conversions API (CAPI) is a server-to-server connection that sends Shopify purchase events directly to Meta without going through the browser. CAPI Shield sets this up free using Make.com, sending hashed email, phone, name, IP and user-agent with every event: the match-key set Meta scores Event Match Quality against, and a larger one than the browser pixel carries.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much conversion data am I losing without server-side tracking?
&lt;/h3&gt;

&lt;p&gt;We do not publish a number, because the honest answer is specific to your store: the loss is driven by how much of your traffic is mobile and iOS, and how many of those visitors decline tracking. Measure yours instead. The Shopify vs Meta Attribution Gap Calculator turns your Ads Manager conversion count and your Shopify order count into your own gap; the Meta EMQ Score Estimator scores the match quality of what you are currently sending; and the Shopify iOS Attribution Gap Benchmark publishes the sourced figures behind the mechanism. After CAPI Shield deployment most stores see an immediate increase in reported conversions in Meta Events Manager within 24–48 hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does CAPI Shield replace my Meta Pixel?
&lt;/h3&gt;

&lt;p&gt;No. CAPI Shield works alongside your existing Meta Pixel, not instead of it. The pixel still captures upper-funnel events like ViewContent and AddToCart. CAPI Shield adds server-side Purchase events on top. Both fire simultaneously and Meta deduplicates them automatically using the event_id included in every server event.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between CAPI Shield and Elevar or Northbeam?
&lt;/h3&gt;

&lt;p&gt;Elevar's Core plan is $225/month, and Northbeam lists its Starter plan at $1,500 with every plan quoted through a sales call. CAPI Shield uses Make.com to achieve the same server-side event forwarding for free. The core technical outcome, server-side purchase events reaching Meta CAPI, is identical. Elevar and Northbeam add analytics dashboards on top. If you only need conversion recovery, CAPI Shield is the free equivalent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will CAPI Shield improve my ROAS?
&lt;/h3&gt;

&lt;p&gt;Indirectly, yes. CAPI Shield gives Meta complete purchase data. Advantage+ Shopping uses conversion signals to optimise delivery. Most stores see measurable ROAS improvement within 14–21 days as the algorithm relearns with full data.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Event Match Quality and why does it matter?
&lt;/h3&gt;

&lt;p&gt;Event Match Quality (EMQ) is Meta's 0–10 score for how well server-side events are matched to Meta user accounts. Scores above 7.0 indicate strong matching. CAPI Shield sends hashed email, phone, name, IP and user-agent with every event. More match keys give Meta more to match on, which is what the score measures. We have not sampled EMQ across stores, so we do not publish a typical score: the EMQ estimator projects a range from your own parameter set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is CAPI Shield GDPR compliant?
&lt;/h3&gt;

&lt;p&gt;The data flows through your own Make.com workspace. CAPI Shield hashes all personally identifiable information (email, phone, name) before transmission using SHA-256, as required by Meta's Conversions API specification. Ensure your store's privacy policy covers server-side tracking data sharing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does CAPI Shield setup take?
&lt;/h3&gt;

&lt;p&gt;Most stores are fully live within 6 minutes. The Make.com scenario takes 3–4 minutes to configure. The Shopify webhook takes under 2 minutes. Testing takes 1–2 minutes. No developer, no code, no infrastructure required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is Shopify's built-in CAPI not enough?
&lt;/h3&gt;

&lt;p&gt;Shopify's native CAPI is still browser-triggered: the thank-you page Web Pixel must execute before the server event fires. If a browser blocks the Web Pixel, the server event may not fire. CAPI Shield uses a server-to-server webhook that fires from Shopify's backend the moment payment is confirmed, regardless of browser behaviour, and it sends a larger match-key set than the native integration does. We have not benchmarked the two side by side, so we put no scores on either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this work with TikTok Ads as well?
&lt;/h3&gt;

&lt;p&gt;Yes. The same Make.com scenario can route events to Meta CAPI and TikTok Events API from one webhook. Most stores start with Meta, then add TikTok as a second branch if they advertise there: using the same webhook trigger, no additional Shopify setup required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Graph API version does CAPI Shield use?
&lt;/h3&gt;

&lt;p&gt;The Meta request names a Graph API version in its URL. Meta retires each version about two years after release, and requests to a retired version fail. Check the version in your scenario's URL against Meta's Graph API changelog and update it before it expires, then send one test order to confirm events still arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I want to remove CAPI Shield?
&lt;/h3&gt;

&lt;p&gt;Delete the Make.com scenario and the Shopify webhook. Nothing is installed on your Shopify store. Data stops flowing immediately. No residue, no lock-in, no cancellation fees.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/capi-shield/" rel="noopener noreferrer"&gt;Meta CAPI for Shopify — CAPI Shield, Free Server-Side Setup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>webdev</category>
      <category>ecommerce</category>
      <category>automation</category>
    </item>
    <item>
      <title>Tidio for Shopify: Complete AI Live Chat Setup Guide (2026)</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Sat, 08 Aug 2026 14:02:30 +0000</pubDate>
      <link>https://dev.to/stack_architect/tidio-for-shopify-complete-ai-live-chat-setup-guide-2026-1il8</link>
      <guid>https://dev.to/stack_architect/tidio-for-shopify-complete-ai-live-chat-setup-guide-2026-1il8</guid>
      <description>&lt;p&gt;Tidio installs on Shopify from the App Store, and its Lyro AI answers order-status, shipping and product questions automatically. Tidio says Lyro resolves 67% of conversations instantly (tidio.com); that is Tidio's figure, not one we measured. The free plan covers live chat, a basic bot and Shopify order lookup. Full setup, including Lyro training, takes about 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Tidio free for Shopify?
&lt;/h3&gt;

&lt;p&gt;Yes. Tidio's free plan includes live chat, basic chatbot flows, and 50 one-time Lyro AI conversations (a trial allowance, not a monthly one). Whether that is enough depends on how many conversations your store handles each month, not on its revenue. Paid plans extend the Lyro AI conversation limit and add advanced features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Tidio replace Gorgias for Shopify?
&lt;/h3&gt;

&lt;p&gt;Yes, for most stores. Tidio says Lyro resolves 67% of conversations instantly (tidio.com). Lyro handles order status, shipping questions and product FAQs without human intervention. Gorgias lists its Starter plan at $40/month billed monthly, of which $10 is the helpdesk and $30 its AI Agent (gorgias.com/pricing, read 25 September 2026). Tidio's free tier is permanently functional, not a crippled trial.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Tidio Lyro AI work?
&lt;/h3&gt;

&lt;p&gt;Lyro AI is a generative language model trained on your FAQ content and Shopify product catalogue. It reads customer queries and responds using your knowledge base content, resolving queries without human agents. Unresolved queries are escalated to a human agent automatically. Setup involves uploading FAQ content in the Lyro configuration panel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Tidio integrate with Shopify orders?
&lt;/h3&gt;

&lt;p&gt;Yes. Tidio's native Shopify integration pulls live order data (order status, tracking numbers, fulfilment status, and purchase history) directly into the chat sidebar. Both Lyro AI and human agents can see this data without switching systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Tidio Lyro AI resolution rate?
&lt;/h3&gt;

&lt;p&gt;Tidio says Lyro resolves 67% of conversations instantly (tidio.com). That is Tidio's own figure, not one we measured. Your rate depends mostly on how complete the knowledge base is and how queries are routed, which is what the weekly tuning routine in this post works on.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/blog/tidio-for-shopify-complete-setup-guide/" rel="noopener noreferrer"&gt;Tidio for Shopify: Free AI Live Chat Setup Guide 2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>webdev</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Fixing Shopify Google Ads Conversion Tracking: Server-Side Setup Without GTM</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:01:03 +0000</pubDate>
      <link>https://dev.to/stack_architect/fixing-shopify-google-ads-conversion-tracking-server-side-setup-without-gtm-1ld1</link>
      <guid>https://dev.to/stack_architect/fixing-shopify-google-ads-conversion-tracking-server-side-setup-without-gtm-1ld1</guid>
      <description>&lt;p&gt;Google Ads under-reports Shopify purchases because its conversion tag fires in the browser, where iOS restrictions and ad blockers stop it. A server-side upload through a Make.com webhook can add hashed customer email, but Shopify's order data carries no Google click ID, so what Google can match depends on how your conversion action is set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is my Shopify Google Ads conversion tracking wrong in 2026?
&lt;/h3&gt;

&lt;p&gt;Three causes account for most Google Ads attribution gaps on Shopify in 2026: iOS Link Tracking Protection stripping gclid click IDs from URLs before customers open them in Safari, Mail, or Messages; Safari Intelligent Tracking Prevention (ITP) clearing attribution cookies before the purchase completes; and ad blockers preventing the gTag from loading. Server-side Google Enhanced Conversions fixes all three because events are sent from Shopify's server to Google directly, bypassing the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Google Enhanced Conversions for Shopify?
&lt;/h3&gt;

&lt;p&gt;Google Enhanced Conversions is Google's server-side conversion tracking supplement. It sends hashed first-party customer data (email, phone, name) alongside each conversion event via API. Google uses this to match conversions to signed-in Google accounts with greater accuracy, improving Smart Bidding and Performance Max optimisation even when cookies and gclid are blocked. For Shopify stores, it carries purchase events that the browser-based gTag misses when iOS and Safari strip the click ID or clear the cookie. We have not measured what share of events that is, and are not aware of a published figure for Google Ads specifically, so this page does not quote one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I set up Google Ads conversion tracking on Shopify for free?
&lt;/h3&gt;

&lt;p&gt;The free two-layer approach: first, ensure the Google &amp;amp; YouTube sales channel is installed in Shopify for baseline browser-side tracking. Then add Google Enhanced Conversions via Make.com: create a Make.com free account, add a Webhooks module as the trigger, create a Shopify Order payment webhook pointing to your Make.com URL, add an HTTP module calling the Google Ads Conversions API endpoint, and hash customer email and phone with SHA-256. Full setup takes 6 minutes. No code, no GTM, no paid apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is GCLID and why does it matter for Shopify Google Ads?
&lt;/h3&gt;

&lt;p&gt;GCLID (Google Click ID) is the unique identifier Google appends to your ad URLs when someone clicks an ad: for example, ?gclid=CjwKCAjw8. Shopify's thank-you page uses this ID to attribute the purchase back to the specific Google Ads click. When iOS Link Tracking Protection or Safari ITP strips or clears the gclid, the conversion is unattributed. Google's Enhanced Conversions adds hashed customer data to help Google match purchases. A server-side upload built on Shopify's Order payment webhook has no gclid to send, because Shopify's order data has no field for one, so whether Google matches it depends on how your conversion action is set up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Google Tag Manager for Shopify Google Ads conversion tracking?
&lt;/h3&gt;

&lt;p&gt;No. The Make.com approach sends Google Enhanced Conversions events directly to the Google Ads Conversions API via HTTP request: no Google Tag Manager container required. GTM is one implementation route. Direct server-to-server API calls via Make.com are simpler and more reliable for Shopify stores because the entire flow operates without a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between standard conversion tracking and Enhanced Conversions?
&lt;/h3&gt;

&lt;p&gt;Standard Google Ads conversion tracking fires a browser-based gTag when the order confirmation page loads. It fails when iOS or Safari blocks the gclid, when ad blockers prevent the gTag from loading, or when the browser session expires before the customer reaches the thank-you page. Google Enhanced Conversions supplements this by sending hashed customer data server-to-server, independently of browser state. Running both simultaneously gives Google the most accurate conversion picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I fix Google Ads showing fewer conversions than Shopify?
&lt;/h3&gt;

&lt;p&gt;If Google Ads reports significantly fewer conversions than Shopify shows orders for the same period, you are losing attribution due to iOS restrictions or ad blockers. A server-side upload via Make.com runs independently of the browser, but it only helps if Google can match what it sends: Shopify's order data has no Google click ID, so the upload carries hashed customer email alone, and whether Google accepts that depends on how your conversion action is set up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will server-side Google Ads tracking improve my Smart Bidding performance?
&lt;/h3&gt;

&lt;p&gt;Yes. Smart Bidding, Target ROAS, and Performance Max all optimise based on the conversion signals Google receives. When real conversions are invisible due to browser-based tracking failures, the algorithm is bidding on incomplete data: underbidding on high-intent traffic and over-bidding on low-converting segments. Completing conversion data with server-side Enhanced Conversions typically improves Smart Bidding performance within 14–21 days as the model relearns with full data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this work alongside my existing Google Ads conversion tag?
&lt;/h3&gt;

&lt;p&gt;Yes. Google Enhanced Conversions is designed to supplement, not replace, your existing conversion tags. The browser-side gTag continues to fire as normal. The server-side Enhanced Conversions layer adds hashed customer data on top. Google automatically deduplicates events using the order ID, so the same purchase is not counted twice. Running both layers together gives maximum conversion accuracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does it cost to fix Google Ads conversion tracking on Shopify?
&lt;/h3&gt;

&lt;p&gt;The complete free setup costs $0/month. The Make.com free plan (1,000 credits/month (formerly called operations)) covers Enhanced Conversions for stores processing under 500 orders per month. Above that, Make.com Core at $9/month covers 10,000 credits. Every paid alternative, Stape ($17/month billed yearly), Littledata ($159+/month), Analyzify ($145–$275/month), charges a monthly subscription for the same server-side outcome Make.com delivers free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this GDPR compliant?
&lt;/h3&gt;

&lt;p&gt;All customer PII (email, phone, name) is hashed with SHA-256 inside Make.com before transmission to Google, as required by Google's Enhanced Conversions specification. The hashed data flows through your own Make.com workspace. Ensure your privacy policy discloses that hashed customer data is shared with Google for conversion matching purposes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/shopify-google-ads-conversion-tracking/" rel="noopener noreferrer"&gt;Fix Shopify Google Ads Tracking — Free Setup 2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>google</category>
      <category>shopify</category>
      <category>webhooks</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Fix ‘Service Invoked Too Many Times’ in Google Apps Script 2026</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:12:53 +0000</pubDate>
      <link>https://dev.to/stack_architect/how-to-fix-service-invoked-too-many-times-in-google-apps-script-2e0o</link>
      <guid>https://dev.to/stack_architect/how-to-fix-service-invoked-too-many-times-in-google-apps-script-2e0o</guid>
      <description>&lt;p&gt;Google Apps Script stops any single execution after six minutes, on consumer and Workspace accounts alike. Daily quotas depend on the account: triggers may run for 90 minutes a day on a consumer account and six hours on Workspace. No setting or support request raises them, which is why heavy Autocrat and Sheets automations eventually fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Google Apps Script execution time limit in 2026?
&lt;/h3&gt;

&lt;p&gt;Both consumer (free) and Google Workspace accounts are capped at 6 minutes per execution in 2026: the 30-minute Workspace limit cited by older guides no longer applies. Scripts that exceed the limit are hard-stopped mid-run: no graceful shutdown, no warning, just termination.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the daily runtime limit for Google Apps Script in 2026?
&lt;/h3&gt;

&lt;p&gt;Google's official quota for triggers total runtime is 90 minutes per day on consumer accounts and 6 hours per day on Workspace accounts. This applies to trigger-driven executions; manual runs are not counted. The quota resets 24 hours after the first request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Google Apps Script UrlFetch daily limit?
&lt;/h3&gt;

&lt;p&gt;Consumer accounts can make 20,000 UrlFetch calls per day. Google Workspace accounts get 100,000 per day. If your automation calls external APIs on every order, this limit can be exhausted faster than expected on high-volume days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Have Google Apps Script quotas changed for 2026?
&lt;/h3&gt;

&lt;p&gt;The core limits, 6-minute execution (both consumer and Workspace) and 90-minute daily trigger runtime on consumer accounts, have remained consistent into 2026. Google does not announce quota changes prominently. If you are seeing new failures, it is more likely that your volume has grown into the limits rather than that Google has tightened them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Google Apps Script quotas be increased?
&lt;/h3&gt;

&lt;p&gt;Not by request or configuration. Google publishes one set of figures for consumer accounts and a higher set for Google Workspace accounts, so moving to Workspace raises the daily quotas, such as trigger runtime and URL Fetch calls. The six-minute limit per execution is the same on both. To work beyond it, move the work outside Apps Script, into Make.com or a dedicated API layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my script work in the morning but fail in the afternoon?
&lt;/h3&gt;

&lt;p&gt;This is the clearest sign of a daily quota problem. Trigger-driven automations are exhausting the 90-minute daily trigger runtime (consumer) in the first half of the day, leaving nothing for afternoon processing. The quota resets 24 hours after the first request, which is why timing shifts day to day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the same script work one day and fail the next?
&lt;/h3&gt;

&lt;p&gt;Variable load. A day with higher order volume or more form submissions hits limits that a quieter day never reached. The script did not change: the pressure on its quota allowance did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Apps Script still useful for automation in 2026?
&lt;/h3&gt;

&lt;p&gt;Yes: as a lightweight trigger and orchestration layer for simple spreadsheet actions and low-volume workflows. It becomes a liability when it is the execution engine for mission-critical, high-volume processing. For those cases, Make.com is the right tool.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/blog/google-apps-script-quotas-explained-how-to-avoid-limits-and-scale-your-automations/" rel="noopener noreferrer"&gt;Google Apps Script Quotas 2026 — Every Limit Explained&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googleappsscript</category>
      <category>javascript</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Shopify Agentic Storefronts 2026 — Sell on ChatGPT &amp; Google AI Mode (Setup Guide)</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Mon, 27 Jul 2026 09:57:05 +0000</pubDate>
      <link>https://dev.to/stack_architect/shopify-agentic-storefronts-2026-sell-on-chatgpt-google-ai-mode-setup-guide-h29</link>
      <guid>https://dev.to/stack_architect/shopify-agentic-storefronts-2026-sell-on-chatgpt-google-ai-mode-setup-guide-h29</guid>
      <description>&lt;p&gt;Shopify Agentic Storefronts connect your catalog to ChatGPT, Google AI Mode, Microsoft Copilot and Meta from one admin setting. Since 24 March 2026 the channels split: Google, Copilot and Meta complete checkout inside the AI surface, while ChatGPT redirects shoppers to your own Shopify checkout, which is why most limits apply to only some channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Shopify Agentic Storefronts?
&lt;/h3&gt;

&lt;p&gt;Shopify Agentic Storefronts connect your product catalog to AI assistants (ChatGPT, Google AI Mode and Gemini, Microsoft Copilot, and Meta surfaces such as Muse) through one setting in your Shopify admin, with no separate integration per platform. What each channel then does with a shopper differs: Google, Copilot and Meta complete the purchase inside their own interface, while ChatGPT sends the shopper to your own Shopify checkout. Announced in Shopify's Winter '26 Edition on 10 December 2025 and generally available to eligible merchants from 24 March 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does ChatGPT still let customers buy without leaving the chat?
&lt;/h3&gt;

&lt;p&gt;No. OpenAI retired Instant Checkout on 24 March 2026, and Shopify’s documentation now describes ChatGPT as a discovery-focused referrer: shoppers complete the purchase on your own Shopify checkout, opened in the ChatGPT in-app browser or a new tab. This matters more than it sounds. Because the transaction happens on your checkout, the product-type restrictions, fulfilment restrictions and pixel restrictions that apply to direct checkout do not apply to ChatGPT traffic at all: your normal checkout rules and your client-side pixels work as they always have. Guides published before 24 March 2026 describe the opposite arrangement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Am I eligible for Shopify Agentic Storefronts?
&lt;/h3&gt;

&lt;p&gt;Eligibility is per channel, not one blanket rule, and it stacks two separate conditions: where your store is based, and where you sell. ChatGPT and Microsoft Copilot accept a store based in any country provided it sells to customers in the United States; Meta accepts one that sells to customers in the United States, Canada or Mexico. Google AI Mode and Gemini require the store itself to be based in the United States and to sell to customers there, and direct checkout renders only for US-based customers, with everyone else redirected to the store. So a UK-based store cannot use Google, but can use ChatGPT and Copilot if it sells to US customers, and Meta if it sells to customers in the US, Canada or Mexico. Google is still rolling out, and its product discovery runs on Google Merchant Center, which Shopify feeds through the Google &amp;amp; YouTube sales channel. Agentic storefronts are active by default for eligible stores; the setting lives under Sales channels, then Agentic.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does the Shopify Knowledge Base app cost?
&lt;/h3&gt;

&lt;p&gt;It is free. The Shopify Knowledge Base app is published by Shopify, launched on 16 May 2025, and its App Store listing shows no paid tier and no Plus-only pricing. Guides quoting a monthly fee for standard plans appear to be describing third-party knowledge-base apps instead. The app is not visible on your storefront: your FAQs serve as a data source that AI platforms read when answering questions about your products.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which products cannot be sold through Agentic Storefronts?
&lt;/h3&gt;

&lt;p&gt;Subscriptions, product bundles, customizable products and B2B-only products are not supported in agentic storefront direct checkouts. Local delivery, pickup in store and pickup points are not available as delivery methods, and custom checkout fields, checkout upsells, checkout validations, a subset of accelerated checkout options and requiring customer sign-in do not work either. Every one of those restrictions applies to the direct-checkout channels only: Google, Copilot and Meta. ChatGPT sends shoppers to your own checkout, where none of them apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do discount codes work in agentic checkout?
&lt;/h3&gt;

&lt;p&gt;Yes. Shopify’s documentation states that automatic discounts and discount codes you set up are both supported in agentic storefront direct checkouts. Several widely-shared guides claim the opposite; that claim does not match Shopify’s current documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will Google Analytics and my Meta pixel track agentic orders?
&lt;/h3&gt;

&lt;p&gt;Not in direct checkout. Shopify states that Google Analytics and custom pixels will not fire there: only server-to-server pixels for the started and completed events. So an order placed inside Google AI Mode, Copilot or Meta reaches your Shopify admin but arrives in GA4 and Meta Ads Manager with no client-side event behind it. If you buy ads and measure return on ad spend from browser pixels, those orders look like they came from nowhere. Server-side tracking is the fix, and it is worth having in place before you enable the channels rather than after. ChatGPT is unaffected: that checkout is yours, so your pixels fire normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Shopify charge a fee for selling through AI channels?
&lt;/h3&gt;

&lt;p&gt;No. Shopify’s documentation states there are no fees associated with selling in ChatGPT, in Google AI Mode and Gemini’s direct checkout, or in agentic storefront direct checkouts generally. You pay your standard payment processing fees and nothing further. Shopify also offers a separate free Agentic plan aimed at merchants who do not use Shopify as their primary storefront; it excludes the Online Store, gift cards, manual orders and Shop post-purchase offers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Perplexity a Shopify agentic channel?
&lt;/h3&gt;

&lt;p&gt;No, despite appearing in Shopify’s December 2025 launch announcement. In-chat buying on Perplexity runs on PayPal Instant Buy, those orders are managed through PayPal rather than your Shopify admin, and merchants apply to the Perplexity Merchant Program separately. Perplexity is no longer listed as an Agentic Storefronts channel in Shopify’s help documentation. Guides presenting it as a Shopify channel with redirect checkout are describing an arrangement that does not exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  My store is not eligible yet. Is any of this worth doing now?
&lt;/h3&gt;

&lt;p&gt;Yes, because discovery already happens without it. AI assistants crawl the web and read Shopify Catalog regardless of your agentic settings: what the feature adds is the checkout, not the visibility. Four things pay off immediately: install and populate the free Knowledge Base app, complete your product data so AI assistants can match products to conversational queries, add FAQPage structured data to your content, and get server-side tracking working so you have no attribution gap on the day the channels switch on.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/blog/shopify-agentic-storefronts-setup-guide-2026/" rel="noopener noreferrer"&gt;Shopify Agentic Storefronts — Eligibility &amp;amp; Limits 2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Shopify Server-Side Tracking Setup — Step by Step</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Sun, 26 Jul 2026 11:13:35 +0000</pubDate>
      <link>https://dev.to/stack_architect/recover-20-40-of-invisibleshopify-conversions-free-35fj</link>
      <guid>https://dev.to/stack_architect/recover-20-40-of-invisibleshopify-conversions-free-35fj</guid>
      <description>&lt;p&gt;Setting up Shopify server-side tracking means connecting a Shopify orders/paid webhook to a free Make.com scenario: the webhook fires when an order is paid, Make.com hashes the customer data with SHA-256, then posts the event to Meta Conversions API, Google Enhanced Conversions, and TikTok Events API. A full three-platform build takes about 18 minutes, no code required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Shopify server-side tracking and how does it work?
&lt;/h3&gt;

&lt;p&gt;Shopify server-side tracking sends purchase events directly from your server to ad platforms (Meta, Google, TikTok) without going through the customer’s browser. When a Shopify order is paid, a webhook fires to Make.com which routes the event to each platform’s API endpoint: bypassing iOS restrictions, ad blockers, and cookie limitations. It works alongside your existing browser pixel, not instead of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to remove my browser pixel when setting up server-side tracking?
&lt;/h3&gt;

&lt;p&gt;No: keep your browser pixel running alongside server-side tracking. The pixel captures upper-funnel events (ViewContent, AddToCart, InitiateCheckout) that server webhooks don’t cover since they only fire on confirmed payment. Set a matching event_id in both your pixel and server events so each platform deduplicates and you see one conversion, not two.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much of my lost Shopify conversion data will server-side tracking recover?
&lt;/h3&gt;

&lt;p&gt;We publish no recovery percentage, and no honest one exists as an industry figure: what server-side tracking recovers is whatever your browser pixel was losing, which is set by your own mobile/iOS traffic mix and consent rate. Measure it as Additional Conversions Reported in Meta Events Manager, or with the attribution gap calculator, and see why the 20–40% estimate was retracted. Meta reported in April 2026 that advertisers running the Conversions API for web events saw an average 17.8% lower cost per result versus those without it; that is Meta's figure across its advertisers, not a measurement of your store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is server-side tracking GDPR compliant?
&lt;/h3&gt;

&lt;p&gt;The data flows through your own Make.com workspace: you control what is sent and can delete the scenario at any time. All personally identifiable information (email, phone) is SHA-256 hashed before transmission as required by each platform’s API specification. Ensure your store privacy policy discloses server-side conversion data sharing with advertising platforms. For GDPR-specific advice consult a legal professional.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long before I see results after setting up server-side tracking?
&lt;/h3&gt;

&lt;p&gt;Server events start flowing immediately after your first paid order. More conversions will appear in your ad platform reporting within 24–48 hours. Meaningful CPA and ROAS improvement typically takes 7–14 days as ad platform algorithms recalibrate delivery optimisation using the more complete conversion signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often does Make.com’s free tier actually fail or rate-limit?
&lt;/h3&gt;

&lt;p&gt;The Make.com free plan limit is 1,000 credits/month, refreshing on the 1st of each month. One Shopify order = 4 credits (webhook trigger + Meta + Google + TikTok), so the free tier handles ~250 orders/month before hitting the cap. Make.com does not “fail” mid-month: it pauses your scenario and emails you. Real failure modes are different: a temporary Meta API outage, an expired access token, or a malformed payload. These are caught in Make.com’s execution log under each scenario, and re-runs are free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will server-side tracking keep working as iOS tightens privacy further?
&lt;/h3&gt;

&lt;p&gt;Yes. Server-side tracking is specifically designed to be immune to iOS browser-level restrictions because the event originates from Shopify’s server, not the customer’s iPhone. Every iOS release since 17 has extended Link Tracking Protection and tightened Safari’s Intelligent Tracking Prevention: both of which only affect browser pixels. CAPI, Google Enhanced Conversions, and TikTok Events API will continue to function exactly as documented. If anything, server-side tracking becomes more valuable with each iOS release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I still need server-side tracking if I’m using Shopify’s native Customer Events / Web Pixels?
&lt;/h3&gt;

&lt;p&gt;Yes. Shopify’s Web Pixels framework is still browser-based: it loads in a sandboxed iframe in the customer’s browser and is subject to the same ad blocker and ITP restrictions as a regular pixel. It improves reliability slightly versus a raw Meta Pixel install, but does not match the recovery rate of a true server-side webhook firing from Shopify’s backend on confirmed payment. Run Web Pixels for upper-funnel events (PageView, AddToCart) and the Make.com webhook method for the Purchase event.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between Meta CAPI and Shopify’s native data sharing?
&lt;/h3&gt;

&lt;p&gt;Shopify’s native Maximum data sharing setting enables their own CAPI integration, but it is still partially browser-triggered: if the thank-you page pixel fails to execute, the server event may not fire. The Make.com webhook method fires directly from Shopify’s backend on payment confirmation with zero browser dependency, making it more reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I set up Shopify server-side tracking without a developer?
&lt;/h3&gt;

&lt;p&gt;Yes. The Make.com webhook setup requires no code. You configure a Shopify webhook in Settings, build an HTTP module in Make.com’s visual drag-and-drop interface, and paste your API credentials. Most store owners complete the setup in under 20 minutes. No developer, no agency, no Shopify app installation required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does server-side tracking work with Shopify’s native Maximum data sharing?
&lt;/h3&gt;

&lt;p&gt;Yes: the Make.com webhook method works independently of and alongside Shopify’s native data sharing. You do not need to disable native CAPI. Running both adds redundancy: if one delivery method has a transient failure, the other ensures the event still reaches the platform. Use matching event_id values in both so platforms deduplicate correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does Shopify server-side tracking cost?
&lt;/h3&gt;

&lt;p&gt;The Make.com webhook method costs $0/month for Shopify stores under approximately 250 orders per month, since each order uses 4 Make.com credits (the webhook trigger plus one each for Meta, Google and TikTok) and the free tier covers 1,000 credits monthly. Above that, the Make.com Core plan at $9/month covers 10,000 credits: enough for roughly 2,500 orders/month at 4 credits each (make.com/en/pricing, read 25 September 2026). Managed alternatives like Elevar (from $225/month) and Triple Whale (priced by store revenue) achieve the same tracking quality at significantly higher cost. The premium pays for managed onboarding and dashboards, not for superior API access.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Event Match Quality (EMQ) score is good for Meta CAPI on Shopify?
&lt;/h3&gt;

&lt;p&gt;Meta scores Event Match Quality from 1 to 10. The score rises with the number of match keys an event carries. The Make.com webhook method sends hashed email, phone, zip, IP, user agent, fbc and fbp. Scores of 9–10 are excellent but typically require logged-in customer data with full address fields. Anything below 5 indicates missing match keys: investigate email format (lowercase + trim before SHA-256), phone format (E.164 digits-only before SHA-256), and the external_id field (Shopify customer.id, hashed).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my TikTok event match quality low even with server-side tracking?
&lt;/h3&gt;

&lt;p&gt;The single most common cause is sending the event name “Purchase” instead of CompletePayment. TikTok’s Events API requires CompletePayment as the event name for purchase events: sending “Purchase” (which works for Meta) results in low Event Match Score and prevents purchase-campaign optimisation. Other common causes: missing hashed email, missing IP address (you must map order.client_details.browser_ip from the Shopify webhook payload, not the Make.com runtime IP), missing TikTok Click ID (ttclid) for paid traffic, and missing user agent. Send all four high-impact signals (email, phone, IP/UA, external_id): that is the full set TikTok calculates the score from. We publish no figure for what it scores; read yours in TikTok Events Manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Shopify server-side tracking free?
&lt;/h3&gt;

&lt;p&gt;Yes: the tracking itself is free to implement. The Make.com webhook method in this guide costs $0/month for stores under ~250 orders/month, because each order uses 4 Make.com credits and the free tier covers 1,000. Above that, Make.com Core is $9/month for 10,000 credits. The platform APIs (Meta CAPI, Google Enhanced Conversions, TikTok Events API) are all free to use. You only pay if you choose a managed tool: Elevar runs from $225/month and Triple Whale prices by store revenue, but they charge for dashboards and managed onboarding, not for better tracking access. The same events reach the same endpoints either way. See the free vs paid comparison for the full breakdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shopify server-side tracking vs Elevar: what’s the difference?
&lt;/h3&gt;

&lt;p&gt;Both send the same server-side events to the same destinations (Meta CAPI, Google, TikTok): the difference is who builds and maintains the pipeline. Elevar is a managed service from $225/month: it provisions a server-side GTM container, supplies a pre-built Shopify data layer, monitors delivery with alerts, and handles refunds and order edits automatically. The free Make.com webhook method delivers the same events and the same Event Match Quality ceiling, but you build the scenario yourself in ~18 minutes and maintain it. Choose Make.com for zero cost and full control; choose Elevar if managed monitoring, automatic edge-case handling, and a reporting dashboard justify the monthly fee at your ad spend. For most stores under 1,000 orders/month, the Make.com method delivers the tracking quality without the recurring cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need server-side GTM (sGTM) for Shopify server-side tracking?
&lt;/h3&gt;

&lt;p&gt;No. Server-side Google Tag Manager is one way to do server-side tracking, but it requires provisioning and paying for a Google Cloud tagging server and configuring containers: overkill for most Shopify stores. The Make.com webhook method skips GTM entirely: Shopify fires an order webhook directly to Make.com, which POSTs to each platform’s API. You get the same server-to-server delivery with no server to host or maintain. sGTM earns its complexity only for large stores with dedicated analytics engineers needing custom tag logic across many destinations; for everyone else, the webhook method is simpler, free, and equally effective for conversion tracking.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/blog/shopify-server-side-tracking-complete-setup-guide/" rel="noopener noreferrer"&gt;Shopify Server-Side Tracking Setup — Step by Step&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>webhooks</category>
      <category>analytics</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Google Apps Script Quota Limits 2026 — Every Error, Every Fix</title>
      <dc:creator>Luke Sandelands</dc:creator>
      <pubDate>Sat, 25 Jul 2026 21:06:33 +0000</pubDate>
      <link>https://dev.to/stack_architect/google-apps-script-quota-limits-2026-every-error-every-fix-2p87</link>
      <guid>https://dev.to/stack_architect/google-apps-script-quota-limits-2026-every-error-every-fix-2p87</guid>
      <description>&lt;p&gt;Google Apps Script stops any single execution after six minutes, on consumer and Workspace accounts alike. Daily quotas depend on the account: triggers may run for 90 minutes a day on a consumer account and six hours on Workspace. No setting or support request raises them, which is why heavy Autocrat and Sheets automations eventually fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this guide answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Google Apps Script execution time limit in 2026?
&lt;/h3&gt;

&lt;p&gt;Both consumer (free) and Google Workspace accounts are capped at 6 minutes per execution in 2026: the 30-minute Workspace limit cited by older guides no longer applies. Scripts that exceed the limit are hard-stopped mid-run: no graceful shutdown, no warning, just termination.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the daily runtime limit for Google Apps Script in 2026?
&lt;/h3&gt;

&lt;p&gt;Google's official quota for triggers total runtime is 90 minutes per day on consumer accounts and 6 hours per day on Workspace accounts. This applies to trigger-driven executions; manual runs are not counted. The quota resets 24 hours after the first request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Google Apps Script UrlFetch daily limit?
&lt;/h3&gt;

&lt;p&gt;Consumer accounts can make 20,000 UrlFetch calls per day. Google Workspace accounts get 100,000 per day. If your automation calls external APIs on every order, this limit can be exhausted faster than expected on high-volume days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Have Google Apps Script quotas changed for 2026?
&lt;/h3&gt;

&lt;p&gt;The core limits, 6-minute execution (both consumer and Workspace) and 90-minute daily trigger runtime on consumer accounts, have remained consistent into 2026. Google does not announce quota changes prominently. If you are seeing new failures, it is more likely that your volume has grown into the limits rather than that Google has tightened them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Google Apps Script quotas be increased?
&lt;/h3&gt;

&lt;p&gt;Not by request or configuration. Google publishes one set of figures for consumer accounts and a higher set for Google Workspace accounts, so moving to Workspace raises the daily quotas, such as trigger runtime and URL Fetch calls. The six-minute limit per execution is the same on both. To work beyond it, move the work outside Apps Script, into Make.com or a dedicated API layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my script work in the morning but fail in the afternoon?
&lt;/h3&gt;

&lt;p&gt;This is the clearest sign of a daily quota problem. Trigger-driven automations are exhausting the 90-minute daily trigger runtime (consumer) in the first half of the day, leaving nothing for afternoon processing. The quota resets 24 hours after the first request, which is why timing shifts day to day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the same script work one day and fail the next?
&lt;/h3&gt;

&lt;p&gt;Variable load. A day with higher order volume or more form submissions hits limits that a quieter day never reached. The script did not change: the pressure on its quota allowance did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Apps Script still useful for automation in 2026?
&lt;/h3&gt;

&lt;p&gt;Yes: as a lightweight trigger and orchestration layer for simple spreadsheet actions and low-volume workflows. It becomes a liability when it is the execution engine for mission-critical, high-volume processing. For those cases, Make.com is the right tool.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The full guide, with every step, source and date:&lt;/strong&gt; &lt;a href="https://stackarchitect.xyz/blog/google-apps-script-quotas-explained-how-to-avoid-limits-and-scale-your-automations/" rel="noopener noreferrer"&gt;Google Apps Script Quotas 2026 — Every Limit Explained&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://stackarchitect.xyz/" rel="noopener noreferrer"&gt;Stack Architect&lt;/a&gt; by Luke Sandelands. Figures there carry the date they were last checked; this copy does not, so prefer the original.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googleappsscript</category>
      <category>automation</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
