Slack News Bot Integration Tutorial: 2026 Python Guide

Erick Horn

Erick Horn

·

19 นาที อ่านต่อ

Slack News Bot Integration Tutorial: 2026 Python Guide

Slack News Bot Integration Tutorial: News API to Block Kit Digest

A Slack news bot is a service that pulls articles from a news API on a schedule, filters them by topic or sentiment, and posts a formatted digest to a Slack channel via an incoming webhook. It is four moving parts: a Slack app with a webhook URL, a news source, a formatter, and a scheduler.

This tutorial wires those four parts into one Python script. Most existing webhook tutorials stop at "POST hello world to Slack" — you leave knowing the webhook URL exists but not how to connect it to a real news feed, schedule it on real infrastructure, or stop it from re-posting yesterday's headlines tomorrow morning.

Disclosure: I work on APITube, a news API. The pipeline below is provider-agnostic — substitute your own endpoint where shown. APITube ships sentiment and category fields on every article, which makes filtering one HTTP parameter instead of an extra NLP layer. This article is for DevOps engineers and team leads wiring a daily news digest into Slack — say, a #market-watch channel, a #pr-monitor, or a dev-ecosystem feed.

Key Takeaways

  • A Slack incoming webhook accepts roughly 1 message per second per channel. A 50-article digest uses 0.06% of the daily ceiling.
  • Use a webhook when you only post; upgrade to a Slack bot user with the Events API when users need to interact back.
  • Without deduplication, a 24-hour news window will re-post the same articles every morning. SHA-1 of the article URL is enough.
  • Block Kit with a sentiment-driven color bar makes a digest readable at a glance — green for positive coverage, red for negative.
  • The whole bot is ~80 lines of Python plus one of three schedulers: cron, GitHub Actions, or AWS Lambda + EventBridge.

What You'll Build

A Python script that runs once a day, fetches the last 24 hours of English technology news, filters by sentiment, deduplicates against the previous run, and posts each article as a Block Kit card to a Slack channel.

Build time: ~30 minutes. You'll get:

  1. A Slack app with an incoming webhook
  2. A news API call that returns structured articles
  3. Block Kit formatting with image preview and sentiment-driven color
  4. Deduplication using a SHA-1 hash of the article URL
  5. Three deployment paths: cron, GitHub Actions, AWS Lambda + EventBridge

Decision: Webhook vs Slack Bot User vs RSS App

Before you write code, pick the right transport. Most tutorials skip this — they jump straight to webhooks. But the wrong transport here costs you a refactor later.

ApproachWhen to useHard limits
Incoming webhook (this tutorial)One-way posting, one channel per webhook, no interactivityCannot read messages, cannot react, cannot DM, cannot delete
Slack bot user (Events API)Replies, slash commands, buttons, multiple channelsRequires OAuth + bot token + an event subscription URL
Built-in /feed RSS appThe source already exposes RSS and you need zero filteringNo sentiment filter, no category filter, no dedup beyond the feed

Unlike the built-in /feed app, a custom webhook bot can filter on enriched fields like sentiment polarity or article category, which means a #market-watch channel doesn't drown in unrelated posts. Rule of thumb: post-only and ≤1 channel → webhook. Need buttons, replies, or @-mentions back → upgrade to a bot.

Prerequisites

  • Python 3.9+
  • A Slack workspace where you can install apps
  • A news API key. This tutorial uses APITube (free tier: 100 requests/day, more than enough for a daily digest). Sign up at apitube.io.
  • requests library: pip install requests

