<?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: Sunday Emmanuel Azeez</title>
    <description>The latest articles on DEV Community by Sunday Emmanuel Azeez (@sunny171p).</description>
    <link>https://dev.to/sunny171p</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%2F4148026%2F67d723a2-f30f-4bee-81b9-652c8ce65bd4.png</url>
      <title>DEV Community: Sunday Emmanuel Azeez</title>
      <link>https://dev.to/sunny171p</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sunny171p"/>
    <language>en</language>
    <item>
      <title>Shopify devs and store owners, how do your clients actually handle cash flow?</title>
      <dc:creator>Sunday Emmanuel Azeez</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:33:22 +0000</pubDate>
      <link>https://dev.to/sunny171p/shopify-devs-and-store-owners-how-do-your-clients-actually-handle-cash-flow-5cn</link>
      <guid>https://dev.to/sunny171p/shopify-devs-and-store-owners-how-do-your-clients-actually-handle-cash-flow-5cn</guid>
      <description>&lt;p&gt;I run a small Shopify store in the UK and I have a data and finance background. I'm looking into a question many online sellers seem to face: when sales look healthy, how do you know how much cash you'll actually have in a few weeks, once ad spend, supplier orders, payout delays, fees and VAT have come out?&lt;/p&gt;

&lt;p&gt;I'm researching whether this is a common problem before building anything. If you run a store yourself, or you build or maintain Shopify stores for clients, I'd really value your view.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reply
&lt;/h2&gt;

&lt;p&gt;Copy the questions below into a comment and write your answer after each "Answer:". Skip any that don't apply to you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Do you run your own store, build stores for clients, or both?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How do you (or your clients) track cash flow today? Spreadsheet, accounting software, an app, or not really?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Have you seen a store that was profitable on paper but still ran short of cash? What caused it?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What's the hardest part: payout delays, ad spend, paying for stock upfront, VAT, refunds, or something else?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. How much manual work goes into pulling numbers together from Shopify, ad platforms and the bank?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Have you or your clients used a cash flow or profit app? What was good or missing?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. From a dev point of view, what's the trickiest part of getting reliable data out of Shopify Payments, payouts or ad platforms?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. If a simple tool showed cash for the next 7, 30 and 90 days and warned before a shortfall, would you or your clients use it? What must it do?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Answer:&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Anything else I should know:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not selling anything, just trying to understand the problem properly. Thanks to anyone who takes the time to answer!&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>discuss</category>
      <category>startup</category>
    </item>
    <item>
      <title>How to send Shopify orders straight into a cloud database with Python: a beginner's guide</title>
      <dc:creator>Sunday Emmanuel Azeez</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:25:09 +0000</pubDate>
      <link>https://dev.to/sunny171p/how-to-send-shopify-orders-straight-into-a-cloud-database-with-python-a-beginners-guide-42jj</link>
      <guid>https://dev.to/sunny171p/how-to-send-shopify-orders-straight-into-a-cloud-database-with-python-a-beginners-guide-42jj</guid>
      <description>&lt;p&gt;For the first few months of running my Shopify store, I got my sales data the slow way. I exported a CSV from the admin, dropped it into a folder and ran my analysis scripts. It worked, but the data was always out of date by the time I looked at it.&lt;br&gt;
