# Price Drop Alerts to Slack or Email with a Scraper

> Get price drop alerts in Slack or email: a scheduled change-only scrape, Apify's Slack and Gmail integrations, and a tested webhook handler with a threshold.

- URL: https://datatooly.xyz/guides/price-drop-alerts-email-slack/
- Author: Omar Eldeeb (https://datatooly.xyz/about/)
- Published: 2026-10-20
- Updated: 2026-10-20
- Free tool: [E-commerce price tracker — scrape product prices, stock and variants](https://datatooly.xyz/ecommerce-price-tracker/)
- Tags: ecommerce, python, slack, webscraping

To get price drop alerts, run a scraper on a schedule in change-only mode, then notify on the finished run. On Apify: schedule [E-commerce Product Scraper Pro](https://apify.com/constructive_calm/ecommerce-product-scraper-pro?fpr=v77kxu) with `monitorMode` and `onlyChanges`, and either add the built-in Slack or Gmail integration (no code, every run) or a webhook to a small handler that posts only drops above your threshold.

This page covers both, with a webhook handler I tested end-to-end on 2026-10-20 against a real run that contained a real price drop.

## Step 1: a run that only outputs changes

The alert is only as good as the diff behind it. Save this as a task (add your own product URLs; the listing URL here is the collection used for the tests below):

```json
{
  "listingUrls": [{ "url": "https://www.skullcandy.com/collections/headphones" }],
  "maxItemsPerStartUrl": 5,
  "maxProductResults": 5,
  "monitorMode": true,
  "onlyChanges": true
}
```

The first run stores a baseline and returns every product as `new`. From the second run on, unchanged products aren't written (they're billed as a cheaper "product checked" event), and each changed product row carries `changeType` (`price-down`, `price-up`, `back-in-stock`, `out-of-stock`, `changed`), `previousPrice`, `priceChange` and `priceChangePct`.

I ran exactly this input on 2026-10-20 against a baseline from 2026-10-18 (run `heds8zid9ccytKKhQ`). It billed 3 products as checked-unchanged and wrote 2 rows: Aviator 900 ANC `price-down` from $299.99 to $229.99 (−23.33%), and Crusher ANC 2 `price-up` from $169.99 to $239.99.

Then add a schedule to the task. [Apify schedules](https://docs.apify.com/platform/schedules?fpr=v77kxu) use cron with a timezone setting; `0 */4 * * *` checks every four hours (6 runs a day).

## Option A: Slack or Gmail integration, no code

On the task's **Integrations** tab you can connect:

- **Slack.** Click Configure on the Slack integration, log in, pick the workspace, channel, events and a message template. The template is Handlebars: per [Apify's Slack docs](https://docs.apify.com/platform/integrations/slack?fpr=v77kxu), you can use `userId`, `eventType`, `createdAt`, `eventData` and `resource`, with dot notation for nested fields. A useful template:
  `Price check finished: {{resource.status}}. Changed products: dataset {{resource.defaultDatasetId}}`
- **Gmail.** "Send results email via Gmail" emails the run's results after a successful run and, per the [Gmail integration docs](https://docs.apify.com/platform/integrations/gmail?fpr=v77kxu), can attach the dataset in several formats. With `onlyChanges`, the attachment is exactly the list of changed products.

The limit: both fire on every successful run, including runs where nothing changed or everything went up. Fine for a daily digest, noisy every few hours. For "only tell me about real drops", use option B.

## Option B: a webhook to your own handler

An Apify webhook sends an HTTP POST to your URL when an event happens, for example `ACTOR.RUN.SUCCEEDED`. Add it on the same Integrations tab. Facts from the [webhook actions docs](https://docs.apify.com/platform/integrations/webhooks/actions?fpr=v77kxu) that shape the handler:

- The default payload has the keys `userId`, `createdAt`, `eventType`, `eventData` and `resource`, and for run events `resource` is the run object, the same as the Get run API returns. So `resource.defaultDatasetId` is where the changed rows are.
- Requests time out after 2 minutes, so respond first and do slow work after.
- A non-2xx answer is retried with exponential backoff (about 1, 2, 4 minutes and so on, up to 11 retries).
- A webhook can be delivered more than once, so the handler must be idempotent.

Here is the handler. It reads the run's dataset through the Apify API, keeps `price-down` rows at or past your threshold plus `back-in-stock` rows, and posts a Slack message through an [incoming webhook](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks). Python standard library only:

```python
"""Receive an Apify webhook, read the run's changed products, post a Slack message. Python 3.9+, stdlib only."""
import json
import os
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

APIFY_TOKEN = os.environ["APIFY_TOKEN"]
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL")   # unset = print the message instead
MIN_DROP_PCT = float(os.environ.get("MIN_DROP_PCT", "5"))
seen_runs = set()                                          # Apify may deliver a webhook more than once


def dataset_items(dataset_id):
    url = f"https://api.apify.com/v2/datasets/{dataset_id}/items?clean=1"
    req = urllib.request.Request(url, headers={"Authorization": f"Bearer {APIFY_TOKEN}"})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)


def alert_lines(rows):
    lines = []
    for row in rows:
        if row.get("recordType") != "product" or "identity-changed" in (row.get("changes") or []):
            continue
        pct = row.get("priceChangePct")
        if row.get("changeType") == "price-down" and pct is not None and -pct >= MIN_DROP_PCT:
            lines.append(f"*{row['name']}* {row['previousPrice']:.2f} → {row['price']:.2f} {row['currency']}"
                         f" ({pct:+.1f}%) <{row['url']}|{row['site']}>")
        elif row.get("changeType") == "back-in-stock":
            lines.append(f"*{row['name']}* is back in stock at {row['price']} {row['currency']} <{row['url']}|{row['site']}>")
    return lines


def slack_payload(run, lines):
    text = f"{len(lines)} price alert(s) from run {run['id']}"
    return {"text": text,                                   # fallback for notifications
            "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": text}}]
                      + [{"type": "section", "text": {"type": "mrkdwn", "text": line}} for line in lines[:45]]}


def send(payload):
    if not SLACK_WEBHOOK_URL:
        print(json.dumps(payload, indent=1, ensure_ascii=False))
        return
    req = urllib.request.Request(SLACK_WEBHOOK_URL, data=json.dumps(payload).encode(),
                                 headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=30).read()


class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        run = body["resource"]                              # the run object, from the default payload
        self.send_response(200)                             # answer fast; Apify times out after 2 minutes
        self.end_headers()
        if body.get("eventType") != "ACTOR.RUN.SUCCEEDED" or run["id"] in seen_runs:
            return
        seen_runs.add(run["id"])
        lines = alert_lines(dataset_items(run["defaultDatasetId"]))
        print(f"run {run['id']}: {len(lines)} alert(s)")
        if lines:
            send(slack_payload(run, lines))


if __name__ == "__main__":
    HTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8000"))), Hook).serve_forever()
```

Notes on the choices:

- **Rows with `identity-changed` are skipped.** If a store starts serving a different product at a watched URL, the actor reports that once instead of a fake price change. Don't alert on it as a drop.
- **The threshold uses `priceChangePct`**, which the actor computes against the stored previous price (same currency only). Set `MIN_DROP_PCT` per channel.
- **`lines[:45]`** keeps the message under Slack's limit of 50 blocks per message ([Block Kit reference](https://docs.slack.dev/reference/block-kit/blocks)).
- **`seen_runs` lives in memory.** For a long-running service, store processed run IDs in a file or database so a restart doesn't re-alert.

### How I tested it (no real Slack needed)

I ran a local mock that prints whatever is POSTed to it, started the handler with `SLACK_WEBHOOK_URL=http://127.0.0.1:9001/`, and POSTed a webhook body built the same way Apify builds the default payload, with `resource` set to the real run object of `heds8zid9ccytKKhQ`. I sent it twice to check duplicate handling. The output of curl, the handler and the mock, with the JSON compacted:

```text
POST 1 -> 200
POST 2 -> 200
run heds8zid9ccytKKhQ: 1 alert(s)
MOCK SLACK RECEIVED: {
 "text": "1 price alert(s) from run heds8zid9ccytKKhQ",
 "blocks": [
  { "type": "section", "text": { "type": "mrkdwn", "text": "1 price alert(s) from run heds8zid9ccytKKhQ" } },
  { "type": "section", "text": { "type": "mrkdwn",
    "text": "*Aviator® 900 ANC* 299.99 → 229.99 USD (-23.3%) <https://www.skullcandy.com/products/aviator-900-anc-headphones|skullcandy.com>" } }
 ]
}
```

One alert, sent once. The `price-up` row in the same run was correctly ignored. Leave `SLACK_WEBHOOK_URL` unset and the handler prints the payload instead, which is the easiest way to try it. To deploy, put it behind HTTPS on any small server or serverless function and paste that URL into the webhook's request URL.

### Email instead of Slack

Swap `send()` for your mail provider's API, or keep Slack and add the Gmail integration for a daily digest. The filtering logic (`alert_lines`) doesn't change.

## What alerts cost

You pay for the runs, not the alerts. From the actor's pricing table (pay per event; FREE and GOLD are Apify plan tiers):

| Event | FREE | GOLD |
|---|---|---|
| Product checked, unchanged | $0.0012 | $0.0002 |
| Product scraped (a changed row) | $0.0048 | $0.0008 |
| Actor start, per run | $0.005 | $0.005 |

The test run above: 1 start + 1 listing page ($0.0004 FREE / $0.00021 GOLD) + 2 changed rows + 3 checks = **$0.0186 on FREE, $0.00741 on GOLD**. For 200 product URLs checked every four hours with nothing changing, one run is 200 × $0.0012 + $0.005 = $0.245 on FREE or 200 × $0.0002 + $0.005 = $0.045 on GOLD, times 6 runs a day. Each product that does change costs $0.0036 more on FREE ($0.0006 on GOLD) than an unchanged check. Sites reached through residential proxies also bill traffic per MB (see the README). Apify's free plan includes $5 of usage a month with no credit card; the actor has no in-actor free trial.

## During Black Friday and the holiday season

Two things change in the sale window. You'll want faster checks, and you'll get far more alerts. A temporary schedule `0 * 23-30 11 *` runs hourly from Monday 23 November to Monday 30 November 2026 (Cyber Monday): 192 runs in `America/New_York`. For 200 unchanged products that is 192 × $0.245 = $47.04 on FREE or 192 × $0.045 = $8.64 on GOLD, before change rows. Raise `MIN_DROP_PCT` to 15-20% for that week, or the channel turns into a feed, and remember a "drop" is measured against your stored previous price, not the retailer's "was" price. Start the baseline in October so that comparison means something. Timing by retailer is in the [holiday sales calendar](https://datatooly.xyz/guides/holiday-sales-calendar-price-tracking/), and [how to spot fake Black Friday discounts](https://datatooly.xyz/guides/spot-fake-black-friday-discounts/) covers the baseline maths.

Related: [competitor price monitoring](https://datatooly.xyz/guides/competitor-price-monitoring-scraper/) (watchlists and GTIN matching), [back-in-stock alerts](https://datatooly.xyz/guides/back-in-stock-alert-scraper/), and the [e-commerce price tracker query builder](https://datatooly.xyz/ecommerce-price-tracker/), which assembles the monitor input for you.

*Disclosure: I built the actor and this site. The webhook handler works with any job that writes rows with a previous and current price.*

Sources: [Apify docs: webhook actions](https://docs.apify.com/platform/integrations/webhooks/actions?fpr=v77kxu) (payload, retries, timeout, duplicates), [Apify docs: webhook events](https://docs.apify.com/platform/integrations/webhooks/events?fpr=v77kxu), [Apify docs: Slack integration](https://docs.apify.com/platform/integrations/slack?fpr=v77kxu), [Apify docs: Gmail integration](https://docs.apify.com/platform/integrations/gmail?fpr=v77kxu), [Apify docs: Schedules](https://docs.apify.com/platform/schedules?fpr=v77kxu), [Slack: sending messages using incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks), [Slack Block Kit blocks](https://docs.slack.dev/reference/block-kit/blocks), and the actor's README.

## Quick answers

### How do I get an alert when a price drops on a website?

Schedule a scraper to re-check the product page, compare each price with the last one it saw, and send a message only when the new price is lower by more than your threshold. Browser extensions do this for single products on a few big stores; a scheduled scraper does it for any list of supported pages and sends the alert where your team works.

### Can Apify send a Slack message when a run finishes?

Yes. The Slack integration on an Actor's or task's Integrations tab posts a templated message to a channel on events you pick, such as run succeeded. The template can use run fields like `{{resource.defaultDatasetId}}`, but it can't filter rows, so use a webhook to your own code if you only want messages about real drops.

### Why did I get the same price alert twice?

Apify's docs say a webhook can occasionally be delivered more than once, and two overlapping runs of the same watchlist can both report the same change. Remember the run IDs you've already processed and don't schedule runs of the same list so close together that they overlap.

### What percentage price drop should trigger an alert?

Start around 5-10% for regular monitoring so rounding and small promotions don't flood the channel, and lower it only for the few products where any cut matters. During big sale weeks, raise it, because many products move at once.

## Related guides

- [Retail Sales Calendar 2026-2027: When to Track Prices](https://datatooly.xyz/guides/holiday-sales-calendar-price-tracking/)
- [Amazon Price History Without Keepa: Build Your Own](https://datatooly.xyz/guides/amazon-price-history-without-keepa/)
- [Amazon Price Scraper: Prices by ASIN on Any Marketplace](https://datatooly.xyz/guides/amazon-price-scraper-asin/)
- [Back in Stock Alert Scraper: Products, Sizes, Colours](https://datatooly.xyz/guides/back-in-stock-alert-scraper/)
- [Back to School Price Tracker: Tax Holidays & Laptops](https://datatooly.xyz/guides/back-to-school-price-tracker/)
- [Compare Black Friday Prices Across Stores (Google Shopping)](https://datatooly.xyz/guides/black-friday-google-shopping-price-comparison/)
- [Black Friday Price Tracker: Watch 2026 Prices Daily](https://datatooly.xyz/guides/black-friday-price-tracker/)
- [Boxing Day Sales Tracker UK 2026: Prices in GBP](https://datatooly.xyz/guides/boxing-day-sales-price-tracker-uk/)
- [Christmas Price Tracker: When to Buy Gifts Cheapest](https://datatooly.xyz/guides/christmas-price-tracker/)
- [Competitor Price Monitoring: Scraper Setup and Cost](https://datatooly.xyz/guides/competitor-price-monitoring-scraper/)
- [Cyber Monday Price Tracker: Hourly Checks for Nov 30](https://datatooly.xyz/guides/cyber-monday-deal-tracker/)
- [Grocery Price Tracker: Build a Basket Index in Python](https://datatooly.xyz/guides/grocery-price-tracker-scraper/)
- [MAP Monitoring: Find Minimum Advertised Price Violations](https://datatooly.xyz/guides/map-price-monitoring/)
- [Prime Day Price Tracker: ASINs, Big Deal Days, 2027](https://datatooly.xyz/guides/prime-day-price-tracker/)
- [Ramadan Sale Price Tracker 2027: Noon & Amazon UAE](https://datatooly.xyz/guides/ramadan-eid-sales-price-tracking/)
- [Scrape Product Prices with Python from JSON-LD](https://datatooly.xyz/guides/scrape-product-prices-json-ld-python/)
- [Scrape Product Reviews from Walmart, Target and Amazon](https://datatooly.xyz/guides/scrape-product-reviews-walmart-target-amazon/)
- [Scrape Product Variants: Sizes, Colors and Price per SKU](https://datatooly.xyz/guides/scrape-product-variants-sizes-colors/)
- [Fake Black Friday Deals: Check the Real Discount](https://datatooly.xyz/guides/spot-fake-black-friday-discounts/)
- [Apify E-commerce Scraping Tool Alternative: Same Input](https://datatooly.xyz/guides/switch-from-apify-ecommerce-scraping-tool/)
- [When Do Black Friday Prices Drop? The 2026 Timeline](https://datatooly.xyz/guides/thanksgiving-black-friday-price-drop-timeline/)
- [Walmart and Target Price Scraper: Search and Product Pages](https://datatooly.xyz/guides/walmart-target-price-scraper/)
- [White Friday & 11.11 Tracker: Noon and Amazon UAE Prices](https://datatooly.xyz/guides/white-friday-11-11-noon-amazon-uae-prices/)
- [Shopify products.json Pagination: 250 Limit and the 25,000 Cap](https://datatooly.xyz/guides/shopify-products-json-pagination/)
- [AI Share of Voice: Measure It in ChatGPT and Perplexity](https://datatooly.xyz/guides/ai-share-of-voice-chatgpt-perplexity/)