Step 1 — Create the Slack App and Webhook

  1. Go to https://api.slack.com/apps → Create New App → From scratch.
  2. Name the app (e.g., News Digest Bot), pick the workspace, click Create.
  3. In the left sidebar, select Incoming Webhooks and toggle Activate Incoming Webhooks to On.
  4. Click Add New Webhook to Workspace, choose the channel (e.g., #news-digest), and authorize.
  5. Copy the URL. It looks like https://hooks.slack.com/services/T0xx/B0xx/xxxxx.

Treat that URL like a password. Slack actively revokes leaked webhook URLs, so commit it to env vars, not git.

export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T0.../B0.../..."
export APITUBE_API_KEY="your_apitube_key"

Step 2 — Fetch Structured News from the API

A daily digest needs articles from the last 24 hours, in English, ideally with sentiment and category metadata so you can filter without running your own NLP pipeline.

import os
import requests
from datetime import datetime, timedelta, timezone

API_URL = "https://api.apitube.io/v1/news/everything"

def fetch_news(api_key: str, hours: int = 24, limit: int = 10) -> list[dict]:
    since = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
    params = {
        "language.code": "en",
        "category.id": "medtop:04000000",  # technology
        "published_at.start": since,
        "per_page": limit,
        "sort.by": "published_at",
        "sort.order": "desc",
    }
    headers = {"X-API-Key": api_key}
    r = requests.get(API_URL, params=params, headers=headers, timeout=15)
    r.raise_for_status()
    return r.json().get("results", [])

A real response item looks roughly like this — note the sentiment block, that's what powers the colored bar in the Slack card:

{
  "title": "OpenAI ships new agent runtime",
  "href": "https://example.com/openai-agent",
  "description": "The new runtime lowers latency by 30%...",
  "published_at": "2026-05-02T08:14:00Z",
  "source": { "domain": "example.com" },
  "image": { "href": "https://cdn.example.com/cover.jpg" },
  "sentiment": { "overall": { "polarity": "positive", "score": 0.71 } }
}

If you're using a different news API, the schema will differ — adjust the field names in Step 3 accordingly.

Step 3 — Format Each Article as a Block Kit Card

Plain text in Slack reads like a log line. Block Kit gives you a header, source line, image, and a sentiment-colored side bar. The trick to embedding a preview image is to add an accessory of type image to the section block, then wrap the whole thing in attachments so the side color can vary per article.

SENTIMENT_COLOR = {
    "positive": "#36a64f",  # green
    "negative": "#e01e5a",  # red
    "neutral":  "#9aa0a6",  # grey
}

def article_to_blocks(a: dict) -> dict:
    polarity = a.get("sentiment", {}).get("overall", {}).get("polarity", "neutral")
    image_url = a.get("image", {}).get("href")
    blocks = [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*<{a['href']}|{a['title']}>*\n{a.get('description', '')[:240]}…",
            },
        },
        {
            "type": "context",
            "elements": [
                {"type": "mrkdwn",
                 "text": f"📰 {a['source']['domain']}  ·  "
                         f"sentiment: `{polarity}`  ·  "
                         f"{a['published_at'][:16].replace('T', ' ')} UTC"},
            ],
        },
    ]
    if image_url:
        blocks[0]["accessory"] = {
            "type": "image",
            "image_url": image_url,
            "alt_text": a["title"],
        }
    return {
        "attachments": [{
            "color": SENTIMENT_COLOR.get(polarity, "#9aa0a6"),
            "blocks": blocks,
        }]
    }

Validate the JSON in Block Kit Builder before you hit the webhook — Slack returns a generic invalid_payload and won't tell you which field is wrong.

Step 4 — Deduplicate and Respect Rate Limits

Slack throttles incoming webhooks at approximately 1 message per second per channel. The math: 86,400 seconds × 1 message/sec = 86,400 daily messages per channel maximum, so a 50-article digest uses 0.06% of budget. Bursting 200 articles in 30 seconds, however, will trigger HTTP 429. Pace the loop with time.sleep(1.1).

Deduplication is the second piece nobody covers. Without it, the 09:00 cron tomorrow re-posts every article still inside the rolling 24-hour window. Hash each article's href with SHA-1, store the hashes in a local file, and skip on collision.

import hashlib, json, time, pathlib

SEEN_FILE = pathlib.Path(".seen.json")

def load_seen() -> set[str]:
    return set(json.loads(SEEN_FILE.read_text())) if SEEN_FILE.exists() else set()

def save_seen(seen: set[str]) -> None:
    SEEN_FILE.write_text(json.dumps(list(seen)))

def fingerprint(href: str) -> str:
    return hashlib.sha1(href.encode()).hexdigest()

