"use client" import { useState, useEffect, useCallback } from "react" import { useTheme } from "next-themes" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/tabs" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" import { fetchApi } from "../lib/api-config" import { Bell, BellOff, Send, CheckCircle2, XCircle, Loader2, AlertTriangle, Info, Settings2, Zap, Eye, EyeOff, Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook, Copy, Server, Shield, ExternalLink, RefreshCw } from "lucide-react" interface ChannelConfig { enabled: boolean rich_format?: boolean bot_token?: string chat_id?: string url?: string token?: string webhook_url?: string // Email channel fields host?: string port?: string username?: string password?: string tls_mode?: string from_address?: string to_addresses?: string subject_prefix?: string } interface EventTypeInfo { type: string title: string default_enabled: boolean } interface ChannelOverrides { categories: Record events: Record } interface NotificationConfig { enabled: boolean channels: Record event_categories: Record event_toggles: Record event_types_by_group: Record channel_overrides: Record ai_enabled: boolean ai_provider: string ai_api_key: string ai_model: string ai_language: string ai_ollama_url: string ai_openai_base_url: string channel_ai_detail: Record hostname: string webhook_secret: string webhook_allowed_ips: string pbs_host: string pve_host: string pbs_trusted_sources: string } interface ServiceStatus { enabled: boolean running: boolean channels: Record queue_size: number last_sent: string | null total_sent_24h: number } interface HistoryEntry { id: number event_type: string channel: string title: string severity: string sent_at: string success: boolean error_message: string | null } const EVENT_CATEGORIES = [ { key: "vm_ct", label: "VM / CT", desc: "Start, stop, crash, migration" }, { key: "backup", label: "Backups", desc: "Backup start, complete, fail" }, { key: "resources", label: "Resources", desc: "CPU, memory, temperature" }, { key: "storage", label: "Storage", desc: "Disk space, I/O, SMART" }, { key: "network", label: "Network", desc: "Connectivity, bond, latency" }, { key: "security", label: "Security", desc: "Auth failures, Fail2Ban, firewall" }, { key: "cluster", label: "Cluster", desc: "Quorum, split-brain, HA fencing" }, { key: "services", label: "Services", desc: "System services, shutdown, reboot" }, { key: "health", label: "Health Monitor", desc: "Health checks, degradation, recovery" }, { key: "updates", label: "Updates", desc: "System and PVE updates" }, { key: "other", label: "Other", desc: "Uncategorized notifications" }, ] const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email"] as const const AI_PROVIDERS = [ { value: "groq", label: "Groq", model: "llama-3.3-70b-versatile", description: "Very fast, generous free tier (30 req/min). Ideal to start.", keyUrl: "https://console.groq.com/keys", icon: "/icons/Groq Logo_White 25.svg", iconLight: "/icons/Groq Logo_Black 25.svg" }, { value: "openai", label: "OpenAI", model: "gpt-4o-mini", description: "Industry standard. Very accurate and widely used.", keyUrl: "https://platform.openai.com/api-keys", icon: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openai.webp", iconLight: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openai-light.webp" }, { value: "anthropic", label: "Anthropic (Claude)", model: "claude-3-haiku-20240307", description: "Excellent for writing and translation. Fast and economical.", keyUrl: "https://console.anthropic.com/settings/keys", icon: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/claude-light.webp", iconLight: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/claude-dark.webp" }, { value: "gemini", label: "Google Gemini", model: "gemini-1.5-flash", description: "Free tier available, great quality/price ratio.", keyUrl: "https://aistudio.google.com/app/apikey", icon: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/google-gemini.webp", iconLight: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/google-gemini.webp" }, { value: "ollama", label: "Ollama (Local)", model: "llama3.2", description: "100% local execution. No costs, total privacy, no internet required.", keyUrl: "", icon: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ollama.webp", iconLight: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ollama-light.webp" }, { value: "openrouter", label: "OpenRouter", model: "meta-llama/llama-3.3-70b-instruct", description: "Aggregator with access to 100+ models using a single API key. Maximum flexibility.", keyUrl: "https://openrouter.ai/keys", icon: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openrouter-light.webp", iconLight: "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openrouter-dark.webp" }, ] const AI_LANGUAGES = [ { value: "en", label: "English" }, { value: "es", label: "Espanol" }, { value: "fr", label: "Francais" }, { value: "de", label: "Deutsch" }, { value: "pt", label: "Portugues" }, { value: "it", label: "Italiano" }, { value: "ru", label: "Russkiy" }, { value: "sv", label: "Svenska" }, { value: "no", label: "Norsk" }, { value: "ja", label: "Nihongo" }, { value: "zh", label: "Zhongwen" }, { value: "nl", label: "Nederlands" }, ] const AI_DETAIL_LEVELS = [ { value: "brief", label: "Brief", desc: "2-3 lines, essential only" }, { value: "standard", label: "Standard", desc: "Concise with basic context" }, { value: "detailed", label: "Detailed", desc: "Complete technical details" }, ] const DEFAULT_CONFIG: NotificationConfig = { enabled: false, channels: { telegram: { enabled: false }, gotify: { enabled: false }, discord: { enabled: false }, email: { enabled: false }, }, event_categories: { vm_ct: true, backup: true, resources: true, storage: true, network: true, security: true, cluster: true, services: true, health: true, updates: true, other: true, }, event_toggles: {}, event_types_by_group: {}, channel_overrides: { telegram: { categories: {}, events: {} }, gotify: { categories: {}, events: {} }, discord: { categories: {}, events: {} }, email: { categories: {}, events: {} }, }, ai_enabled: false, ai_provider: "groq", ai_api_key: "", ai_model: "", ai_language: "en", ai_ollama_url: "http://localhost:11434", ai_openai_base_url: "", channel_ai_detail: { telegram: "brief", gotify: "brief", discord: "brief", email: "detailed", }, hostname: "", webhook_secret: "", webhook_allowed_ips: "", pbs_host: "", pve_host: "", pbs_trusted_sources: "", } export function NotificationSettings() { const { resolvedTheme } = useTheme() const [config, setConfig] = useState(DEFAULT_CONFIG) const [status, setStatus] = useState(null) const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [testing, setTesting] = useState(null) const [testResult, setTestResult] = useState<{ channel: string; success: boolean; message: string } | null>(null) const [showHistory, setShowHistory] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false) const [showSecrets, setShowSecrets] = useState>({}) const [editMode, setEditMode] = useState(false) const [hasChanges, setHasChanges] = useState(false) const [expandedCategories, setExpandedCategories] = useState>(new Set()) const [originalConfig, setOriginalConfig] = useState(DEFAULT_CONFIG) const [showProviderInfo, setShowProviderInfo] = useState(false) const [showTelegramHelp, setShowTelegramHelp] = useState(false) const [testingAI, setTestingAI] = useState(false) const [aiTestResult, setAiTestResult] = useState<{ success: boolean; message: string; model?: string } | null>(null) const [ollamaModels, setOllamaModels] = useState([]) const [loadingOllamaModels, setLoadingOllamaModels] = useState(false) const [webhookSetup, setWebhookSetup] = useState<{ status: "idle" | "running" | "success" | "failed" fallback_commands: string[] error: string }>({ status: "idle", fallback_commands: [], error: "" }) const loadConfig = useCallback(async () => { try { const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings") if (data.success && data.config) { setConfig(data.config) setOriginalConfig(data.config) } } catch (err) { console.error("Failed to load notification settings:", err) } finally { setLoading(false) } }, []) const loadStatus = useCallback(async () => { try { const data = await fetchApi<{ success: boolean } & ServiceStatus>("/api/notifications/status") if (data.success) { setStatus(data) } } catch { // Service may not be running yet } }, []) const loadHistory = useCallback(async () => { try { const data = await fetchApi<{ success: boolean; history: HistoryEntry[]; total: number }>("/api/notifications/history?limit=20") if (data.success) { setHistory(data.history || []) } } catch { // Ignore } }, []) useEffect(() => { loadConfig() loadStatus() }, [loadConfig, loadStatus]) useEffect(() => { if (showHistory) loadHistory() }, [showHistory, loadHistory]) const updateConfig = (updater: (prev: NotificationConfig) => NotificationConfig) => { setConfig(prev => { const next = updater(prev) setHasChanges(true) return next }) } const updateChannel = (channel: string, field: string, value: string | boolean) => { updateConfig(prev => ({ ...prev, channels: { ...prev.channels, [channel]: { ...prev.channels[channel], [field]: value }, }, })) } /** Reusable 10+1 category block rendered inside each channel tab. */ const renderChannelCategories = (chName: string) => { const overrides = config.channel_overrides?.[chName] || { categories: {}, events: {} } const evtByGroup = config.event_types_by_group || {} return (
{EVENT_CATEGORIES.filter(cat => cat.key !== "other").map(cat => { const isEnabled = overrides.categories[cat.key] ?? true const isExpanded = expandedCategories.has(`${chName}.${cat.key}`) const eventsForGroup = evtByGroup[cat.key] || [] const enabledCount = eventsForGroup.filter( e => (overrides.events?.[e.type] ?? e.default_enabled) ).length return (
{/* Category row -- entire block is clickable to expand/collapse */}
{ if (!isEnabled) return setExpandedCategories(prev => { const next = new Set(prev) const key = `${chName}.${cat.key}` if (next.has(key)) next.delete(key) else next.add(key) return next }) }} > {/* Expand arrow */} {/* Label */}
{cat.label}
{/* Count badge */} {isEnabled && eventsForGroup.length > 0 && ( {enabledCount}/{eventsForGroup.length} )} {/* Toggle -- same style as channel enable toggle */}
{/* Sub-event toggles */} {isEnabled && isExpanded && eventsForGroup.length > 0 && (
{eventsForGroup.map(evt => { const evtEnabled = overrides.events?.[evt.type] ?? evt.default_enabled return (
{evt.title}
) })}
)}
) })}
) } /** Flatten the nested NotificationConfig into the flat key-value map the backend expects. */ const flattenConfig = (cfg: NotificationConfig): Record => { const flat: Record = { enabled: String(cfg.enabled), ai_enabled: String(cfg.ai_enabled), ai_provider: cfg.ai_provider, ai_api_key: cfg.ai_api_key, ai_model: cfg.ai_model, ai_language: cfg.ai_language, ai_ollama_url: cfg.ai_ollama_url, ai_openai_base_url: cfg.ai_openai_base_url, hostname: cfg.hostname, webhook_secret: cfg.webhook_secret, webhook_allowed_ips: cfg.webhook_allowed_ips, pbs_host: cfg.pbs_host, pve_host: cfg.pve_host, pbs_trusted_sources: cfg.pbs_trusted_sources, } // Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ... for (const [chName, chCfg] of Object.entries(cfg.channels)) { for (const [field, value] of Object.entries(chCfg)) { flat[`${chName}.${field}`] = String(value ?? "") } } // Per-channel category & event toggles: telegram.events.vm_ct, telegram.event.vm_start, etc. // Each channel independently owns its notification preferences. if (cfg.channel_overrides) { for (const [chName, overrides] of Object.entries(cfg.channel_overrides)) { if (overrides.categories) { for (const [cat, enabled] of Object.entries(overrides.categories)) { flat[`${chName}.events.${cat}`] = String(enabled) } } if (overrides.events) { for (const [evt, enabled] of Object.entries(overrides.events)) { flat[`${chName}.event.${evt}`] = String(enabled) } } } } // Per-channel AI detail level if (cfg.channel_ai_detail) { for (const [chName, level] of Object.entries(cfg.channel_ai_detail)) { flat[`${chName}.ai_detail_level`] = level } } return flat } const handleSave = async () => { setSaving(true) try { // If notifications are being disabled, clean up PVE webhook first const wasEnabled = originalConfig.enabled const isNowDisabled = !config.enabled if (wasEnabled && isNowDisabled) { try { await fetchApi("/api/notifications/proxmox/cleanup-webhook", { method: "POST" }) } catch { // Non-fatal: webhook cleanup failed but we still save settings } } const payload = flattenConfig(config) await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(payload), }) setOriginalConfig(config) setHasChanges(false) setEditMode(false) setSaved(true) setTimeout(() => setSaved(false), 3000) loadStatus() } catch (err) { console.error("Failed to save notification settings:", err) } finally { setSaving(false) } } const handleCancel = () => { setConfig(originalConfig) setHasChanges(false) setEditMode(false) } const handleTest = async (channel: string) => { setTesting(channel) setTestResult(null) try { // Auto-save current config before testing so backend has latest channel data const payload = flattenConfig(config) await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(payload), }) setOriginalConfig(config) setHasChanges(false) const data = await fetchApi<{ success: boolean message?: string error?: string results?: Record }>("/api/notifications/test", { method: "POST", body: JSON.stringify({ channel }), }) // Extract message from the results object if present let message = data.message || "" if (!message && data.results) { const channelResult = data.results[channel] if (channelResult) { message = channelResult.success ? "Test notification sent successfully" : channelResult.error || "Test failed" } } if (!message && data.error) { message = data.error } if (!message) { message = data.success ? "Test notification sent successfully" : "Test failed" } setTestResult({ channel, success: data.success, message }) } catch (err) { setTestResult({ channel, success: false, message: String(err) }) } finally { setTesting(null) setTimeout(() => setTestResult(null), 8000) } } const fetchOllamaModels = useCallback(async (url: string) => { if (!url) return setLoadingOllamaModels(true) try { const data = await fetchApi<{ success: boolean; models: string[]; message: string }>("/api/notifications/ollama-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ollama_url: url }), }) if (data.success && data.models && data.models.length > 0) { setOllamaModels(data.models) // Auto-select first model if current selection is empty or not in the list updateConfig(prev => { if (!prev.ai_model || !data.models.includes(prev.ai_model)) { return { ...prev, ai_model: data.models[0] } } return prev }) } else { setOllamaModels([]) } } catch { setOllamaModels([]) } finally { setLoadingOllamaModels(false) } }, []) // Note: We removed the automatic useEffect that fetched models on URL change // because it caused infinite loops. Users now use the "Load" button explicitly. const handleTestAI = async () => { setTestingAI(true) setAiTestResult(null) try { const data = await fetchApi<{ success: boolean; message: string; model: string }>("/api/notifications/test-ai", { method: "POST", body: JSON.stringify({ provider: config.ai_provider, api_key: config.ai_api_key, model: config.ai_model, ollama_url: config.ai_ollama_url, openai_base_url: config.ai_openai_base_url, }), }) setAiTestResult(data) } catch (err) { setAiTestResult({ success: false, message: String(err) }) } finally { setTestingAI(false) setTimeout(() => setAiTestResult(null), 8000) } } const handleClearHistory = async () => { try { await fetchApi("/api/notifications/history", { method: "DELETE" }) setHistory([]) } catch { // Ignore } } const toggleSecret = (key: string) => { setShowSecrets(prev => ({ ...prev, [key]: !prev[key] })) } if (loading) { return (
Notifications
) } const activeChannels = Object.entries(config.channels).filter(([, ch]) => ch.enabled).length const handleEnable = async () => { setSaving(true) setWebhookSetup({ status: "running", fallback_commands: [], error: "" }) try { // 1) Save enabled=true const newConfig = { ...config, enabled: true } await fetchApi("/api/notifications/settings", { method: "POST", body: JSON.stringify(newConfig), }) setConfig(newConfig) setOriginalConfig(newConfig) // 2) Auto-configure PVE webhook try { const setup = await fetchApi<{ configured: boolean secret?: string fallback_commands?: string[] error?: string }>("/api/notifications/proxmox/setup-webhook", { method: "POST" }) if (setup.configured) { setWebhookSetup({ status: "success", fallback_commands: [], error: "" }) // Update secret in local config if one was generated if (setup.secret) { const updated = { ...newConfig, webhook_secret: setup.secret } setConfig(updated) setOriginalConfig(updated) } } else { setWebhookSetup({ status: "failed", fallback_commands: setup.fallback_commands || [], error: setup.error || "Unknown error", }) } } catch { setWebhookSetup({ status: "failed", fallback_commands: [], error: "Could not reach setup endpoint", }) } setEditMode(true) loadStatus() } catch (err) { console.error("Failed to enable notifications:", err) setWebhookSetup({ status: "idle", fallback_commands: [], error: "" }) } finally { setSaving(false) } } // ── Disabled state: show activation card ── if (!config.enabled && !editMode) { return (
Notifications Disabled
Get real-time alerts about your Proxmox environment via Telegram, Discord, Gotify, or Email.

Enable notification service

Monitor system health, VM/CT events, backups, security alerts, and cluster status. PVE webhook integration is configured automatically.

{/* Webhook setup result */} {webhookSetup.status === "success" && (

PVE webhook configured automatically. Proxmox will send notifications to ProxMenux.

)} {webhookSetup.status === "failed" && (

Automatic PVE configuration failed: {webhookSetup.error}

Notifications are enabled. Run the commands below on the PVE host to complete webhook setup.

{webhookSetup.fallback_commands.length > 0 && (
{webhookSetup.fallback_commands.join('\n')}
                    
)}
)}
) } return ( <>
Notifications {config.enabled && ( Active )}
{saved && ( Saved )} {editMode ? ( <> ) : ( )}
Configure notification channels and event filters. Receive alerts via Telegram, Gotify, Discord, or Email.
{/* ── Service Status ── */} {status && (
{status.running ? "Service running" : "Service stopped"} {status.total_sent_24h > 0 && ( {status.total_sent_24h} sent in last 24h )}
{activeChannels > 0 && ( {activeChannels} channel{activeChannels > 1 ? "s" : ""} )}
)} {/* ── Enable/Disable ── */}
{config.enabled ? ( ) : ( )}
Enable Notifications

Activate the notification service

{config.enabled && ( <> {/* ── Channel Configuration ── */}
Channels
Telegram Gotify Discord Email {/* Telegram */}
{config.channels.telegram?.enabled && ( <>
updateChannel("telegram", "bot_token", e.target.value)} />
updateChannel("telegram", "chat_id", e.target.value)} />
{/* Message format */}

Enrich notifications with contextual emojis and icons

{renderChannelCategories("telegram")} {/* Send Test */}
)}
{/* Gotify */}
{config.channels.gotify?.enabled && ( <>
updateChannel("gotify", "url", e.target.value)} />
updateChannel("gotify", "token", e.target.value)} />
{/* Message format */}

Enrich notifications with contextual emojis and icons

{renderChannelCategories("gotify")} {/* Send Test */}
)}
{/* Discord */}
{config.channels.discord?.enabled && ( <>
updateChannel("discord", "webhook_url", e.target.value)} />
{/* Message format */}

Enrich notifications with contextual emojis and icons

{renderChannelCategories("discord")} {/* Send Test */}
)}
{/* Email */}
{config.channels.email?.enabled && ( <>
updateChannel("email", "host", e.target.value)} />
updateChannel("email", "port", e.target.value)} />
updateChannel("email", "username", e.target.value)} />
updateChannel("email", "password", e.target.value)} />
updateChannel("email", "from_address", e.target.value)} />
updateChannel("email", "to_addresses", e.target.value)} />
updateChannel("email", "subject_prefix", e.target.value)} />