Webhooks fix that. Instead of you asking Shopify for data, Shopify tells you the moment something happens. When a customer places an order, Shopify sends the full order to a web address you choose, within seconds. In this guide I'll show you how I catch those orders with a small Python app and save each one in a cloud database, where it's safe even if the app restarts.&lt;br&gt;
You don't need to be an experienced developer. If you can install Python packages and run a script, you can follow this.&lt;br&gt;
What you'll need&lt;br&gt;
• Python 3.10 or newer&lt;br&gt;
• A Shopify store where you have admin access&lt;br&gt;
• A free Supabase account (a hosted PostgreSQL database)&lt;br&gt;
• ngrok, free, for testing on your own computer&lt;br&gt;
Step 1: Create a table to hold the orders&lt;br&gt;
In Supabase, create a new project, open the SQL Editor and run this:&lt;br&gt;
create table webhook_events (&lt;br&gt;
    id bigint generated always as identity primary key,&lt;br&gt;
    webhook_id text unique,&lt;br&gt;
    topic text,&lt;br&gt;
    shopify_order_id bigint,&lt;br&gt;
    order_name text,&lt;br&gt;
    payload jsonb not null,&lt;br&gt;
    received_at timestamptz default now()&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;alter table webhook_events enable row level security;&lt;br&gt;
A few choices here are worth explaining:&lt;br&gt;
• payload is a jsonb column, which stores the whole order exactly as Shopify sent it. Shopify orders have dozens of fields. Rather than decide now which ones matter, keep everything and pick out what you need later.&lt;br&gt;
• webhook_id is marked unique. Shopify sometimes delivers the same webhook more than once, and this lets the database quietly ignore the repeats. We'll use it in Step 3.&lt;br&gt;
• Turning on row level security, with no rules added, means nobody can read the table using your public API key. The script uses a private server key instead, which still works.&lt;br&gt;
Step 2: Set up the project&lt;br&gt;
Make a new folder and install three packages:&lt;br&gt;
pip install flask supabase python-dotenv&lt;br&gt;
Then create a file called .env in the same folder:&lt;br&gt;
SHOPIFY_WEBHOOK_SECRET=paste-this-in-step-5&lt;br&gt;
SUPABASE_URL=&lt;a href="https://your-project.supabase.co" rel="noopener noreferrer"&gt;https://your-project.supabase.co&lt;/a&gt;&lt;br&gt;
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key&lt;br&gt;
You'll find the URL and the service role key in Supabase under Project Settings &amp;gt; API. The service role key has full access to your database, so treat it like a password. Never put it in front-end code, and never commit the .env file to GitHub. Add .env to your .gitignore straight away.&lt;br&gt;
Step 3: Write the listener&lt;br&gt;
Create app.py:&lt;br&gt;
import base64&lt;br&gt;
import hashlib&lt;br&gt;
import hmac&lt;br&gt;
import os&lt;/p&gt;

&lt;p&gt;from dotenv import load_dotenv&lt;br&gt;
from flask import Flask, abort, request&lt;br&gt;
from supabase import create_client&lt;/p&gt;

&lt;p&gt;load_dotenv()&lt;/p&gt;

&lt;p&gt;WEBHOOK_SECRET = os.environ["SHOPIFY_WEBHOOK_SECRET"]&lt;br&gt;
supabase = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_ROLE_KEY"])&lt;/p&gt;

&lt;p&gt;app = Flask(&lt;strong&gt;name&lt;/strong&gt;)&lt;/p&gt;

&lt;p&gt;def is_from_shopify(raw_body: bytes, signature: str) -&amp;gt; bool:&lt;br&gt;
    digest = hmac.new(WEBHOOK_SECRET.encode("utf-8"), raw_body, hashlib.sha256).digest()&lt;br&gt;
    expected = base64.b64encode(digest).decode("utf-8")&lt;br&gt;
    return hmac.compare_digest(expected, signature or "")&lt;/p&gt;

&lt;p&gt;@app.route("/webhooks/orders/create", methods=["POST"])&lt;br&gt;
def order_created():&lt;br&gt;
    raw_body = request.get_data()&lt;br&gt;
    if not is_from_shopify(raw_body, request.headers.get("X-Shopify-Hmac-Sha256")):&lt;br&gt;
        abort(401)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order = request.get_json()
supabase.table("webhook_events").upsert(
    {
        "webhook_id": request.headers.get("X-Shopify-Webhook-Id"),
        "topic": request.headers.get("X-Shopify-Topic"),
        "shopify_order_id": order.get("id"),
        "order_name": order.get("name"),
        "payload": order,
    },
    on_conflict="webhook_id",
    ignore_duplicates=True,
).execute()

return "", 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
    app.run(port=5000)&lt;br&gt;
The most important part is is_from_shopify. Your webhook address will be public, so anyone who finds it could send you a fake order. Shopify signs every webhook it sends with a secret that only you and Shopify know. The function works out what the signature should be and compares it with the one Shopify attached. If they don't match, the request is rejected with a 401 and nothing is saved.&lt;br&gt;
Two details catch beginners out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Check the signature against the raw body, not the parsed JSON. The signature was calculated on the exact bytes Shopify sent. Turning them into a Python dictionary and back can change the spacing or the order of the fields, and then the signatures never match.&lt;/li&gt;
&lt;li&gt; Use hmac.compare_digest, not ==. It takes the same time whether the first character is wrong or the last one is, so an attacker can't guess the signature piece by piece from how long each attempt takes.
The upsert with ignore_duplicates=True goes with the unique webhook_id from Step 1. If Shopify sends the same delivery twice, the second copy is simply skipped.
Step 4: Run it and make it reachable
Shopify can't send anything to a laptop sitting behind a home router, so for testing we'll use ngrok to create a public address that forwards to your computer. In one terminal:
python app.py
In a second terminal:
ngrok http 5000
ngrok prints an address that looks like &lt;a href="https://a1b2c3.ngrok-free.app" rel="noopener noreferrer"&gt;https://a1b2c3.ngrok-free.app&lt;/a&gt;. Keep both terminals open.
Step 5: Tell Shopify where to send orders
In your Shopify admin, go to Settings &amp;gt; Notifications &amp;gt; Webhooks and click Create webhook:
• Event: Order creation
• Format: JSON
• URL: your ngrok address followed by /webhooks/orders/create
Once it's saved, Shopify shows the secret your webhooks are signed with, on the same page. Copy it into SHOPIFY_WEBHOOK_SECRET in your .env file, then restart python app.py so it picks up the change.
Step 6: Send a test
Next to your new webhook, click Send test notification. Within a second or two you should see a 200 in the ngrok terminal. Open the Table Editor in Supabase and a new row will be sitting in webhook_events, with the whole order in the payload column.
One warning from my own testing. Shopify's test notification is a fixed sample order, not an empty message. It has a real-looking order number, customer and total, and the one I received was marked as voided. If your analysis reads from this table, filter it out (the payload includes "test": true) or delete the row once you've finished testing. Otherwise a fake sale ends up in your real revenue figures.
Step 7: Take it live
ngrok is fine for testing, but it stops when your laptop does. For real orders, deploy app.py to an always-on host such as Render, Railway or Fly.io. Add the same three values from .env as environment variables in the host's settings, then change the webhook URL in Shopify to your new address.
Keep one Shopify rule in mind as you build on this. Your app has five seconds to reply. If it's slower, or it's down, Shopify counts the delivery as failed and retries up to eight times over four hours. After that it stops and removes the webhook subscription altogether. So keep the listener small: check the signature, save the order, reply. Anything slower, like cleaning the data or updating reports, belongs in a separate script that reads from the table afterwards.
Where I took it next
This small listener became the front door of my store's data platform. Orders land in Supabase first, then a second script turns them into clean rows for my analysis pipeline, which feeds a live dashboard and an API. Keeping "receive" and "transform" as separate steps means that when I find a bug in the transform, I fix it and run it again over the saved orders. Nothing is lost.
If you'd like to see the full version, the code for my project is on GitHub: &lt;a href="https://github.com/sunny171p/The-Seamark-Global-Innovations-DataScience-Platform-2." rel="noopener noreferrer"&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>How to send Shopify orders straight into a cloud database with Python: a beginner's guide</title>
      <dc:creator>Sunday Emmanuel Azeez</dc:creator>
      <pubDate>Mon, 28 Sep 2026 20:45:40 +0000</pubDate>
      <link>https://dev.to/sunny171p/how-to-send-shopify-orders-straight-into-a-cloud-database-with-python-a-beginners-guide-29dh</link>
      <guid>https://dev.to/sunny171p/how-to-send-shopify-orders-straight-into-a-cloud-database-with-python-a-beginners-guide-29dh</guid>
      <description>&lt;p&gt;For the first few months of running my Shopify store, I got my sales data the slow way. I exported a CSV from the admin, dropped it into a folder and ran my analysis scripts. It worked, but the data was always out of date by the time I looked at it.&lt;/p&gt;

&lt;p&gt;Webhooks fix that. Instead of you asking Shopify for data, Shopify tells you the moment something happens. When a customer places an order, Shopify sends the full order to a web address you choose, within seconds. In this guide I'll show you how I catch those orders with a small Python app and save each one in a cloud database, where it's safe even if the app restarts.&lt;/p&gt;

&lt;p&gt;You don't need to be an experienced developer. If you can install Python packages and run a script, you can follow this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10 or newer&lt;/li&gt;
&lt;li&gt;A Shopify store where you have admin access&lt;/li&gt;
&lt;li&gt;A free &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt; account (a hosted PostgreSQL database)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ngrok.com" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;, free, for testing on your own computer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create a table to hold the orders
&lt;/h2&gt;

&lt;p&gt;In Supabase, create a new project, open the &lt;strong&gt;SQL Editor&lt;/strong&gt; and run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;webhook_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;shopify_order_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few choices here are worth explaining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;payload&lt;/code&gt; is a &lt;code&gt;jsonb&lt;/code&gt; column, which stores the whole order exactly as Shopify sent it. Shopify orders have dozens of fields. Rather than decide now which ones matter, keep everything and pick out what you need later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webhook_id&lt;/code&gt; is marked &lt;code&gt;unique&lt;/code&gt;. Shopify sometimes delivers the same webhook more than once, and this lets the database quietly ignore the repeats. We'll use it in Step 3.&lt;/li&gt;
&lt;li&gt;Turning on row level security, with no rules added, means nobody can read the table using your public API key. The script uses a private server key instead, which still works.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Set up the project
&lt;/h2&gt;

&lt;p&gt;Make a new folder and install three packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask supabase python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a file called &lt;code&gt;.env&lt;/code&gt; in the same folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SHOPIFY_WEBHOOK_SECRET=paste-this-in-step-5
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll find the URL and the service role key in Supabase under &lt;strong&gt;Project Settings &amp;gt; API&lt;/strong&gt;. The service role key has full access to your database, so treat it like a password. Never put it in front-end code, and never commit the &lt;code&gt;.env&lt;/code&gt; file to GitHub. Add &lt;code&gt;.env&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt; straight away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write the listener
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;supabase&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_client&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHOPIFY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_from_shopify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&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="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhooks/orders/create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;order_created&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;raw_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_from_shopify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Shopify-Hmac-Sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webhook_events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webhook_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Shopify-Webhook-Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Shopify-Topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shopify_order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;on_conflict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webhook_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ignore_duplicates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important part is &lt;code&gt;is_from_shopify&lt;/code&gt;. Your webhook address will be public, so anyone who finds it could send you a fake order. Shopify signs every webhook it sends with a secret that only you and Shopify know. The function works out what the signature should be and compares it with the one Shopify attached. If they don't match, the request is rejected with a &lt;code&gt;401&lt;/code&gt; and nothing is saved.&lt;/p&gt;

&lt;p&gt;Two details catch beginners out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the signature against the raw body, not the parsed JSON.&lt;/strong&gt; The signature was calculated on the exact bytes Shopify sent. Turning them into a Python dictionary and back can change the spacing or the order of the fields, and then the signatures never match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;hmac.compare_digest&lt;/code&gt;, not &lt;code&gt;==&lt;/code&gt;.&lt;/strong&gt; It takes the same time whether the first character is wrong or the last one is, so an attacker can't guess the signature piece by piece from how long each attempt takes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;upsert&lt;/code&gt; with &lt;code&gt;ignore_duplicates=True&lt;/code&gt; goes with the unique &lt;code&gt;webhook_id&lt;/code&gt; from Step 1. If Shopify sends the same delivery twice, the second copy is simply skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run it and make it reachable
&lt;/h2&gt;

&lt;p&gt;Shopify can't send anything to a laptop sitting behind a home router, so for testing we'll use ngrok to create a public address that forwards to your computer. In one terminal:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;In a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ngrok prints an address that looks like &lt;code&gt;https://a1b2c3.ngrok-free.app&lt;/code&gt;. Keep both terminals open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Tell Shopify where to send orders
&lt;/h2&gt;

&lt;p&gt;In your Shopify admin, go to &lt;strong&gt;Settings &amp;gt; Notifications &amp;gt; Webhooks&lt;/strong&gt; and click &lt;strong&gt;Create webhook&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event:&lt;/strong&gt; Order creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format:&lt;/strong&gt; JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; your ngrok address followed by &lt;code&gt;/webhooks/orders/create&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once it's saved, Shopify shows the secret your webhooks are signed with, on the same page. Copy it into &lt;code&gt;SHOPIFY_WEBHOOK_SECRET&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; file, then restart &lt;code&gt;python app.py&lt;/code&gt; so it picks up the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Send a test
&lt;/h2&gt;

&lt;p&gt;Next to your new webhook, click &lt;strong&gt;Send test notification&lt;/strong&gt;. Within a second or two you should see a &lt;code&gt;200&lt;/code&gt; in the ngrok terminal. Open the &lt;strong&gt;Table Editor&lt;/strong&gt; in Supabase and a new row will be sitting in &lt;code&gt;webhook_events&lt;/code&gt;, with the whole order in the &lt;code&gt;payload&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;One warning from my own testing. Shopify's test notification is a fixed sample order, not an empty message. It has a real-looking order number, customer and total, and the one I received was marked as voided. If your analysis reads from this table, filter it out (the payload includes &lt;code&gt;"test": true&lt;/code&gt;) or delete the row once you've finished testing. Otherwise a fake sale ends up in your real revenue figures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Take it live
&lt;/h2&gt;

&lt;p&gt;ngrok is fine for testing, but it stops when your laptop does. For real orders, deploy &lt;code&gt;app.py&lt;/code&gt; to an always-on host such as Render, Railway or Fly.io. Add the same three values from &lt;code&gt;.env&lt;/code&gt; as environment variables in the host's settings, then change the webhook URL in Shopify to your new address.&lt;/p&gt;

&lt;p&gt;Keep one Shopify rule in mind as you build on this. Your app has &lt;strong&gt;five seconds&lt;/strong&gt; to reply. If it's slower, or it's down, Shopify counts the delivery as failed and retries up to eight times over four hours. After that it stops and removes the webhook subscription altogether. So keep the listener small: check the signature, save the order, reply. Anything slower, like cleaning the data or updating reports, belongs in a separate script that reads from the table afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I took it next
&lt;/h2&gt;

&lt;p&gt;This small listener became the front door of my store's data platform. Orders land in Supabase first, then a second script turns them into clean rows for my analysis pipeline, which feeds a live dashboard and an API. Keeping "receive" and "transform" as separate steps means that when I find a bug in the transform, I fix it and run it again over the saved orders. Nothing is lost.&lt;/p&gt;

&lt;p&gt;If you'd like to see the full version, the code for my project is on GitHub: &lt;a href="https://github.com/sunny171p/The-Seamark-Global-Innovations-DataScience-Platform-2" rel="noopener noreferrer"&gt;sunny171p/The-Seamark-Global-Innovations-DataScience-Platform-2&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webhooks</category>
      <category>shopify</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