def post_to_slack(webhook: str, payload: dict) -> None:
    r = requests.post(webhook, json=payload, timeout=10)
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", 1)))
        r = requests.post(webhook, json=payload, timeout=10)
    r.raise_for_status()

For multi-instance deploys (Lambda warm pools, two cron hosts), replace the JSON file with Redis: SET seen:<sha1> 1 EX 604800 gives you a 7-day TTL and atomic dedup across runners.

Step 5 — Schedule the Bot

Slack does not run your code. You need an external scheduler — pick one based on where the rest of your stack lives.

Option A — cron on a Linux box

0 9 * * *  cd /opt/news-bot && /usr/bin/python3 bot.py >> /var/log/newsbot.log 2>&1

Simple, free, but state lives on one host and the host must be up.

Option B — GitHub Actions (zero infrastructure)

Save as .github/workflows/digest.yml. Store SLACK_WEBHOOK_URL and APITUBE_API_KEY as repo secrets.

name: Daily News Digest
on:
  schedule:
    - cron: '0 9 * * *'  # 09:00 UTC daily
  workflow_dispatch:
jobs:
  digest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - run: pip install requests
      - run: python bot.py
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          APITUBE_API_KEY: ${{ secrets.APITUBE_API_KEY }}
      - uses: actions/upload-artifact@v4
        with: { name: seen-cache, path: .seen.json }

The upload-artifact step persists .seen.json between runs — without it, every run starts blind and dedup breaks.

Option C — AWS Lambda + EventBridge

aws lambda create-function --function-name news-digest \
  --runtime python3.11 --handler bot.lambda_handler \
  --zip-file fileb://bot.zip \
  --environment "Variables={SLACK_WEBHOOK_URL=...,APITUBE_API_KEY=...}"

aws events put-rule --name daily-9am --schedule-expression "cron(0 9 * * ? *)"
aws events put-targets --rule daily-9am \
  --targets "Id"="1","Arn"="arn:aws:lambda:...:function:news-digest"

The Lambda filesystem is ephemeral — swap .seen.json for a DynamoDB table or S3 object before you ship.

Complete Code

import os, time
from datetime import datetime, timezone
# ... fetch_news, article_to_blocks, post_to_slack, dedup helpers above ...

def main():
    webhook = os.environ["SLACK_WEBHOOK_URL"]
    api_key = os.environ["APITUBE_API_KEY"]
    seen = load_seen()

    articles = fetch_news(api_key, hours=24, limit=10)
    fresh = [a for a in articles if fingerprint(a["href"]) not in seen]

    if not fresh:
        return

    header = {"text": f"📰 News digest · {datetime.now(timezone.utc):%Y-%m-%d}"}
    post_to_slack(webhook, header)
    time.sleep(1.1)

    for a in fresh:
        post_to_slack(webhook, article_to_blocks(a))
        seen.add(fingerprint(a["href"]))
        time.sleep(1.1)

    save_seen(seen)

if __name__ == "__main__":
    main()

That's ~80 lines for a working production bot.