Leave SMTP Host empty to use local sendmail (must be installed on the server). For Gmail, use an App Password instead of your account password.

{renderChannelCategories("email")} {/* Send Test */}
)}
{/* Test Result */} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* close bordered channel container */}
{/* ── Advanced: AI Enhancement ── */}
{showAdvanced && (
AI-Enhanced Messages

Use AI to generate contextual notification messages

{config.ai_enabled && ( <> {/* Provider + Info button */}
{/* Ollama URL (conditional) */} {config.ai_provider === "ollama" && (
updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))} disabled={!editMode} />
{ollamaModels.length > 0 && (

{ollamaModels.length} models found

)}
)} {/* Custom Base URL for OpenAI-compatible APIs */} {config.ai_provider === "openai" && (
(optional)
updateConfig(p => ({ ...p, ai_openai_base_url: e.target.value }))} disabled={!editMode} />

For OpenAI-compatible APIs: BytePlus, LocalAI, LM Studio, vLLM, etc.

)} {/* API Key (not shown for Ollama) */} {config.ai_provider !== "ollama" && (
updateConfig(p => ({ ...p, ai_api_key: e.target.value }))} disabled={!editMode} />
)} {/* Model - selector for Ollama, read-only for others */}
{config.ai_provider === "ollama" ? ( ) : (
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || "default"}
)}
{/* Language selector */}
{/* Test Connection button */} {/* Test result */} {aiTestResult && (
{aiTestResult.success ? : }

{aiTestResult.message} {aiTestResult.model && ` (${aiTestResult.model})`}

)} {/* Per-channel detail level */}
{CHANNEL_TYPES.map(ch => (
{ch}
))}

