sendword
HTTP webhook receiver that runs commands. Define hooks in TOML, trigger them with HTTP requests, see results in a web dashboard.
What it does
sendword sits next to your application as a sidecar. It listens for incoming webhooks and executes shell commands, scripts, or HTTP calls in response. Configuration lives in a single TOML file. Execution history and logs are stored in SQLite.
GitHub/CI/Monitoring ──HTTP POST──▶ sendword ──▶ shell command
script
JavaScript
Python
HTTP request
Features
- Webhook authentication --- Bearer tokens, HMAC-SHA256 signature verification, or open access. Constant-time comparison prevents timing attacks.
- Payload validation --- JSON schema per hook. Malformed payloads are rejected before your command runs.
- Trigger rules --- Filter by payload fields, restrict to time windows, enforce cooldowns, and apply rate limits before execution.
- Script runtime executors --- Shell commands with payload interpolation, executable scripts, JavaScript, Python, or HTTP forwarding. Per-hook timeouts and working directories.
- Retries with backoff --- None, linear, or exponential. Configurable per hook or globally, with max delay caps.
- Execution barriers --- Mutex or queue-based concurrency control. Approval workflows gate hooks behind human review with optional timeouts.
- Secret masking --- Redact env var values and regex pattern matches from dashboard log views.
- Backup and restore --- Snapshot database and config to S3-compatible storage on a cron schedule. Restore with one command. Retention policies handle cleanup.
- Web dashboard --- View hooks, executions, trigger attempts, and stream logs in real time via SSE.
- Config portability --- Export/import configuration as JSON. Environment variable overrides for every setting.
Quick start
Install
Install from crates.io if you already have a Rust toolchain:
Prebuilt release artifacts are also published for Linux x86_64 and Windows
x86_64. The installers put sendword in $HOME/.cargo/bin by default.
Linux x86_64:
|
Windows PowerShell:
powershell -ExecutionPolicy Bypass -c "irm https://releases.sendword.online/latest/sendword-installer.ps1 | iex"
The installers default to latest. To pin a release, set SENDWORD_VERSION
to a version tag:
| SENDWORD_VERSION=v0.8.7
Use Docker when you want sendword and its script runtimes packaged together:
Manual archives and checksums are available under
https://releases.sendword.online/latest/. Replace latest with a version tag
such as v0.8.7 to download a specific release.
Build from source
Source builds require Rust nightly. The project pins nightly-2026-01-05 via
rust-toolchain.toml.
With Nix and direnv, dependencies are managed automatically:
# Build
# Run (creates data/ directory and SQLite database on first start)
Or during development:
sendword starts on 127.0.0.1:8080 by default. Open it in a browser to access the dashboard.
Create a user
The dashboard requires authentication. Create your first user:
You'll be prompted for a password.
Configuration
sendword loads config from sendword.toml, then sendword.json, then environment variables (in that priority order). Environment variables use the SENDWORD_<SECTION>__<KEY> format.
Minimal example
[]
= "127.0.0.1"
= 8080
[[]]
= "Deploy App"
= "deploy-app"
[]
= "shell"
= "cd /opt/app && git pull && make deploy"
This creates a hook at POST /hook/deploy-app that runs the deploy command.
Full example
[]
= "127.0.0.1"
= 9090
[]
= "data/sendword.db"
[]
= "data/logs"
[]
= "24h"
= false
[]
= "smtp.example.com"
= 587
= "sendword@example.com"
= "your-smtp-password"
= "sendword@example.com"
= true
[]
= "data/scripts"
[]
= "30s"
[]
= 60
[]
= 0
= "exponential"
= "1s"
= "60s"
[]
= ["DATABASE_URL", "API_KEY", "AWS_SECRET_ACCESS_KEY"]
= ["Bearer [A-Za-z0-9._~+/=-]+", "ghp_[A-Za-z0-9]{36}"]
[[]]
= "Deploy App"
= "deploy-app"
= "Triggers app deployment"
= true
= "/opt/app"
= "120s"
[]
= "bearer"
= "secret-deploy-token"
[]
= "shell"
= "echo 'deploying $APP_ENV'"
[]
= "production"
[]
= 2
= "exponential"
= "2s"
= "30s"
[]
= 5
[]
= "30s"
= [{ = "action", = "equals", = "deploy" }]
[]
= 10
= "1h"
[[]]
= ["Mon", "Tue", "Wed", "Thu", "Fri"]
= "09:00"
= "17:00"
[]
= "mutex"
[]
= true
= "30m"
[]
= "https://hooks.slack.com/services/T00/B00/xxx"
= ["failure", "timeout"]
= '{"text": "Hook {{hook_name}} {{outcome}}"}'
[]
= "https://s3.amazonaws.com"
= "sendword-backups"
= "AKIA..."
= "..."
= "us-east-1"
= "prod/"
= "0 0 3 * * *"
[]
= 30
= "90d"
Masking is applied when logs are read for the dashboard; raw log files are left unchanged. Env-var value masks stream live with a small retained suffix. Regex masks are applied when the full log content can be evaluated, so running executions with regex masks withhold live log output until the execution reaches a terminal state.
Executor types
Shell --- runs a command in a shell process:
[]
= "shell"
= "deploy.sh --env production"
Script --- runs an executable script directly. The file needs executable permissions and a shebang:
[]
= "script"
= "data/scripts/deploy.sh"
JavaScript --- runs a script with node:
[]
= "javascript"
= "data/scripts/deploy.js"
Python --- runs a script with python3, falling back to python:
[]
= "python"
= "data/scripts/deploy.py"
HTTP --- forwards to an endpoint:
[]
= "http"
= "POST"
= "https://api.example.com/deploy"
= { = "Bearer token" }
= '{"ref": "main"}'
= true
The Docker image includes Node.js and Python. Outside Docker, JavaScript hooks require node on PATH, and Python hooks require python3 or python.
Shell commands receive the raw payload in SENDWORD_PAYLOAD. Script, JavaScript, and Python executors also receive SENDWORD_PAYLOAD, flattened payload fields as SENDWORD_FIELD_*, and a payload.json file in the execution log directory. In Node.js, read these through process.env; in Python, read them through os.environ.
console.log;
console.log;
Webhook authentication
Bearer token:
[]
= "bearer"
= "your-secret-token"
Send as: Authorization: Bearer your-secret-token
HMAC-SHA256:
[]
= "hmac"
= "X-Hub-Signature-256"
= "sha256"
= "your-hmac-secret"
Compatible with GitHub webhook signatures.
Trigger rules
Control when a hook fires:
[]
= "60s" # minimum time between executions
# Only fire when payload matches
= [
{ = "action", = "equals", = "deploy" },
{ = "environment", = "contains", = "prod" },
{ = "tag", = "regex", = "^v\\d+\\.\\d+\\.\\d+$" },
{ = "metadata.priority", = "gte", = "5" },
]
# Rate limit triggers
[]
= 10
= "1h"
# Only allow during business hours
[[]]
= ["Mon", "Tue", "Wed", "Thu", "Fri"]
= "09:00"
= "17:00"
Filter operators: equals, not_equals, contains, regex, exists, gt, lt, gte, lte.
Execution barriers
Prevent conflicting concurrent executions:
# Mutex: only one execution at a time, others are rejected
[]
= "mutex"
# Queue: executions wait in line
[]
= "queue"
= 10
Gate hooks behind human approval:
[]
= true
= "30m" # optional, auto-reject after timeout
Environment variable overrides
Every config field can be set via environment variables:
SENDWORD_SERVER__PORT=9090
SENDWORD_DATABASE__PATH=/var/lib/sendword/db.sqlite
SENDWORD_AUTH__SESSION_LIFETIME=48h
SENDWORD_DEFAULTS__TIMEOUT=60s
CLI
sendword [COMMAND]
Commands:
serve Start the web server (default)
export Export current config as JSON to stdout
import Import config from a JSON file
user User management
backup Backup management
restore Restore from a backup
Examples
# Start the server
# Export config for version control or migration
# Import config from JSON
# Create a user
# Create a backup
# List backups
# Restore from backup
Triggering hooks
Send an HTTP POST to /hook/<slug>:
# Simple trigger
# With payload
# With bearer auth
Development
sendword uses Nix flakes for development environment management and just as a command runner.
The web UI is rendered by typed Askama views in src/views/ that compose the
published wavefunk-ui component and app-shell crate. Cargo.toml is the
source of truth for the crates.io wavefunk-ui version; for local
co-development, use the gitignored .cargo/config.toml path override to point
Cargo at ../ui while preserving the published dependency metadata.
wavefunk-ui serves shared CSS, fonts, HTMX, SSE, and Wave Funk JavaScript
under /static/wavefunk. Sendword only vendors app-specific behavior under
static/js/sendword.js; the old MiniJinja template directory and local Wave
Funk CSS/font copies are no longer part of the project.
Database
Tech stack
| Layer | Choice |
|---|---|
| Language | Rust (nightly) |
| Async runtime | Tokio |
| Web framework | Axum |
| Database | SQLite via SQLx |
| Templating | Askama |
| Frontend | HTMX + wavefunk-ui |
| UI assets | wavefunk-ui asset router plus static/js/sendword.js |
| Config | Figment (TOML + JSON + env) |
License
See LICENSE for details.