Black Friday is 27 November this year. Over that weekend nobody has time to look at every order, so I put together the automations I'd want running before it starts.
Seven of them run entirely in Shopify Flow, with no server. The eighth is a webhook receiver for the cases Flow can't handle, or for stores whose plan doesn't include Flow's HTTP action.
What you need
- Shopify Flow is a free app on the Basic, Grow, Advanced and Plus plans (Shopify Help).
- Send HTTP request, which several of these automations use, is "only available to the Shopify Plus, Advanced, or Grow plans" (docs). On Basic, use Send internal email in place of the Slack steps. For #1 and #2, use the webhook approach in #7.
- Slack alerts use a Slack incoming webhook URL. Save it as a Flow secret called
slack_webhook, so it never appears in the editor or in run logs.
Trigger and action names below are the exact labels Flow shows.
1. Flag fake and disposable signup emails
Black Friday discounts attract throwaway signups: people who create an account for the code with a temporary inbox, plus plenty of typos. If those addresses go into your Cyber Monday campaign, they bounce or never get read.
Trigger: Customer created
Action: Send HTTP request
- Method:
POST - URL:
https://mailvalid.io/api/v1/verify/single - Headers:
Content-Type: application/jsonandX-API-Key: {{secrets.mailvalid_key}} - Body:
{"email": {{ customer.email | json }}} The json filter quotes and escapes the address, so an unusual character can't break the JSON. Flow supports all of the open-source Liquid filters, and Shopify's own Flow examples use | json.
Action: Run code
Flow can't read JSON fields directly, so a small Run code step parses the response.
Input:
{ sendHttpRequest { body } } Output:
type Output { status: String! disposable: Boolean! } Code:
export default function main(input) { let r = {}; try { r = JSON.parse(input.sendHttpRequest.body).result || {}; } catch (e) { console.log("Could not parse MailValid response", e); } return { status: r.status || "unknown", disposable: r.is_disposable === true, }; } Then:
- Condition:
runCode.statusis equal toinvalid. If true, Add customer tagsemail-invalid. - Condition:
runCode.disposableis true. If true, Add customer tagsemail-disposable.
Exclude both tags from your Black Friday email segments.
A few notes:
- I tested the parser against an invalid result, a disposable result, an HTML error page and a failed response. The last two fall back to
unknowninstead of crashing the run. - Leave
catch_allandunknownuntagged. They aren't bad addresses; the check just can't be sure. MailValid doesn't charge credits forunknownresults. - In the HTTP action, set 429 to Retry. Flow keeps retrying for up to 24 hours.
(Disclosure: I work at MailValid. Any verification API with a JSON response works here; just change the field names in the Run code step.)
2. Catch typos on orders before the confirmation bounces
Guest checkouts never go through #1. If someone types gmial.com, the order confirmation and every shipping update go nowhere, and the customer opens a "where's my order?" ticket.
Trigger: Order created
Use the same Send HTTP request and Run code steps as #1, with {{ order.email | json }} in the body.
If runCode.status is invalid:
- Add order tags
check-email. - Send internal email to support with
{{ order.name }},{{ order.email }}and the shipping phone number, so someone can contact the customer.
3. Hold orders that Shopify flags as risky
Trigger: Order risk analyzed
Condition: order.risk.recommendation is equal to CANCEL or INVESTIGATE. These are two of the four values in Shopify's OrderRiskRecommendationResult; the other two are ACCEPT and NONE.
Actions:
- Hold fulfillment order. On an order-level trigger, this holds every fulfillment order on the order. Fill in a reason so whoever picks it up knows why it was held.
- Add order tags
risk-review. - Send internal email to whoever reviews fraud.
Nothing ships until someone releases the hold. That beats finding a chargeback in January.
4. Get pinged about big orders
Trigger: Order created
Condition: order.totalPriceSet.shopMoney.amount is greater than 500. Pick your own threshold.
Actions:
- Add order tags
high-value. - Send HTTP request:
POSTto{{secrets.slack_webhook}}with this body:
{"text": {{ order.name | prepend: "High-value order " | append: ": " | append: order.totalPriceSet.shopMoney.amount | append: " " | append: order.totalPriceSet.shopMoney.currencyCode | json }}} Building the whole message first and passing it through json once keeps the payload valid, whatever the order contains.
If you'd also rather check large orders by hand before they ship, Flow has a ready-made template for that: Hold fulfillments based on total order value.
5. Out-of-stock alerts (and cleanup when stock returns)
Trigger: Product variant out of stock
Actions:
- Send HTTP request to Slack:
{"text": {{ productVariant.displayName | prepend: "Out of stock: " | json }}} - Add product tags
oos, so you can filter it in the admin or hide it in a theme collection.
Second workflow: Product variant back in stock, then Remove product tags oos.
During a sale, a bestseller can sell out in minutes. An alert lets you swap the ad or the homepage banner before people click through to a sold-out page.
6. Tag first-time Black Friday buyers
Trigger: Order created
Condition: order.customer.numberOfOrders is equal to 1. Shopify defines this as "the number of orders that the customer has made at the store in their lifetime."
Action: Add customer tags bfcm26-first-order.
That gives you a clean segment for a post-holiday welcome series, kept separate from returning customers who already know you.
Place a test order first and check the run log to confirm the count already includes the new order before you rely on == 1.
7. A webhook receiver for everything Flow can't do
Flow is the easiest option, but sometimes you need your own code:
- You're on Basic, which doesn't include Send HTTP request.
- The work takes longer than Flow's 30-second HTTP timeout.
- You need to write to your own database or warehouse system.
Here's a receiver that verifies Shopify's signature, removes duplicate deliveries, answers right away and does the slow work in a background worker. It handles customers/create (#1, done server-side) and uses only the Python standard library.
import base64 import hashlib import hmac import json import os import queue import threading import urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer CLIENT_SECRET = os.environ["SHOPIFY_CLIENT_SECRET"].encode() SHOP = os.environ["SHOPIFY_SHOP"] # your-store.myshopify.com ADMIN_TOKEN = os.environ["SHOPIFY_ADMIN_TOKEN"] MAILVALID_KEY = os.environ["MAILVALID_API_KEY"] MAILVALID_URL = "https://mailvalid.io/api/v1/verify/single" ADMIN_URL = f"https://{SHOP}/admin/api/2026-10/graphql.json" jobs = queue.Queue() seen_ids = set() # use Redis or your database in production seen_lock = threading.Lock() def valid_hmac(raw_body: bytes, header: str) -> bool: digest = hmac.new(CLIENT_SECRET, raw_body, hashlib.sha256).digest() expected = base64.b64encode(digest).decode() return hmac.compare_digest(expected, header or "") class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) if not valid_hmac(raw, self.headers.get("X-Shopify-Hmac-Sha256")): self.send_response(401) self.end_headers() return webhook_id = self.headers.get("X-Shopify-Webhook-Id") with seen_lock: duplicate = webhook_id in seen_ids seen_ids.add(webhook_id) if not duplicate: jobs.put((self.headers.get("X-Shopify-Topic"), json.loads(raw))) # Answer straight away. The slow work happens in the worker. self.send_response(200) self.end_headers() def post_json(url, body, headers): req = urllib.request.Request( url, data=json.dumps(body).encode(), headers={"Content-Type": "application/json", **headers}, ) with urllib.request.urlopen(req, timeout=10) as r: return json.load(r) def check_email(email): try: data = post_json(MAILVALID_URL, {"email": email}, {"X-API-Key": MAILVALID_KEY}) return data["result"] except Exception as e: print("verification failed, skipping:", e) return None def add_tags(gid, tags): mutation = """ mutation tagsAdd($id: ID!, $tags: [String!]!) { tagsAdd(id: $id, tags: $tags) { userErrors { field message } } }""" return post_json( ADMIN_URL, {"query": mutation, "variables": {"id": gid, "tags": tags}}, {"X-Shopify-Access-Token": ADMIN_TOKEN}, ) def handle_new_customer(customer): email = (customer.get("email") or "").strip() if not email: return result = check_email(email) if result is None: return tags = [] if result["status"] == "invalid": tags.append("email-invalid") if result.get("is_disposable"): tags.append("email-disposable") if tags: add_tags(customer["admin_graphql_api_id"], tags) def worker(): while True: topic, payload = jobs.get() try: if topic == "customers/create": handle_new_customer(payload) except Exception as e: print("job failed:", topic, e) finally: jobs.task_done() if __name__ == "__main__": threading.Thread(target=worker, daemon=True).start() ThreadingHTTPServer(("0.0.0.0", 8000), WebhookHandler).serve_forever() Why it's built this way
All of the following comes from Shopify's webhook docs:
- Answer fast. Shopify has "a one-second connection timeout and a five-second timeout for the entire request." A MailValid call plus a GraphQL mutation can take longer than that, so the handler only queues the job and returns 200. In my local test it answered in 11 ms.
- Failures add up. If a delivery fails, Shopify "retries 8 times over the next 4 hours." For subscriptions created through the Admin API, "after 8 consecutive failures, the subscription is automatically deleted." Losing your webhook on Black Friday because the endpoint was slow is a bad way to find out.
- Duplicates happen. Use
X-Shopify-Webhook-Idto skip repeat deliveries. In production, keep the IDs in Redis or a database, not in memory. - Verify the signature. Compute HMAC-SHA256 over the raw body with your app's client secret, base64-encode it, and compare it with
X-Shopify-Hmac-Sha256using a constant-time comparison. If you parse the JSON before checking the signature, the bytes can change and the check fails.
Subscribing
If your app uses the Shopify CLI, add the subscription to shopify.app.toml:
[webhooks] api_version = "2026-10" [[webhooks.subscriptions]] topics = ["customers/create"] uri = "https://your-domain.com/webhooks" Customer topics include protected customer data, so your app needs that data access approved before Shopify sends them. The tagsAdd mutation needs the write_customers scope.
How I tested it
I sent signed requests to the receiver, with MailValid and the Shopify Admin API replaced by local mock servers:
- A valid signature returned 200.
- A wrong signature returned 401.
- A repeated
X-Shopify-Webhook-Idwas queued only once. - An invalid address was tagged
email-invalid, a disposable one was taggedemail-disposable, and a valid one wasn't tagged.
I haven't run it against a live store. Try it on a development store first.
The in-memory queue also loses jobs if the process restarts. For real traffic, use a proper queue (SQS, Redis, or whatever you already have).
8. Get alerted when an automation breaks
This is the automation for everything above. If your Flow workflows fail quietly during the busiest weekend of the year, you'll find out in December.
Trigger: Workflow error occurred
Action: Send HTTP request to Slack, or Send internal email on Basic. Use Add variable to include the workflow name and the error.
For the webhook receiver, monitor the endpoint itself as well. Shopify also sends warning emails about failing deliveries to your app's emergency developer email address, so make sure someone actually reads that inbox.
Checklist for the week before
- Run every workflow against a test order or test customer, and read the run log.
- For each Send HTTP request action, decide how 4XX, 5XX and 429 errors are handled (Retry, Fail or Ignore). If you choose Fail, #8 will tell you about it.
- Keep secrets as Flow secrets, not pasted into the body.
- Make sure your Black Friday email segments exclude
email-invalidandemail-disposable. - Send a test webhook to the receiver and confirm it returns 200 well inside the 5-second limit.
- Decide who gets the Slack alerts, and who's covering them on Friday night.
If you're also worried about your emails reaching the inbox over the holidays, I wrote a separate guide on holiday email deliverability.
Which automation would you add? I'm especially curious what people run on Basic without the HTTP action.
