Summary
- Connect Promptwatch's webhook API to Slack to receive real-time alerts when AI engines cite your brand
- Set up incoming webhooks in Slack and configure Promptwatch to push citation events to your workspace
- Build a simple Flask or Express server to process webhook payloads and format messages for Slack
- Deploy your bot as a background worker on platforms like Render to handle persistent connections
- Extend the bot with interactive buttons, threading, and custom formatting for better team collaboration
Why you need a real-time AI citation bot
Your brand just got mentioned in a ChatGPT response. A competitor got cited instead of you in Perplexity. Someone asked Claude for product recommendations and your name didn't come up. By the time you check your dashboard tomorrow morning, the moment has passed.
This is the problem with batch monitoring. AI search visibility changes constantly -- new prompts, new citations, new competitors appearing in answers. If you're only checking once a day or once a week, you're flying blind. A real-time Slack bot fixes this by pushing alerts the moment something happens. Your team sees the citation, the context, and the AI model that generated it. You can react immediately instead of discovering problems days later.
Promptwatch tracks brand mentions across ChatGPT, Perplexity, Claude, Gemini, and 10+ other AI engines. Its webhook API lets you pipe citation events directly into Slack, Discord, or any other system that accepts HTTP POST requests. This guide shows you how to build the Slack integration from scratch.

Understanding webhook architecture for AI monitoring
Webhooks are HTTP callbacks -- your application registers a URL, and Promptwatch sends a POST request to that URL whenever a citation event occurs. The payload contains everything you need: the prompt that triggered the citation, which AI model responded, the exact text of the citation, your visibility score, and metadata about competing brands.
Slack's incoming webhook system works the same way in reverse. You create a webhook URL in your Slack workspace, then POST formatted JSON to that URL. Slack receives the payload and renders it as a message in the channel you specified.
Your bot sits in the middle. It receives webhook events from Promptwatch, processes the data, formats it for human readability, and forwards it to Slack. The flow looks like this:
- User asks ChatGPT a question
- ChatGPT cites your brand in the response
- Promptwatch detects the citation and sends a webhook POST to your server
- Your server receives the payload, extracts relevant fields, and formats a Slack message
- Your server POSTs the formatted message to Slack's incoming webhook URL
- Slack displays the message in your designated channel
This happens in seconds. The entire pipeline from citation to Slack notification takes under 5 seconds in most cases.