AI enhancement translates and formats notifications to your selected language. Each channel can have different detail levels. If the AI service is unavailable, standard templates are used as fallback.

)}
)}
)} {/* ── Footer info ── */}

{config.enabled ? "Notifications are active. Each channel sends events based on its own category and event selection." : "Enable notifications to receive alerts about system events, health status changes, and security incidents via Telegram, Gotify, Discord, or Email."}

{/* AI Provider Information Modal */} AI Providers Information
{AI_PROVIDERS.map(provider => (
{/* Provider icon with theme support */}
{provider.label} { // Fallback if icon fails to load (e.target as HTMLImageElement).style.display = 'none' }} />
{provider.label}
{provider.value === "ollama" && ( Local )}
Default model: {provider.model}

{provider.description}

{/* OpenAI compatibility note */} {provider.value === "openai" && (

OpenAI-Compatible APIs

You can use any OpenAI-compatible API by setting a custom Base URL. Compatible services include:

  • BytePlus/ByteDance (Kimi K2.5)
  • LocalAI, LM Studio, vLLM
  • Together AI, Fireworks AI
  • Any service using OpenAI format
)}
))}
{/* Telegram Setup Guide Modal */} Telegram Bot Setup Guide
{/* Step 1 */}
1

Create a Bot with BotFather

1. Open Telegram and search for @BotFather

2. Send the command /newbot

3. Choose a name for your bot (e.g., "ProxMenux Notifications")

4. Choose a username ending in "bot" (e.g., "proxmenux_alerts_bot")

{/* Step 2 */}
2

Get the Bot Token

After creating the bot, BotFather will give you a token like:

{":"}

Copy this token and paste it in the Bot Token field.

{/* Step 3 */}
3

Get Your Chat ID

Option A: Using a Bot (Easiest)

1. Search for @userinfobot or @getmyid_bot on Telegram

2. Send any message and it will reply with your Chat ID

Option B: Manual Method

1. Send a message to your new bot

2. Open this URL in your browser (replace YOUR_TOKEN):

https://api.telegram.org/botYOUR_TOKEN/getUpdates

3. Look for "chat":{"id": XXXXXX} - that number is your Chat ID

{/* Step 4 */}
4

For Groups or Channels

1. Add your bot to the group/channel as administrator

2. Send a message in the group

3. Use the getUpdates URL method above to find the group Chat ID

4. Group IDs are negative numbers (e.g., -1001234567890)

{/* Summary */}

Quick Summary

  • Bot Token: Identifies your bot (from BotFather)
  • Chat ID: Where to send messages (your ID or group ID)
) }