Skip to content

Webhook Examples

Daniel Ellison edited this page Sep 3, 2026 · 1 revision

Webhook Examples

Kai has two webhook endpoints for receiving notifications from external services:

  • /webhook/github - GitHub-specific, validates the X-Hub-Signature-256 HMAC against GITHUB_WEBHOOK_SECRET; it ignores the X-Webhook-Secret header entirely
  • /webhook - generic endpoint that accepts any JSON object, authenticated by the X-Webhook-Secret header matching GENERIC_WEBHOOK_SECRET

Each endpoint is registered only when its secret is configured; without one, the route returns 404. The generic endpoint looks for a message field in the JSON body, or dumps the full payload if none is found, capped at about four million characters (413 beyond that). A non-object JSON body gets a 400.

Kai records an accepted delivery as a canonical message on your generic notification route channel, and the delivery outbox fans it out to the surfaces you use; the response is structured ({"status", "message_id", "inserted"}), and a failure to record returns 503 so a well-behaved sender retries. Include an Idempotency-Key (or X-Kai-Delivery) header and retries become exactly-once: a replay of the same key reports "inserted": false instead of posting a duplicate.

The GitHub integration is covered in Exposing Kai to the Internet. This page focuses on the generic webhook and what you can do with it.

Security considerations

Read this before setting up external webhooks. If you're only using webhooks from the same machine or LAN (Home Assistant, local Docker, cron scripts), many of these risks don't apply.

Network exposure

Kai's HTTP server binds to loopback only, so accepting webhooks from external services means fronting it with a tunnel or reverse proxy (see Exposing Kai to the Internet).