Setting up your Slack workspace and webhook
Before you write any code, you need a Slack webhook URL. This is the endpoint your bot will POST messages to.
Head to api.slack.com/apps and click "Create New App". Choose "From scratch" and give your app a recognizable name like "Promptwatch Citations Bot". Select the workspace where you want alerts to appear.
Once your app is created, navigate to "Incoming Webhooks" in the left sidebar under Features. Toggle the switch to "On". Scroll down and click "Add New Webhook to Workspace". Slack will prompt you to select a channel -- pick something like #ai-visibility or #marketing-alerts. Click "Allow".
Slack generates a webhook URL that looks like https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX. Copy this URL and store it somewhere secure. Anyone with this URL can post messages to your Slack channel, so treat it like a password. Never hardcode it in public repositories.
Test the webhook by sending a curl request:
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Test message from Promptwatch bot"}' \
YOUR_WEBHOOK_URL
If you see "Test message from Promptwatch bot" appear in your Slack channel, the webhook is working.
Configuring Promptwatch webhooks
Log into your Promptwatch account and navigate to Settings > Integrations > Webhooks. Click "Add Webhook" and enter the URL where your bot will listen for events. This is NOT your Slack webhook URL -- this is the public endpoint where Promptwatch will send citation data.
If you're developing locally, you'll need a tool like ngrok to expose your local server to the internet. Run ngrok http 5000 (assuming your bot runs on port 5000) and copy the HTTPS URL ngrok provides. Use that as your webhook URL in Promptwatch.
Configure which events you want to receive. For a citation bot, you'll want:
- New citation detected
- Citation lost (your brand stopped appearing for a prompt)
- Visibility score change
- Competitor citation detected
Promptwatch will send a POST request to your webhook URL every time one of these events occurs. The payload is JSON and includes fields like event_type, prompt_text, ai_model, citation_text, visibility_score, and competitors_cited.
Save your webhook configuration. Promptwatch will send a test event to verify the endpoint is reachable. If your server isn't running yet, the test will fail -- that's fine, we'll build the server next.
Building the webhook receiver (Flask example)
Your bot needs a web server to receive webhook POSTs from Promptwatch. Flask is a lightweight Python framework that makes this straightforward.
Install Flask and the requests library:
pip install flask requests
Create a file called bot.py:
from flask import Flask, request, jsonify
import requests
import os
app = Flask(__name__)
SLACK_WEBHOOK_URL = os.getenv('SLACK_WEBHOOK_URL')
@app.route('/webhook', methods=['POST'])
def handle_promptwatch_webhook():
payload = request.json
event_type = payload.get('event_type')
prompt_text = payload.get('prompt_text')
ai_model = payload.get('ai_model')
citation_text = payload.get('citation_text')
visibility_score = payload.get('visibility_score')
if event_type == 'new_citation':
slack_message = {
"text": f"🎯 New citation detected in {ai_model}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*New citation in {ai_model}*\n\n*Prompt:* {prompt_text}\n\n*Citation:* {citation_text}\n\n*Visibility Score:* {visibility_score}/100"
}
}
]
}
response = requests.post(SLACK_WEBHOOK_URL, json=slack_message)
if response.status_code != 200:
return jsonify({'error': 'Failed to send Slack message'}), 500
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Set your Slack webhook URL as an environment variable:
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
Run the server:
python bot.py
Your bot is now listening on port 5000. When Promptwatch sends a webhook event, the /webhook endpoint receives it, extracts the relevant fields, formats a Slack message using Slack's Block Kit syntax, and POSTs it to your Slack webhook URL.
Building the webhook receiver (Node.js/Express example)
If you prefer JavaScript, here's the same bot in Node.js using Express:
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
app.post('/webhook', async (req, res) => {
const payload = req.body;
const eventType = payload.event_type;
const promptText = payload.prompt_text;
const aiModel = payload.ai_model;
const citationText = payload.citation_text;
const visibilityScore = payload.visibility_score;
if (eventType === 'new_citation') {
const slackMessage = {
text: `🎯 New citation detected in ${aiModel}`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*New citation in ${aiModel}*\n\n*Prompt:* ${promptText}\n\n*Citation:* ${citationText}\n\n*Visibility Score:* ${visibilityScore}/100`
}
}
]
};
try {
await axios.post(SLACK_WEBHOOK_URL, slackMessage);
} catch (error) {
console.error('Failed to send Slack message:', error);
return res.status(500).json({ error: 'Failed to send Slack message' });
}
}
res.status(200).json({ status: 'ok' });
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Bot listening on port ${PORT}`);
});
Install dependencies:
npm install express axios
Run the server:
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" node bot.js
Both implementations do the same thing. Choose whichever language you're more comfortable with.
Deploying your bot to production
Running your bot locally works for testing, but you need a production deployment for 24/7 uptime. Render, Heroku, Railway, and Fly.io all support simple webhook servers.
For Render, create a new Web Service and connect your GitHub repository. Render detects Flask or Express automatically and builds your app. Add your SLACK_WEBHOOK_URL as an environment variable in the Render dashboard.
For Discord bots or more complex integrations, you'll need a background worker instead of a web service. Discord requires persistent WebSocket connections, which don't fit the stateless HTTP model. Render's background workers handle this by keeping a long-running process alive.
If you're only building a Slack bot with incoming webhooks, a standard web service is fine. Your bot receives HTTP POSTs, processes them, and responds -- no persistent connections needed.
Once deployed, update your Promptwatch webhook URL to point at your production server (e.g. https://your-bot.onrender.com/webhook). Promptwatch will start sending events to the live endpoint.
Formatting Slack messages with Block Kit
The basic text messages work, but Slack's Block Kit lets you create richer, more interactive notifications. You can add buttons, dividers, context blocks, and images.
Here's an enhanced message format that includes a button linking to the full citation details in Promptwatch:
slack_message = {
"text": f"New citation detected in {ai_model}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"🎯 New citation in {ai_model}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Prompt:*\n{prompt_text}"
},
{
"type": "mrkdwn",
"text": f"*Visibility Score:*\n{visibility_score}/100"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Citation:*\n>{citation_text}"
}
},
{
"type": "divider"
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View in Promptwatch"
},
"url": f"https://promptwatch.com/citations/{payload.get('citation_id')}",
"style": "primary"
}
]
}
]
}
This creates a structured message with a header, field layout, quoted citation text, and a button that opens the citation details in Promptwatch. The button uses a deep link to the specific citation so your team can see the full context, competing brands, and historical visibility trends.
You can also add conditional formatting based on event type. Lost citations get a red warning, new citations get a green checkmark, competitor citations get a yellow alert.
Handling different event types
Promptwatch sends multiple event types through the same webhook endpoint. Your bot should handle each one differently.
@app.route('/webhook', methods=['POST'])
def handle_promptwatch_webhook():
payload = request.json
event_type = payload.get('event_type')
if event_type == 'new_citation':
send_new_citation_alert(payload)
elif event_type == 'citation_lost':
send_citation_lost_alert(payload)
elif event_type == 'competitor_citation':
send_competitor_alert(payload)
elif event_type == 'visibility_score_change':
send_score_change_alert(payload)
return jsonify({'status': 'ok'}), 200
def send_new_citation_alert(payload):
# Format and send new citation message
pass
def send_citation_lost_alert(payload):
# Format and send lost citation warning
pass
def send_competitor_alert(payload):
# Format and send competitor citation alert
pass
def send_score_change_alert(payload):
# Format and send visibility score change notification
pass
Each function formats the Slack message differently. Lost citations might use a red color scheme and include suggestions for fixing the issue. Competitor alerts might tag specific team members who need to respond. Score changes might only notify if the change exceeds a threshold (e.g. +/- 10 points).
Adding threading and context
Slack threads keep related messages grouped together. If a citation gets updated or lost later, you can reply to the original message instead of creating a new top-level notification.
To use threading, you need to store the ts (timestamp) value Slack returns when you post a message. This requires a database or key-value store to map citation IDs to Slack message timestamps.
Here's a simplified example using an in-memory dictionary (in production, use Redis or a proper database):
message_cache = {}
def send_new_citation_alert(payload):
citation_id = payload.get('citation_id')
slack_message = {
# ... message formatting ...
}
response = requests.post(SLACK_WEBHOOK_URL, json=slack_message)
response_data = response.json()
# Store the message timestamp for future threading
message_cache[citation_id] = response_data.get('ts')
def send_citation_lost_alert(payload):
citation_id = payload.get('citation_id')
original_ts = message_cache.get(citation_id)
slack_message = {
"text": "Citation lost",
"thread_ts": original_ts # Reply to the original message
}
requests.post(SLACK_WEBHOOK_URL, json=slack_message)
This creates a conversation thread where the initial citation alert is the parent message and subsequent updates (lost citation, score changes) are replies. Your team sees the full lifecycle of each citation in one place.
Filtering and prioritization
Not every citation deserves an immediate alert. If you're tracking 500 prompts across 10 AI models, you'll get flooded with notifications. Add filtering logic to only alert on high-priority events.
Example filters:
- Only alert for citations in high-volume prompts (e.g. prompts with 1000+ monthly searches)
- Only alert when visibility score drops below a threshold (e.g. below 50/100)
- Only alert for citations in specific AI models (e.g. ChatGPT and Perplexity, skip Mistral)
- Only alert during business hours (suppress notifications on weekends)
- Only alert when a top competitor gets cited instead of you
def should_send_alert(payload):
prompt_volume = payload.get('prompt_volume', 0)
visibility_score = payload.get('visibility_score', 100)
ai_model = payload.get('ai_model')
# Skip low-volume prompts
if prompt_volume < 1000:
return False
# Skip if visibility is still high
if visibility_score > 70:
return False
# Only alert for priority models
if ai_model not in ['ChatGPT', 'Perplexity', 'Claude']:
return False
return True
@app.route('/webhook', methods=['POST'])
def handle_promptwatch_webhook():
payload = request.json
if not should_send_alert(payload):
return jsonify({'status': 'filtered'}), 200
# Process and send alert
# ...
You can also route different event types to different Slack channels. High-priority alerts go to #marketing-urgent, routine updates go to #ai-visibility-log, competitor alerts go to #competitive-intel.
Extending the bot with interactive actions
Slack's interactive components let users take action directly from the notification. Add buttons like "Investigate", "Mark as resolved", or "Create task" that trigger follow-up workflows.
This requires switching from incoming webhooks to Slack's full Events API and Socket Mode, which is more complex but enables bidirectional communication. Your bot can listen for button clicks, respond to slash commands, and update messages dynamically.
For a production bot, consider using a framework like Slack's Bolt SDK (available in Python, JavaScript, and Java) instead of raw HTTP requests. Bolt handles authentication, event routing, and interactive components automatically.
If you don't want to write code at all, tools like Zapier or Make can connect Promptwatch webhooks to Slack with a visual workflow builder. You lose some flexibility but gain speed -- you can have a working bot in 10 minutes.
Monitoring and debugging your bot
Webhooks fail silently. If your server is down or returns an error, Promptwatch will retry a few times and then give up. You won't know unless you're actively monitoring.
Add logging to your bot:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route('/webhook', methods=['POST'])
def handle_promptwatch_webhook():
payload = request.json
logger.info(f"Received webhook event: {payload.get('event_type')}")
try:
# Process event
pass
except Exception as e:
logger.error(f"Failed to process webhook: {e}")
return jsonify({'error': str(e)}), 500
logger.info("Successfully processed webhook")
return jsonify({'status': 'ok'}), 200
Set up health checks in your deployment platform. Render, Heroku, and others can ping a /health endpoint every few minutes to verify your bot is alive. If the health check fails, you get an email alert.
Use a service like Sentry or Rollbar to capture exceptions and track error rates. If your bot starts failing, you'll know immediately instead of discovering it days later when someone asks why they stopped getting alerts.
Comparison: webhook-based vs polling-based monitoring
| Approach | Latency | Server load | Complexity | Cost |
|---|---|---|---|---|
| Webhooks (this guide) | Real-time (seconds) | Low -- only runs when events occur | Medium -- requires public endpoint | Free -- no API rate limits |
| Polling (checking API every N minutes) | Delayed (minutes to hours) | High -- constant API requests | Low -- simple cron job | Higher -- API rate limits apply |
| Slack's Events API + Socket Mode | Real-time | Medium -- persistent WebSocket | High -- requires event handling | Free |
| Third-party automation (Zapier) | Real-time | None -- fully managed | Very low -- visual builder | Paid -- Zapier subscription |
Webhooks are the best approach for real-time alerts. Polling works if you only need hourly or daily summaries. Zapier is fastest to set up but costs money and gives you less control.
Real-world use cases
Teams use AI citation bots for different workflows:
Content teams: Get alerted when a new prompt starts generating citations. Investigate why you're suddenly visible for that query and double down on related content.
SEO teams: Track when competitors get cited instead of you. Analyze the citation text to understand what content gaps you need to fill.
Product teams: Monitor product recommendation prompts. If ChatGPT starts recommending a competitor's product over yours, you need to know immediately.
Agency teams: Run separate bots for each client. Route alerts to client-specific Slack channels so everyone sees their own brand's visibility changes.
Executive teams: Daily digest of visibility score changes across all tracked prompts. High-level summary without the noise of individual citation alerts.
The bot you build depends on your workflow. Start simple -- just new citation alerts -- and add complexity as you learn what your team actually needs.
Next steps
You now have a working real-time AI citation bot. It receives webhook events from Promptwatch, formats them for Slack, and delivers instant alerts to your team.
From here, you can:
- Add filtering logic to reduce notification noise
- Implement threading to group related citations
- Build interactive buttons for common actions
- Route different event types to different channels
- Create daily/weekly digest summaries
- Integrate with project management tools (Jira, Asana) to auto-create tasks
- Connect to your CRM to track which citations convert to leads
The webhook API is flexible. You're not limited to Slack -- send events to Discord, Microsoft Teams, email, SMS, or any system that accepts HTTP requests. The pattern is the same: receive webhook, process data, forward to destination.
AI search visibility changes fast. Real-time monitoring means you catch problems before they cost you traffic and revenue.
