Self-hosted · one container

Vercel events, in the channels you already watch.

Versend verifies the webhook signature, applies your filters and posts a clean card to Discord, Telegram, Slack, ntfy or Gotify. No database, no dashboard, no account.

Read the docs
compose.yaml12 lines
services:
  versend:
    image: ghcr.io/versend/versend:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      WEBHOOK_INTEGRATION_SECRET: ${WEBHOOK_INTEGRATION_SECRET}
      DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL}
      TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
      TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
      FILTER_EVENTS: deployment.succeeded,deployment.error
terminalhealthy
$ docker compose up -d ✔ Container versend  Started$ curl -s localhost:3000/health{ "ok": true, "destinations": ["discord", "telegram"] }

01How it works

One request in, one card out per channel.

The relay is a single Bun process behind one route. There is nothing to migrate, back up or log into.

Vercel posts a webhook to Versend, which verifies, filters and delivers it to Discord, Telegram, Slack, ntfy and Gotify.VercelPOST /hooks/vercelx-vercel-signatureVersend:3000verify signatureapply filtersrate limitdeliverDiscordTelegramSlackntfyGotify
  1. 01

    Verify

    Every request is checked against WEBHOOK_INTEGRATION_SECRET before anything is parsed. A bad signature is a 401, not a message.

  2. 02

    Filter

    FILTER_EVENTS and FILTER_EVENTS_EXCLUDE pick event types, FILTER_TARGETS picks deployment targets. A per-IP rate limit keeps the relay calm.

  3. 03

    Deliver

    The event is turned into one card and posted to every configured destination in parallel. 429s and 5xxs are retried with backoff; one slow channel never blocks another.

02What lands in the channel

The same event, drawn for each destination.

Pick an event and a destination. Every card carries the project, target, branch, commit and message, plus the build error when there is one.

webhookPOST /hooks/vercel
{
"type": "deployment.succeeded","id": "dpl_8f2a","project": "acme-web","target": "production","branch": "main","commit": "a1b2c3d","message": "Ship the billing page",…}
DISCORD_WEBHOOK_URL

Versend appToday

acme-web

Deployment ready

Ship the billing page

Invoice download and the failed-payment state.

Environment
production
Branch
main
Commit
a1b2c3d

dpl_8f2a · deployment.succeeded

04Why a relay

Small on purpose.

Vercel already knows what happened. Versend only has to prove the request is real, decide if you care, and say it once in each place. Everything else was left out.

Signed
HMAC check on every request with your integration secret.
Filtered
Allow or exclude event types; restrict deployment targets.
Patient
Per-IP rate limit and retries with backoff on 429 and 5xx.
Stateless
No database. Configuration is environment variables only.
Portable
One image for amd64 and arm64, published on every push to master.

Point the webhook at it.

Run the container, paste the URL into a Vercel webhook, and the next deployment shows up where your team is already looking.

Run the container