Common Errors

  • invalid_payload — Block Kit JSON is malformed. Validate at Block Kit Builder.
  • channel_is_archived — the webhook is bound to a channel that someone archived. Recreate the webhook for a live channel.
  • HTTP 429 — you're posting faster than 1/sec. The retry shown in Step 4 handles transient bursts; reduce concurrency for steady throughput.
  • Empty articles list — most likely published_at.start is in the future, or the category id is wrong.
  • Image not rendering — Slack only unfurls HTTPS image URLs that respond with a valid Content-Type: image/*. CDN redirects sometimes break this.

FAQ

How do you send news updates to Slack automatically?

You send news updates to Slack automatically by creating an incoming webhook in a Slack app, writing a script that calls a news API and POSTs each article as JSON to the webhook URL, then running that script on a scheduler — cron, GitHub Actions, or an AWS Lambda triggered by EventBridge.

What is the difference between a Slack webhook and a Slack bot?

A Slack webhook is a one-way URL bound to a single channel: it can post messages but cannot read, react, or receive events. A Slack bot is a full app with a bot token that listens to the Events API, supports interactivity (buttons, slash commands), and can act across multiple channels. Use a webhook when you only post; upgrade to a bot when users need to interact back.

Can Slack receive RSS feeds without a third-party tool?

Yes. Slack's built-in /feed slash command subscribes a channel to any RSS URL. The trade-off: no filtering by sentiment or category, no deduplication beyond what the feed provides, and you cannot enrich the message with extra metadata. For a curated digest, a custom webhook bot wins.

How often can a Slack incoming webhook be called?

A Slack incoming webhook is rate-limited at approximately 1 message per second per channel, with short bursts tolerated. Sustained bursts return HTTP 429 with a Retry-After header. Pace your loop with at least 1.1 seconds between calls.

How do I post a news article with a preview image to Slack?

Build a Block Kit section block with mrkdwn text for the headline and add an accessory of type image whose image_url points at the article's cover image. Wrap the section in attachments so you can add a colored side bar that signals sentiment.

Next Steps

  1. Add a sentiment filter — only post articles where sentiment.overall.score > 0.5 to mute noise.
  2. Add a per-channel router — different webhooks for #tech, #finance, #climate, each with its own category filter.
  3. Replace the JSON dedup file with Redis or DynamoDB if you scale beyond one runner.

Try APITube free → apitube.io. Free tier covers a daily digest comfortably.

Resources

Related guides:

APITube - News API

บทความที่เกี่ยวข้อง

10 รูปแบบตัวกรอง News API ที่คุณควรรู้ (2026)</h4><h2></h2>
Developer Guides

10 รูปแบบตัวกรอง News API ที่คุณควรรู้ (2026)</h4><h2></h2>

10 รูปแบบตัวกรอง API ข่าวที่ใช้งานได้จริง — การค้นหาตามชื่อ, วันที่, เอนทิตี, ความรู้สึก, แหล่งข่าว และการสืบค้นแบบผสม — พร้อมตัวอย่าง curl, Python และ JSON พร้อมกับข้อผิดพลาดที่จะทำให้คุณเสียวันแรก

วิธีลดค่าใช้จ่าย News API: 8 กลยุทธ์ (ประหยัด 70-90%)</h4><h2></h2>
Developer Guides

วิธีลดค่าใช้จ่าย News API: 8 กลยุทธ์ (ประหยัด 70-90%)</h4><h2></h2>

คู่มือการตรวจสอบเบื้องต้นเพื่อลดค่าใช้จ่าย API ข่าว 70-90%: การสร้างลายนิ้วมือการเรียกของคุณ, การจัดระดับ TTL แคชตามความเร็วของข่าว, การสลับการดึงข้อมูลที่ตายแล้วด้วย webhooks, และการส่งออกข้อมูลย้อนหลังเป็นชุด คำนวณต้นทุนต่อ 100,000 โค้ด

เริ่มต้นใช้งาน News API: คำขอแรกของคุณใน 5 นาที</h4><h2></h2>
Developer Guides

เริ่มต้นใช้งาน News API: คำขอแรกของคุณใน 5 นาที</h4><h2></h2>

เข้าถึง News API endpoint จริงภายใน 5 นาที — curl, JavaScript และ Python เปรียบเทียบกัน พร้อมการตอบสนอง JSON ที่มีคำอธิบาย และวิธีแก้ไขข้อผิดพลาด 401/429 ที่คุณจะพบเจอ</h4><h2></h2>

Telegram News Bot ใน Python (2026): aiogram APScheduler
Developer Guides

Telegram News Bot ใน Python (2026): aiogram APScheduler

สร้าง Telegram news bot ด้วย Python โดยใช้ aiogram 3.27, APScheduler และ APITube แบบอะซิงโครนัส กรองตามความรู้สึก โพสต์อัตโนมัติลงช่อง — โค้ดเต็ม Docker

เราใช้คุกกี้

โดยการคลิก"ยอมรับ"คุณตกลงที่จะจัดเก็บคุกกี้บนอุปกรณ์ของคุณสำหรับการทำงานและการวิ