Zulip API Integration: Build Custom Bots and Automate Team Notifications
Zulip API Integration: Build Custom Bots and Automate Team Notifications
Here's the thing about team chat: the real power isn't in the messages themselves—it's in what you can automate around them. CI/CD pipeline failed? Your team should know instantly. New customer signed up? Sales channel gets pinged. Server health degraded? DevOps sees it before users complain.
Zulip's API makes this surprisingly straightforward. Let me show you how to build bots that actually solve problems.
Getting Your API Credentials
Every Zulip integration starts with credentials. Head to your Zulip instance, click your profile, then Settings → Personal → Bots.
Create a new bot with type "Incoming webhook" for simple notifications, or "Generic bot" for interactive features. You'll get:
- Bot email: something like
my-bot@yourorg.zulipchat.com - API key: your authentication token
Store these securely. Environment variables, not hardcoded strings.
Sending Your First Message
The simplest integration—posting a message to a stream:
import requests
ZULIP_URL = "https://yourorg.zulipchat.com"
BOT_EMAIL = "deploy-bot@yourorg.zulipchat.com"
API_KEY = "your-api-key-here"
def send_message(stream, topic, content):
response = requests.post(
f"{ZULIP_URL}/api/v1/messages",
auth=(BOT_EMAIL, API_KEY),
data={
"type": "stream",
"to": stream,
"topic": topic,
"content": content
}
)
return response.json()
# Usage
send_message(
stream="deployments",
topic="production",
content="✅ Deploy v2.3.1 completed successfully"
)
That's it. No SDK required, just HTTP requests with basic auth.
Building a CI/CD Notification Bot
Let's make this practical. Here's a complete bot that reports GitHub Actions results:
from flask import Flask, request
import requests
import os
app = Flask(__name__)
ZULIP_URL = os.environ["ZULIP_URL"]
BOT_EMAIL = os.environ["ZULIP_BOT_EMAIL"]
API_KEY = os.environ["ZULIP_API_KEY"]
@app.route("/webhook/github", methods=["POST"])
def github_webhook():
payload = request.json
# Handle workflow run events
if "workflow_run" in payload:
run = payload["workflow_run"]
status = run["conclusion"]
repo = run["repository"]["full_name"]
workflow = run["name"]
emoji = "✅" if status == "success" else "❌"
message = f"""
{emoji} **{workflow}** on `{repo}`
**Status:** {status}
**Branch:** {run["head_branch"]}
**Commit:** {run["head_sha"][:7]}
[View Run]({run["html_url"]})
"""
send_to_zulip("ci-cd", f"{repo}", message.strip())
return {"status": "ok"}
def send_to_zulip(stream, topic, content):
requests.post(
f"{ZULIP_URL}/api/v1/messages",
auth=(BOT_EMAIL, API_KEY),
data={
"type": "stream",
"to": stream,
"topic": topic,
"content": content
}
)
if __name__ == "__main__":
app.run(port=5000)
Point your GitHub webhook to https://your-server/webhook/github and select "Workflow runs" events. Every CI run now reports to your team automatically.
Interactive Bots That Respond
Want bots that reply to commands? Zulip's outgoing webhooks let you build interactive experiences:
@app.route("/webhook/zulip", methods=["POST"])
def zulip_command():
data = request.json
message = data.get("message", {})
content = message.get("content", "").strip()
# Parse command
if content.startswith("!deploy"):
parts = content.split()
if len(parts) >= 2:
env = parts[1]
return {
"content": f"🚀 Triggering deploy to **{env}**...\n\nI'll update this thread when complete."
}
return {"content": "Usage: `!deploy <environment>`"}
if content.startswith("!status"):
# Check your systems here
return {
"content": """
**System Status**
| Service | Status |
|---------|--------|
| API | 🟢 Healthy |
| Database | 🟢 Healthy |
| Cache | 🟡 Degraded |
"""
}
return {"content": "Unknown command. Try `!deploy` or `!status`"}
Configure an outgoing webhook in Zulip's settings pointing to your endpoint. Now team members can trigger deploys or check status directly from chat.
Handling Multiple Streams
Real-world bots often route messages based on context:
ROUTING = {
"critical": {"stream": "alerts", "topic": "critical"},
"deploy": {"stream": "deployments", "topic": "production"},
"support": {"stream": "customer-success", "topic": "tickets"},
}
def route_notification(category, message):
if category not in ROUTING:
category = "support" # fallback
route = ROUTING[category]
send_to_zulip(route["stream"], route["topic"], message)
# Usage
route_notification("critical", "🔥 Database connection pool exhausted")
route_notification("deploy", "✅ API v3.0 rolled out to 100% traffic")
Private Messages for Sensitive Alerts
Some notifications shouldn't be public. Zulip handles direct messages too:
def send_private_message(user_emails, content):
requests.post(
f"{ZULIP_URL}/api/v1/messages",
auth=(BOT_EMAIL, API_KEY),
data={
"type": "private",
"to": user_emails, # comma-separated or list
"content": content
}
)
# Alert on-call engineer directly
send_private_message(
"oncall@yourorg.com",
"⚠️ Production error rate exceeded 5%. Check Grafana dashboard."
)
Common Gotchas
Rate Limits: Zulip allows ~120 requests per minute per bot. For high-volume integrations, batch messages or implement queuing.
Message Formatting: Zulip uses Markdown but with some quirks. Test formatting in the compose box before coding it.
Topic Length: Topics max out at 60 characters. Truncate programmatically or you'll get errors:
topic = original_topic[:57] + "..." if len(original_topic) > 60 else original_topic
Authentication Failures: Double-check bot email format. It's the full email including domain, not just the bot name.
Deploying Your Bot
For production, you need reliability. Here's a Docker setup:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "bot:app"]
Run it on Elestio alongside your Zulip instance for minimal latency and simplified networking.
What to Automate First
If you're just starting, these integrations deliver immediate value:
- Deploy notifications - every merge to main, every production deploy
- Error alerting - pipe exceptions from Sentry or your logging stack
- On-call handoffs - automated reminders when rotations change
- Customer events - new signups, upgrades, churn risks
The API is the same for all of these. Once you've built one integration, the rest take minutes.
Zulip's threading model means these automated messages stay organized—deploy notifications don't drown out customer discussions because they live in separate topics.
That's the real advantage over Slack-style channels. Your bots post to the right context, and humans can ignore or engage as needed.
Thanks for reading! See you in the next one.