Recommendations:

  • Use ingress rules that forward only /webhook, /webhook/github, and /health; the internal /api/* routes have no business being reachable, even though they authenticate with scoped per-agent credentials rather than the webhook secret.
  • Terminate TLS at the proxy (Caddy and Cloudflare both handle this for you).

Transport security

The X-Webhook-Secret header is sent in plaintext over HTTP. Over the public internet, anyone on the network path can read it.

Recommendation: Always use HTTPS for internet-facing webhooks. A reverse proxy with Let's Encrypt (Caddy does this automatically) is the simplest approach.

Shared secret model

The generic webhook endpoint uses a single shared secret for all sources. If any one source leaks it (a compromised CI runner, a misconfigured Home Assistant instance, a script checked into a public repo), every generic integration can send you forged notifications until you rotate it.

The blast radius stops there. The generic secret authenticates the generic endpoint and nothing else: the internal /api/* routes use scoped per-agent credentials, the GitHub endpoint uses its own HMAC secret, and Kai refuses to start if any two ingress secrets match, so no secret can moonlight as another.

Recommendations:

  • Treat the webhook secret like a password. Don't hardcode it in scripts checked into version control; use environment variables or CI secret stores.
  • Rotate it by re-running the wizard and updating your callers.

Prompt injection via webhook payloads

Webhook payloads are recorded as notification messages and delivered to your surfaces as plain text. No agent processes them directly. However, if you reply to or reference a webhook notification in conversation with Kai, the payload content enters the model's context window. A malicious or compromised service could craft a payload containing adversarial instructions.

Recommendations:

  • Only accept webhooks from services you trust.
  • If you notice unusual content in webhook messages, don't ask Claude to act on it.
  • Claude Code's sandbox provides a layer of protection, but should not be relied upon as the sole defense.

Information disclosure

Webhook payloads often contain sensitive information: repository names, branch names, commit messages, usernames, IP addresses, server hostnames, error messages with stack traces, payment amounts. All of this is recorded in Kai's canonical database and delivered to whatever surfaces you use.

Recommendations:

  • Ensure your Telegram account has two-factor authentication enabled.
  • Be aware of what data each integration sends.

Risk summary

RiskSeverityMitigation
HTTP exposes secret in transitHighAlways use HTTPS (reverse proxy + TLS)
Single shared secret for all generic sourcesMediumTreat secret like a password, use env vars; blast radius is forged notifications only
Prompt injection via payloadsMediumOnly connect trusted services
Information disclosure in chatLowEnable Telegram 2FA, be aware of payload content
Relay scripts as attack surfaceLowMinimize relay usage, keep them simple

Generic webhook basics

Any service that can POST JSON can notify you through Kai. The minimum request:

curl -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-d '{"message": "Something happened!"}'

For anything that retries (CI when: always steps, monitoring loops, payment relays), add an idempotency key so a retry can't post the same notification twice:

curl -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-H "Idempotency-Key: deploy-$CI_PIPELINE_ID" \
-d '{"message": "Deploy finished"}'

Pick a key that identifies the event (a pipeline ID, an alert ID, a Stripe event ID); Kai records each key once and answers replays with "inserted": false.


CI/CD pipelines

GitLab CI

Add to .gitlab-ci.yml:

notify:
stage: deployscript:
- | STATUS=$([[ "$CI_JOB_STATUS" == "success" ]] && echo "succeeded" || echo "failed") curl -s -X POST http://your-server:8080/webhook \ -H "Content-Type: application/json" \ -H "X-Webhook-Secret: $GENERIC_WEBHOOK_SECRET" \ -d "{\"message\": \"Pipeline $STATUS for $CI_PROJECT_NAME ($CI_COMMIT_REF_NAME)\"}"when: always

Jenkins

Use the HTTP Request plugin in a post-build step, or add to a Jenkinsfile:

post {
always {
sh """ curl -s -X POST http://your-server:8080/webhook \ -H 'Content-Type: application/json' \ -H 'X-Webhook-Secret: ${GENERIC_WEBHOOK_SECRET}' \ -d '{"message": "Build #${BUILD_NUMBER}${currentBuild.result} - ${JOB_NAME}"}'"""
}
}

GitHub Actions

For repos where you don't want full GitHub webhook integration, add a notification step:

- name: Notify Kaiif: always()run: | STATUS=${{ job.status }} curl -s -X POST http://your-server:8080/webhook \ -H "Content-Type: application/json" \ -H "X-Webhook-Secret: ${{ secrets.KAI_WEBHOOK_SECRET }}" \ -d "{\"message\": \"Workflow $STATUS for ${{ github.repository }} (${{ github.ref_name }})\"}"

Server monitoring

Uptime Kuma

In the notification settings, add a webhook with:

  • URL: http://your-server:8080/webhook
  • Method: POST
  • Header: X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET
  • Body: {"message": "{{ msg }}"}

Disk usage alert

Add to crontab on any server:

# Check disk usage every hour, alert if over 90%
0 **** USAGE=$(df -h / | awk 'NR==2 {print $5}'| tr -d '%'); \
[ "$USAGE"-gt 90 ] && curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-d "{\"message\": \"Disk usage alert: ${USAGE}% on $(hostname)\"}"

Healthchecks.io

Configure a webhook integration pointing to your Kai endpoint. Healthchecks sends JSON payloads when checks fail or recover.


Home automation

Home Assistant

Add a REST command in configuration.yaml:

rest_command:
notify_kai:
url: "http://your-server:8080/webhook"method: POSTheaders:
Content-Type: "application/json"X-Webhook-Secret: "YOUR_SECRET"payload: '{"message": "{{ message }}"}'

Then use it in automations:

automation:
- alias: "Notify when washing machine finishes"trigger:
- platform: stateentity_id: sensor.washing_machine_powerto: "0"for: "00:05:00"action:
- service: rest_command.notify_kaidata:
message: "Washing machine is done!"
- alias: "Notify on front door open"trigger:
- platform: stateentity_id: binary_sensor.front_doorto: "on"action:
- service: rest_command.notify_kaidata:
message: "Front door opened at {{ now().strftime('%H:%M') }}"

Docker / Portainer

Portainer

In Settings > Notifications, add a webhook notification endpoint. Portainer will POST container events (start, stop, crash, restart) to your endpoint.

Docker events via script

Monitor container events and forward to Kai:

#!/bin/bash
docker events --filter 'event=die' --format '{{.Actor.Attributes.name}}'|whileread name;do
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-d "{\"message\": \"Container crashed: ${name}\"}"done

Deployments

Netlify

In Site settings > Build & deploy > Deploy notifications, add an outgoing webhook:

  • URL: http://your-server:8080/webhook
  • Event: "Deploy succeeded" (or "Deploy failed")

Note: Netlify doesn't support custom headers natively in their webhook UI. You may need to use a Netlify function or build plugin to add the X-Webhook-Secret header, or set up a small proxy.

Vercel

Use a project webhook in Settings > Git > Deploy Hooks, then forward via a simple script or integration.


Payments

Stripe

Stripe uses its own signature scheme that doesn't match Kai's X-Webhook-Secret header. You need a small relay script that validates the Stripe signature and forwards to Kai:

# stripe_relay.py - minimal exampleimportstripe, requests, jsonfromflaskimportFlask, requestapp=Flask(__name__)
@app.route("/stripe", methods=["POST"])defhandle():
event=stripe.Webhook.construct_event(
request.data, request.headers["Stripe-Signature"], "whsec_..."
)
msg=f"Stripe: {event['type']} - {event['data']['object'].get('amount', '')/100:.2f}{event['data']['object'].get('currency', '').upper()}"requests.post("http://localhost:8080/webhook",
headers={"Content-Type": "application/json", "X-Webhook-Secret": "YOUR_SECRET"},
json={"message": msg})
return"ok"

Note: Relay scripts add attack surface - each one runs its own HTTP server and holds credentials. Only add relays for services you genuinely need.


Simple notifications from scripts

Any script or cron job can notify you. A few one-liners:

# Notify when a long-running job finishes
./train_model.py && curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-d '{"message": "Model training complete!"}'# Notify on SSH login (add to /etc/profile or ~/.bashrc on a remote server)# Note: requires the webhook endpoint to be reachable from the remote server
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-d "{\"message\": \"SSH login: $(whoami)@$(hostname) from ${SSH_CLIENT%%*}\"}"# Notify when a backup finishes
rsync -a /data /backup && curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-d "{\"message\": \"Backup of /data completed at $(date)\"}"

Clone this wiki locally