Metadata-Version: 2.4
Name: jsleekr-notifykit
Version: 1.2.0
Summary: One command. Every notification channel.
Author-email: JSLEEKR <93jslee@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/JSLEEKR/notifykit
Project-URL: Repository, https://github.com/JSLEEKR/notifykit
Project-URL: Issues, https://github.com/JSLEEKR/notifykit/issues
Keywords: notification,slack,discord,telegram,email,webhook,cli
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.1
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-mock>=3.10; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Dynamic: license-file

<div align="center">

# notifykit

### One command. Every notification channel.

[![Stars](https://img.shields.io/github/stars/JSLEEKR/notifykit?style=for-the-badge)](https://github.com/JSLEEKR/notifykit/stargazers)
[![License](https://img.shields.io/github/license/JSLEEKR/notifykit?style=for-the-badge)](LICENSE)
[![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://python.org)
[![Tests](https://img.shields.io/badge/Tests-260_passed-brightgreen?style=for-the-badge)](tests/)
[![Rounds](https://img.shields.io/badge/Rounds-20%2F20-blue?style=for-the-badge)](#round-log)

<br/>

**Send notifications to Slack, Discord, Email, Telegram, and Webhooks from a single YAML config.**

</div>

---

## Why This Exists

Every project eventually needs notifications. Deploy alerts go to Slack. Error pages ping the on-call Telegram group. Weekly reports land in email. Monitoring webhooks fire to custom dashboards.

The problem: each channel has its own SDK, authentication flow, payload format, and retry logic. You end up writing the same boilerplate in every project — a Slack webhook here, a SendGrid integration there, a Telegram bot scattered somewhere else.

**notifykit** solves this by providing a single unified interface. One YAML config file declares all your channels. One CLI command — or one Python function — routes messages to the right places based on severity level and tags. Rate limiting, retries, formatting, and audit logging come built-in.

No more copy-pasting webhook code. No more forgetting to add retries. No more wondering which notification actually got delivered.

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
  - [Channel Types](#channel-types)
  - [Environment Variables](#environment-variables)
  - [Level-Based Routing](#level-based-routing)
  - [Tag-Based Routing](#tag-based-routing)
- [CLI Reference](#cli-reference)
  - [send](#send)
  - [test](#test)
  - [channels](#channels)
  - [validate](#validate)
  - [history](#history)
  - [init](#init)
- [Python API](#python-api)
  - [Sending Messages](#sending-messages)
  - [Templates](#templates)
  - [Formatting](#formatting)
  - [Rate Limiting](#rate-limiting)
  - [Retry Logic](#retry-logic)
  - [Audit Log](#audit-log)
- [Channel Details](#channel-details)
  - [Slack](#slack)
  - [Discord](#discord)
  - [Email (SMTP)](#email-smtp)
  - [Telegram](#telegram)
  - [Webhook](#webhook)
  - [Console](#console)
- [Custom Channels](#custom-channels)
- [Architecture](#architecture)
- [When to Use This](#when-to-use-this)
- [Example Output](#example-output)
- [Troubleshooting](#troubleshooting)
- [FAQ](#faq)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)

---

## Features

- **6 built-in channels** — Slack, Discord, Email, Telegram, Webhook, Console
- **YAML configuration** — One file declares all channels with `${ENV_VAR}` expansion
- **Level-based routing** — Send debug to console, errors to Slack, critical to PagerDuty webhook
- **Tag-based routing** — Route `deploy` tags to one channel, `alerts` to another
- **Template engine** — `{{variable}}` substitution with dotted access (`{{deploy.env.name}}`)
- **Multi-format output** — Plain text, Markdown, HTML — each channel gets the right format
- **Rate limiting** — Per-channel token bucket prevents flooding
- **Retry with backoff** — Exponential backoff with configurable max delay
- **Audit log** — Every send attempt recorded with timestamp, result, and timing
- **Dry-run mode** — See which channels would receive a message without sending
- **Extensible** — Register custom channels with a simple ABC
- **CLI-first** — Every feature accessible from the command line

---

## Installation

```bash
pip install notifykit
```

Or install from source:

```bash
git clone https://github.com/JSLEEKR/notifykit.git
cd notifykit
pip install -e ".[dev]"
```

### Requirements

- Python 3.10+
- `click` (CLI framework)
- `pyyaml` (configuration)
- `requests` (HTTP channels)

---

## Quick Start

### 1. Generate a config file

```bash
notifykit init
```

This creates `notifykit.yaml` with a console channel enabled and all other channels commented out as examples.

### 2. Send a notification

```bash
notifykit send "Hello from notifykit!" --level info
```

### 3. Send with title and tags

```bash
notifykit send "Deployment complete" --title "Deploy v1.2.3" --tag deploy --tag prod --level info
```

### 4. Test all channels

```bash
notifykit test
```

---

## Configuration

notifykit uses a YAML file (`notifykit.yaml`) to declare channels and their settings.

### Minimal config

```yaml
channels:
  console:
    type: console
    enabled: true
    levels: [debug, info, warning, error, critical]
```

### Full config example

```yaml
channels:
  console:
    type: console
    enabled: true
    levels: [debug, info, warning, error, critical]

  slack-alerts:
    type: slack
    enabled: true
    levels: [warning, error, critical]
    tags: [alerts]
    settings:
      webhook_url: ${SLACK_WEBHOOK_URL}
      channel: "#alerts"
      username: notifykit

  discord-dev:
    type: discord
    enabled: true
    levels: [info, warning, error, critical]
    settings:
      webhook_url: ${DISCORD_WEBHOOK_URL}

  email-ops:
    type: email
    enabled: true
    levels: [error, critical]
    settings:
      smtp_host: smtp.gmail.com
      smtp_port: 587
      use_tls: true
      username: ${EMAIL_USER}
      password: ${EMAIL_PASS}
      from_addr: alerts@example.com
      to_addrs:
        - ops@example.com

  telegram-oncall:
    type: telegram
    enabled: true
    levels: [critical]
    tags: [oncall]
    settings:
      bot_token: ${TELEGRAM_BOT_TOKEN}
      chat_id: ${TELEGRAM_CHAT_ID}

  webhook-monitor:
    type: webhook
    enabled: true
    settings:
      url: https://monitor.example.com/api/events
      method: POST
      headers:
        Authorization: "Bearer ${MONITOR_TOKEN}"

defaults:
  format: plain
  retry_count: 2
  retry_delay: 1.0

templates:
  deploy: "Deployment {{status}}: {{service}} ({{version}}) at {{timestamp}}"
  alert: "[{{level}}] {{title}}: {{body}}"
```

### Channel Types

| Type | Description | Required Settings |
|------|-------------|-------------------|
| `console` | Print to stdout/stderr | None |
| `slack` | Slack incoming webhook | `webhook_url` |
| `discord` | Discord webhook | `webhook_url` |
| `email` | SMTP email | `smtp_host`, `from_addr`, `to_addrs` |
| `telegram` | Telegram Bot API | `bot_token`, `chat_id` |
| `webhook` | Generic HTTP webhook | `url` |

### Environment Variables

Use `${VAR_NAME}` syntax in your config to reference environment variables:

```yaml
settings:
  webhook_url: ${SLACK_WEBHOOK_URL}
  channel: ${SLACK_CHANNEL:-#general}  # with default value
```

The `${VAR:-default}` syntax provides a fallback when the variable is not set.

### Level-Based Routing

Each channel declares which severity levels it accepts:

```yaml
channels:
  console:
    levels: [debug, info, warning, error, critical]  # everything
  slack:
    levels: [warning, error, critical]                # warnings and above
  pagerduty:
    levels: [critical]                                # critical only
```

Available levels: `debug`, `info`, `warning`, `error`, `critical`

### Tag-Based Routing

Channels can filter by message tags. If a channel has tags configured, only messages with at least one matching tag are routed to it:

```yaml
channels:
  slack-deploy:
    tags: [deploy, release]    # only deploy/release messages
  slack-alerts:
    tags: [alerts, monitoring] # only alert messages
  console:
    # no tags = receives everything that matches level filter
```

---

## CLI Reference

### send

Send a notification message to all matching channels.

```bash
notifykit send "Message body" [OPTIONS]
```

| Option | Description |
|--------|-------------|
| `-l, --level` | Severity level: `debug/info/warning/error/critical` (default: `info`) |
| `-t, --title` | Message title |
| `--tag` | Add a tag (repeatable) |
| `-f, --format` | Output format: `plain`, `markdown`, `html` |
| `-c, --config` | Path to config file |
| `--channel` | Send to specific channel(s) only |
| `--template` | Use a named template from config |
| `--var` | Template variable as `key=value` (repeatable) |
| `--dry-run` | Show routing without actually sending |
| `--json` | Output results as JSON (for CI pipelines) |
| `-q, --quiet` | Suppress non-error output |

**Examples:**

```bash
# Basic send
notifykit send "Server is healthy" -l info

# With title and tags
notifykit send "CPU at 95%" -t "High CPU Alert" --tag alerts -l warning

# Using a template
notifykit send "" --template deploy --var status=success --var service=api --var version=1.2.3

# Dry run
notifykit send "Test message" --dry-run

# Send to specific channel only
notifykit send "Debug info" --channel console
```

### test

Send a test notification to all enabled channels to verify configuration.

```bash
notifykit test [-c CONFIG]
```

### channels

List all configured channels with their type, status, levels, and tags.

```bash
notifykit channels [-c CONFIG]
```

### validate

Validate the configuration file for errors.

```bash
notifykit validate [-c CONFIG]
```

Returns exit code 0 if valid, 1 if issues are found.

### history

Show notification history from the audit log.

```bash
notifykit history [OPTIONS]
```

| Option | Description |
|--------|-------------|
| `-n, --limit` | Number of entries (default: 20) |
| `--channel` | Filter by channel name |
| `--failures` | Show only failed sends |
| `--json` | Output as JSON |

### init

Generate a default `notifykit.yaml` configuration file.

```bash
notifykit init [-o FILENAME] [--force]
```

---

## Python API

### Sending Messages

```python
from notifykit.models import Message, Level
from notifykit.config import load_config, parse_channels
from notifykit.exceptions import ConfigNotFoundError
from notifykit.router import Router

# Load config and create router
try:
    config = load_config("notifykit.yaml")
except ConfigNotFoundError:
    print("Run 'notifykit init' first")
    raise

channels = parse_channels(config)
router = Router(channels)

# Create and send a message
msg = Message(
    body="Deployment complete",
    level=Level.INFO,
    title="Deploy v1.2.3",
    tags=["deploy", "prod"],
)

results = router.send(msg)
for r in results:
    print(r.summary)  # "[OK] slack (120ms)" or "[FAIL] email - SMTP error"
```

### Templates

```python
from notifykit.templates import render, list_variables, validate_template

template = "Deployment {{status}}: {{service}} ({{version}})"
variables = {"status": "success", "service": "api", "version": "1.2.3"}

# Render
output = render(template, variables)
# "Deployment success: api (1.2.3)"

# List variables in template
vars = list_variables(template)
# ["status", "service", "version"]

# Validate all variables are available
missing = validate_template(template, variables)
# [] (empty = all resolved)
```

Dotted access is supported:

```python
render("{{user.name}}", {"user": {"name": "Alice"}})
# "Alice"
```

### Formatting

```python
from notifykit.formatter import format_message, convert_format
from notifykit.models import Format, Message

msg = Message(body="Hello", title="Test", level="error", tags=["prod"])

plain = format_message(msg, Format.PLAIN)
# "[ERROR] Test Hello (tags: prod)"

md = format_message(msg, Format.MARKDOWN)
# "## Test\n\n**Level:** ERROR\n\nHello\n\n**Tags:** `prod`"

html = format_message(msg, Format.HTML)
# "<div>...<h2>Test</h2>...[ERROR]...<p>Hello</p>...</div>"

# Convert between formats
convert_format("**bold**", Format.MARKDOWN, Format.PLAIN)
# "bold"
```

### Rate Limiting

```python
from notifykit.rate_limiter import RateLimiter

rl = RateLimiter()
rl.configure("slack", max_per_minute=10)

if rl.allow("slack"):
    # send message
    pass
else:
    wait = rl.wait_time("slack")
    print(f"Rate limited. Wait {wait:.1f}s")
```

### Retry Logic

```python
from notifykit.retry import retry_send

result = retry_send(
    channel.send,
    message,
    max_retries=3,
    base_delay=1.0,
    backoff_factor=2.0,
)
```

Delay schedule: 1s, 2s, 4s, 8s, ... capped at `max_delay`.

### Audit Log

```python
from notifykit.audit import AuditLog

audit = AuditLog("notifications.jsonl")

# Record send results
audit.record(result)
audit.record_many(results)

# Query history
recent = audit.query(limit=10)
failures = audit.query(success=False)
slack_history = audit.query(channel="slack")

# Statistics
stats = audit.stats()
print(f"Success rate: {stats['success_rate']:.0%}")
print(f"Total: {stats['total']}, Failed: {stats['failed']}")
```

---

## Channel Details

### Slack

Sends via [Incoming Webhooks](https://api.slack.com/messaging/webhooks) with Block Kit formatting.

```yaml
slack-alerts:
  type: slack
  levels: [warning, error, critical]
  settings:
    webhook_url: ${SLACK_WEBHOOK_URL}
    channel: "#alerts"     # optional override
    username: notifykit    # optional bot name
```

Features: header blocks, section blocks, context blocks with tags, level-specific emoji.

### Discord

Sends via [Discord Webhooks](https://discord.com/developers/docs/resources/webhook) with rich embeds.

```yaml
discord-dev:
  type: discord
  settings:
    webhook_url: ${DISCORD_WEBHOOK_URL}
    username: NotifyBot    # optional
```

Features: colored embeds by severity, title/description, footer with tags.

### Email (SMTP)

Sends via any SMTP server with TLS support.

```yaml
email-ops:
  type: email
  levels: [error, critical]
  settings:
    smtp_host: smtp.gmail.com
    smtp_port: 587
    use_tls: true
    username: ${EMAIL_USER}
    password: ${EMAIL_PASS}
    from_addr: alerts@example.com
    to_addrs:
      - ops@example.com
      - oncall@example.com
```

Features: HTML and plain text MIME, TLS, authentication optional.

### Telegram

Sends via [Telegram Bot API](https://core.telegram.org/bots/api).

```yaml
telegram-oncall:
  type: telegram
  levels: [critical]
  settings:
    bot_token: ${TELEGRAM_BOT_TOKEN}
    chat_id: ${TELEGRAM_CHAT_ID}
    disable_preview: true  # optional
```

Features: HTML and MarkdownV2 parse modes, hashtag-style tags, web preview control.

### Webhook

Generic HTTP webhook for any endpoint.

```yaml
webhook-monitor:
  type: webhook
  settings:
    url: https://monitor.example.com/api/events
    method: POST           # default
    timeout: 10            # seconds
    headers:
      Authorization: "Bearer ${TOKEN}"
    success_codes: [200, 201, 202, 204]  # default
```

The payload includes: `id`, `level`, `title`, `body`, `tags`, `timestamp`, `metadata`.

### Console

Prints to stdout (info/debug/warning) or stderr (error/critical) with ANSI colors.

```yaml
console:
  type: console
  settings:
    color: true  # default
```

---

## Custom Channels

Register your own channel by extending `BaseChannel`:

```python
from notifykit.channels.base import BaseChannel
from notifykit.channels.registry import register_channel
from notifykit.models import Message, SendResult

class PagerDutyChannel(BaseChannel):
    def send(self, message: Message) -> SendResult:
        # Your implementation here
        api_key = self.config.settings.get("api_key")
        # ... send to PagerDuty ...
        return self._make_result(message, success=True)

register_channel("pagerduty", PagerDutyChannel)
```

Then use it in your config:

```yaml
channels:
  pagerduty:
    type: pagerduty
    levels: [critical]
    settings:
      api_key: ${PAGERDUTY_KEY}
```

---

## Architecture

```
src/notifykit/
  __init__.py          # Package version
  models.py            # Message, SendResult, Level, ChannelConfig
  config.py            # YAML loader, env expansion, validation
  exceptions.py        # Typed exception hierarchy
  router.py            # Level/tag routing engine
  formatter.py         # Plain/Markdown/HTML formatting
  templates.py         # {{variable}} template engine
  rate_limiter.py      # Token bucket rate limiting
  retry.py             # Exponential backoff retry
  audit.py             # Notification audit log
  cli.py               # Click CLI commands
  channels/
    __init__.py
    base.py            # BaseChannel ABC + error sanitization
    registry.py        # Channel type registry
    console.py         # Console (stdout/stderr)
    slack.py           # Slack incoming webhook
    discord.py         # Discord webhook
    email_channel.py   # SMTP email
    telegram.py        # Telegram Bot API
    webhook.py         # Generic HTTP webhook
```

### Message Flow

```
CLI/API  -->  Router  -->  Channel  -->  Destination
  |             |            |
  |        Level/Tag     Rate Limit
  |        Matching      + Retry
  |             |            |
  +-- Template  |       Audit Log
      Rendering |
                |
           Formatter
        (Plain/MD/HTML)
```

---

## When to Use This

| Scenario | What to configure | Command |
|----------|------------------|---------|
| **Deploy pipeline alerts** | Slack channel for `deploy` tag, warn level+ | `notifykit send "Deploy done" --tag deploy --level info` |
| **On-call paging** | Telegram with `oncall` tag, critical only | `notifykit send "DB down" --tag oncall --level critical` |
| **Daily reports** | Email on a schedule (cron + notifykit) | `notifykit send "Daily report ready" --tag report` |
| **Error monitoring** | Multiple channels, error+ level | `notifykit send "500 spike" --level error` |
| **Local dev debugging** | Console channel only | `notifykit send "Checkpoint hit" --level debug` |
| **Webhook integrations** | Custom webhook channel with Bearer auth | `notifykit send "Event fired" --channel webhook-monitor` |
| **CI/CD build status** | Discord embed + Slack, with templates | `notifykit send "" --template deploy --var status=passed` |
| **Security alerts** | Slack + Telegram + Email, critical only | `notifykit send "Auth bypass detected" --level critical` |

notifykit shines when you need to send notifications to multiple channels from scripts, CI pipelines, or background workers — without installing five different SDKs.

---

## Cross-Tool Integration

### CI/CD Pipelines

#### GitHub Actions

```yaml
- name: Notify on deploy
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
  run: |
    notifykit send "Deploy ${{ github.sha }}" \
      --title "Deploy to production" \
      --level info --tag deploy --json
```

Use `--json` for machine-readable output. Exit code 1 on any channel failure.

See [`examples/github-actions.yml`](examples/github-actions.yml) for a complete workflow.

#### GitLab CI

```yaml
notify:
  stage: notify
  script:
    - pip install notifykit
    - notifykit send "Pipeline $CI_PIPELINE_ID finished" --level info --tag deploy --json
  when: always
```

#### Jenkins

```groovy
post {
    success {
        sh 'notifykit send "Build #${BUILD_NUMBER} passed" --level info --tag ci --json'
    }
    failure {
        sh 'notifykit send "Build #${BUILD_NUMBER} failed" --level error --tag ci --tag alerts --json'
    }
}
```

### Cron Jobs

Run notifykit from cron for scheduled notifications:

```bash
# Daily health check report at 9am
0 9 * * * cd /app && notifykit send "Daily health check passed" --level info --tag report

# Alert if disk usage exceeds 90%
*/5 * * * * df -h / | awk 'NR==2 && $5+0 > 90 {print "Disk usage: "$5}' | xargs -I{} notifykit send "{}" --level warning --tag alerts
```

### Monitoring Tools

#### Prometheus Alertmanager

Configure Alertmanager to call notifykit via a webhook receiver:

```yaml
# alertmanager.yml
receivers:
  - name: notifykit
    webhook_configs:
      - url: 'http://localhost:8080/alert'  # your bridge script
```

Bridge script:

```bash
#!/bin/bash
# alertmanager-bridge.sh — receives Alertmanager webhook, forwards via notifykit
BODY=$(cat)
ALERT_NAME=$(echo "$BODY" | jq -r '.alerts[0].labels.alertname')
STATUS=$(echo "$BODY" | jq -r '.alerts[0].status')
notifykit send "$ALERT_NAME: $STATUS" --level error --tag alerts --json
```

#### Systemd Service Watchdog

```ini
# /etc/systemd/system/notifykit-watchdog.service
[Unit]
Description=Notify on service failure

[Service]
ExecStart=/usr/local/bin/notifykit send "Service %i failed" --level critical --tag oncall
Type=oneshot

[Install]
WantedBy=multi-user.target
```

```bash
# Trigger on another service failure
systemctl enable notifykit-watchdog@myapp.service
```

#### Docker Health Checks

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || \
    (notifykit send "Container unhealthy" --level error --tag alerts && exit 1)
```

### Python Integration

```python
# Use as a library in your application
from notifykit.config import load_config, parse_channels
from notifykit.router import Router
from notifykit.models import Message, Level

router = Router(parse_channels(load_config()))

# In your error handler
def on_error(error: Exception):
    router.send(Message(
        body=str(error),
        level=Level.ERROR,
        title="Application Error",
        tags=["alerts"],
    ))
```

---

## Example Output

### `notifykit init`

```
Created notifykit.yaml

Next steps:
  1. Open notifykit.yaml and configure your channels
  2. Uncomment and fill in the channels you want to use
  3. Run 'notifykit send "Hello!"' to send your first notification
  4. Run 'notifykit channels' to see all configured channels
  5. Run 'notifykit validate' to check your config for errors

The console channel is already enabled - try sending a message now!
```

### `notifykit send "Deploy complete" --title "v1.2.3" --level info`

```
[INFO] v1.2.3 Deploy complete
  [OK] console (0ms)
  [OK] slack-alerts (120ms)
```

### `notifykit send "Test" --dry-run`

```
Would send to 2 channel(s):
  - console (console)
  - slack-alerts (slack)
```

### `notifykit channels`

```
Channels (3):

  console
    Type:   console
    Status: enabled
    Levels: debug, info, warning, error, critical

  slack-alerts
    Type:   slack
    Status: enabled
    Levels: warning, error, critical
    Tags:   alerts

  telegram-oncall
    Type:   telegram
    Status: enabled
    Levels: critical
    Tags:   oncall
```

### `notifykit validate`

```
Configuration is valid.
```

Or on failure:

```
Found 2 issue(s):
  - Channel 'slack-alerts': Slack requires 'settings.webhook_url'
  - Channel 'email-ops': Email requires 'settings.smtp_host'
```

### `notifykit history`

```
  [2026-03-28 07:35:23] OK console msg=50c70d432e9e (0ms)
  [2026-03-28 07:35:20] OK slack-alerts msg=a3b4f6e0ef23 (120ms)
  [2026-03-28 07:34:01] FAIL telegram-oncall msg=f1b2c3d4e5f6 (5001ms)
                         Error: Connection timeout
```

### `notifykit test`

```
[INFO] Test Notification This is a test notification from notifykit.
  [OK] console
  [OK] slack-alerts
  [FAIL] email-ops
         Error: SMTP authentication failed
```

---

## Troubleshooting

### No config file found

```
Error: No notifykit config file found. Create notifykit.yaml or run 'notifykit init'.
```

**Fix:** Run `notifykit init` in your project root to create a default config.

---

### Channel not receiving messages

**Symptoms:** `send` exits 0 but the channel is silent.

**Check list:**
1. Run `notifykit channels` — is the channel `enabled`?
2. Does the message level match the channel's `levels` list? (`warning` won't route to a channel that only accepts `error, critical`)
3. Does the message have a tag that matches the channel's `tags` filter? (Channel with `tags: [deploy]` only receives messages with `--tag deploy`)
4. Run `notifykit send "Test" --dry-run` to see which channels would receive it.

---

### Slack / Discord webhook fails

```
  [FAIL] slack-alerts
         Error: 400 Bad Request
```

**Fixes:**
- Verify the webhook URL is correct: `notifykit validate`
- Check that `${SLACK_WEBHOOK_URL}` is exported in your environment
- Test the webhook directly: `curl -X POST -H 'Content-type: application/json' --data '{"text":"test"}' "$SLACK_WEBHOOK_URL"`

---

### Email SMTP authentication error

```
  [FAIL] email-ops
         Error: SMTP authentication failed
```

**Fixes:**
- For Gmail: use an [App Password](https://myaccount.google.com/apppasswords), not your regular password
- Verify `smtp_host`, `smtp_port`, and `use_tls` match your provider's settings
- Check environment variables are set: `echo $EMAIL_USER`

---

### Telegram: chat not found

```
  [FAIL] telegram-oncall
         Error: 400 Bad Request: chat not found
```

**Fixes:**
- The bot must be added to the chat/group before it can send messages
- `chat_id` for a group is negative (e.g., `-1001234567890`) — use `@userinfobot` to find it
- Send a message to the bot first to activate it for private chats

---

### Environment variable not expanded

If you see `${SLACK_WEBHOOK_URL}` literally in error messages, the variable is not exported.

```bash
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
notifykit validate
```

Use `${VAR:-fallback}` syntax to provide a default value for optional variables.

---

### `notifykit history` shows nothing

The audit log is written to `.notifykit/audit.jsonl` in the **current working directory** where you ran `notifykit send`. Run history from the same directory.

---

### Rate limit exceeded

```
No channels matched for this message.
```

If you configured a `rate_limit` on a channel and are sending too fast, the router will skip that channel. The rate limit resets after one minute.

---

## FAQ

**Q: Does notifykit make real HTTP requests during tests?**

No. All channel tests use mocked HTTP via the `responses` library. No API calls are made during `pytest`.

---

**Q: Can I use notifykit without a config file?**

No. notifykit requires a `notifykit.yaml` config file. Run `notifykit init` to create one with a working console channel — no credentials needed.

---

**Q: Can I send to multiple specific channels at once?**

Yes. Use `--channel` multiple times:

```bash
notifykit send "Alert!" --channel slack-alerts --channel telegram-oncall
```

---

**Q: How do I send the same message to all channels regardless of level/tag filters?**

Use a channel with no `levels` or `tags` restrictions, or use `--channel` to target channels directly.

---

**Q: Where is the audit log stored?**

At `.notifykit/audit.jsonl` relative to the working directory. Each line is a JSON record of one send attempt.

---

**Q: How do I rotate or clear the audit log?**

Delete or truncate `.notifykit/audit.jsonl` manually. notifykit will create a new one on the next send.

---

**Q: Does notifykit support async sending?**

Not natively — sends are synchronous. For high-volume use, call notifykit from a task queue worker.

---

**Q: Can I add my own channel type?**

Yes. See [Custom Channels](#custom-channels) for a step-by-step example.

---

**Q: What happens if a channel fails? Does it block other channels?**

No. Each channel is sent independently. A failure in one channel does not block others. All results are collected and reported at the end. The exit code is non-zero only if at least one channel failed.

---

## Migration Guide

### From email-only notifications

If you currently send notifications through email (e.g., Python `smtplib`, SendGrid API):

1. Run `notifykit init` to create a config
2. Configure the email channel with your SMTP settings
3. Add additional channels (Slack, Discord) for instant alerts
4. Replace your email-sending code with `router.send(message)`

**Before:**

```python
import smtplib
from email.mime.text import MIMEText

msg = MIMEText("Server is down!")
msg["Subject"] = "CRITICAL ALERT"
msg["From"] = "alerts@example.com"
msg["To"] = "ops@example.com"
server = smtplib.SMTP("smtp.gmail.com", 587)
server.starttls()
server.login(user, password)
server.sendmail("alerts@example.com", ["ops@example.com"], msg.as_string())
server.quit()
```

**After:**

```python
from notifykit.config import load_config, parse_channels
from notifykit.router import Router
from notifykit.models import Message, Level

router = Router(parse_channels(load_config()))
router.send(Message(
    body="Server is down!",
    level=Level.CRITICAL,
    title="CRITICAL ALERT",
    tags=["alerts"],
))
# Now also goes to Slack + Telegram based on your config
```

### From Slack webhook scripts

If you currently call Slack webhooks directly:

1. Move your webhook URL to an environment variable
2. Configure a Slack channel in `notifykit.yaml`
3. Replace `requests.post(webhook_url, json=...)` with `notifykit send`

**Before:**

```bash
curl -X POST -H 'Content-type: application/json' \
  --data '{"text":"Deploy complete"}' \
  "$SLACK_WEBHOOK_URL"
```

**After:**

```bash
notifykit send "Deploy complete" --level info --tag deploy
```

### From multiple notification libraries

If your project uses different libraries for each channel (slack-sdk, python-telegram-bot, etc.):

1. Remove individual channel dependencies
2. Add `notifykit` as a single dependency
3. Configure all channels in one YAML file
4. Replace all send calls with `router.send()`

Benefits: unified retry logic, rate limiting, audit logging, and a single config file.

---

## Advanced Usage

### Custom Payload for Webhooks

Override the default JSON payload for webhook channels:

```yaml
channels:
  pagerduty:
    type: webhook
    levels: [critical]
    settings:
      url: https://events.pagerduty.com/v2/enqueue
      headers:
        Content-Type: application/json
      payload:
        routing_key: ${PAGERDUTY_KEY}
        event_action: trigger
        payload:
          summary: "{{body}}"
          severity: critical
          source: notifykit
```

### Rate Limiting Configuration

Prevent flooding channels with per-channel rate limits:

```yaml
channels:
  slack-alerts:
    type: slack
    rate_limit: 10  # max 10 messages per minute
    settings:
      webhook_url: ${SLACK_WEBHOOK_URL}
```

### Template Variables from Environment

Combine templates with environment variables for dynamic notifications:

```bash
export DEPLOY_ENV=production
export DEPLOY_VERSION=2.1.0

notifykit send "" \
  --template deploy \
  --var "env=$DEPLOY_ENV" \
  --var "version=$DEPLOY_VERSION" \
  --var "status=success" \
  --tag deploy
```

### Quiet Mode for Scripts

Use `--quiet` to suppress output in scripts that only need the exit code:

```bash
notifykit send "Health check passed" --level info --quiet
if [ $? -eq 0 ]; then
  echo "Notifications sent successfully"
fi
```

### JSON Output for CI Parsing

Parse send results in CI pipelines:

```bash
RESULT=$(notifykit send "Build passed" --level info --json)
SUCCESS=$(echo "$RESULT" | jq -r '.success')
FAILED=$(echo "$RESULT" | jq -r '.failed')

if [ "$SUCCESS" = "false" ]; then
  echo "Warning: $FAILED channel(s) failed"
fi
```

See [`examples/real-world.py`](examples/real-world.py) for Python integration patterns including FastAPI error handlers, batch notifications, and escalation configurations.

---

## Testing

```bash
# Install dev dependencies
pip install -e ".[dev]"

# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific test module
pytest tests/test_channels.py

# Run specific test class
pytest tests/test_channels.py::TestSlackChannel
```

All channel tests use mock HTTP (`responses` library) — no real API calls are made during testing.

### Test Coverage

| Module | Tests | Coverage |
|--------|-------|----------|
| models | 16 | Level, Format, Message, SendResult (summary), ChannelConfig |
| config | 32 | Env expansion, validation, input sanitization, typed exceptions |
| channels | 61 | All 6 channels + registry + base + edge cases + security sanitization |
| router | 11 | Level routing, tag routing, direct send |
| formatter | 15 | Plain, Markdown, HTML, format conversion |
| templates | 16 | Render, dotted access, variables, validation |
| rate_limiter | 11 | Token bucket, reset, wait time |
| retry | 9 | Success, failure, backoff calculation |
| audit | 19 | Record, query, stats, file I/O, skipped line tracking |
| cli | 21 | All 6 commands + version + JSON output + quiet mode |
| integration | 8 | Full pipeline: config -> router -> channel -> audit |
| performance | 8 | Batch 100, rate limiter under load |
| stress | 8 | 1000 notifications, multi-channel, large payloads |
| **Total** | **260** | |

---

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing`)
3. Write tests for new functionality
4. Ensure all tests pass (`pytest`)
5. Submit a pull request

### Development Setup

```bash
git clone https://github.com/JSLEEKR/notifykit.git
cd notifykit
pip install -e ".[dev]"
pytest
```

---

## Round Log

| Round | Perspective | Focus | Tests | Commit |
|-------|------------|-------|-------|--------|
| 1 | User | Onboarding — init guidance, error messages | 201 | `fix: user onboarding` |
| 2 | User | Troubleshooting docs + examples | 201 | `docs: troubleshooting + examples` |
| 3 | Developer | Coverage gaps — edge cases across modules | 212 | `test: coverage gaps` |
| 4 | Developer | Code quality — dead code, unused imports | 212 | `refactor: code quality` |
| 5 | Security | Input validation — env var injection, depth limits | 218 | `fix: input validation` |
| 6 | Security | Error recovery — connection/timeout handling | 222 | `fix: error recovery` |
| 7 | Ecosystem | CI integration — `--json` flag, GitHub Actions | 222 | `feat: CI integration` |
| 8 | Ecosystem | Cross-tool docs — GitLab, Jenkins, cron, systemd | 222 | `docs: cross-tool integration` |
| 9 | Production | Performance — 100-batch, rate limiter stress | 230 | `test: performance` |
| 10 | Production | Cycle 1 complete — badge, changelog | 230 | `docs: cycle 1 complete` |
| 11 | User C2 | UX refinements — `--quiet` flag, dry-run detail | 230 | `fix: UX refinements` |
| 12 | User C2 | Advanced docs — migration guide, real-world examples | 230 | `docs: advanced guide` |
| 13 | Developer C2 | Cycle 1 review — SendResult.summary, audit tracking | 234 | `refactor: cycle 1 review` |
| 14 | Developer C2 | Test organization — helpers module, shared fixtures | 234 | `refactor: test organization` |
| 15 | Security C2 | Security hardening — secret redaction in errors | 240 | `fix: security hardening` |
| 16 | Security C2 | Error audit — typed exception hierarchy | 244 | `fix: error audit` |
| 17 | Ecosystem C2 | Integration tests — full send pipeline | 252 | `test: integration` |
| 18 | Ecosystem C2 | Doc-code sync — README matches actual API | 252 | `docs: doc-code sync` |
| 19 | Production C2 | Stress tests — 1000 notifications, large payloads | 260 | `test: stress` |
| 20 | Production C2 | Final — badges, ROUND_LOG, CHANGELOG | 260 | `docs: final polish` |

---

## License

[MIT](LICENSE) - JSLEEKR 2026
