◆ INTEGRATIONS

Webhooks

Every Swing Deck alert can be POSTed to a URL you control — signed with HMAC-SHA256 so you know it really came from your dashboard. Wire it into Discord, Slack, n8n, Zapier, or a custom bot.

⬡ TIER Outbound webhooks are a Pro+ feature. Add up to 5 endpoints per install in Settings → 🔔 Alerts → Outbound Webhooks. HTTPS required (localhost allowed for testing).

Quick start

  1. Open the dashboard → icon → 🔔 Alerts tab
  2. Scroll to Outbound Webhooks, paste your URL, click Add webhook
  3. Copy the 64-char HMAC secret that appears — it's shown once, never again
  4. Configure your endpoint to verify the signature (examples below)
  5. Click Send test alert to verify the round trip

Payload shape

Every webhook delivery is a POST with Content-Type: application/json. Body:

{
  "event":     "stop_loss_breach",
  "severity":  "critical",
  "ticker":    "NVDA",
  "message":   "NVDA breached its stop-loss. Price $180.00 vs stop $200.00 (-18.2% from entry).",
  "details": {
    "ticker":  "NVDA",
    "price":   180.00,
    "stop":    200.00,
    "shares":  100,
    "entry":   220.00,
    "pct_from_entry": -18.18,
    "severity": "critical"
  },
  "timestamp": "2026-04-21T14:32:05.123456+00:00"
}

Event types

eventdetailsSeverity range
stop_loss_breachticker, price, stop, shares, entry, pct_from_entrycritical / warning
score_dropprevious_score, current_score, drop, grade, pricewarning
vix_regime_changeprevious_regime, current_regime, vixwarning / critical
regime_changeprevious_regime, current_regime, subtitlewarning / critical
earnings_warningearnings_days, earnings_date, pricewarning
pillar13_decayhours_to_event, penalty, ceasefirewarning
daily_digestregime, vix, positions, top_tickersinfo

Headers

HeaderValue
Content-Typeapplication/json
User-AgentSwingDeck-Webhook/1.0
X-SwingDeck-EventEvent type (matches body.event)
X-SwingDeck-Signaturesha256=<hex> — HMAC of the raw body bytes
X-SwingDeck-Delivery16-char hex — idempotency key (dedupe on this)
X-SwingDeck-TimestampUnix seconds at dispatch time (reject old deliveries if you need replay protection)

Signature verification

⚠ ALWAYS VERIFY Your endpoint URL is public — anyone can POST to it. The signature is the only thing that proves the payload came from your Swing Deck install. Use a constant-time comparison (timingSafeEqual / hmac.compare_digest) to avoid timing attacks.

Python (FastAPI / Flask)

import hmac, hashlib
from fastapi import Request, HTTPException

SECRET = "your-64-char-hex-secret-from-swing-deck".encode()

async def verify(request: Request):
    raw = await request.body()
    provided = request.headers.get("X-SwingDeck-Signature", "")
    if not provided.startswith("sha256="):
        raise HTTPException(status_code=401, detail="missing signature")
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, provided):
        raise HTTPException(status_code=401, detail="bad signature")
    # body is now trusted
    return await request.json()

Node.js (Express)

const crypto = require('crypto');

app.post('/swingdeck', express.raw({ type: 'application/json' }), (req, res) => {
  const SECRET = process.env.SWINGDECK_SECRET;
  const provided = req.headers['x-swingdeck-signature'] || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', SECRET)
    .update(req.body)
    .digest('hex');

  if (
    expected.length !== provided.length ||
    !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
  ) {
    return res.status(401).send('bad signature');
  }

  const alert = JSON.parse(req.body.toString());
  // alert.event, alert.ticker, alert.message, alert.details — trusted
  res.status(200).end();
});

Go (net/http)

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
)

func handle(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    provided := r.Header.Get("X-SwingDeck-Signature")

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(provided)) {
        http.Error(w, "bad signature", http.StatusUnauthorized)
        return
    }
    // body is trusted — parse and handle
    w.WriteHeader(http.StatusOK)
}

Recipes

Discord — direct webhook

Discord's "incoming webhooks" expect a different JSON shape than Swing Deck sends, so you need a tiny adapter. Use webhook.site or a Cloudflare Worker to transform:

// Cloudflare Worker — transforms Swing Deck alert → Discord embed
export default {
  async fetch(req, env) {
    const raw = await req.text();
    // TODO: verify HMAC as shown above (skipped for brevity)
    const a = JSON.parse(raw);
    const color = a.severity === 'critical' ? 0xff3d5c :
                  a.severity === 'warning'  ? 0xf0960a : 0x00bcd4;
    return fetch(env.DISCORD_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        embeds: [{
          title: `⚠ ${a.event.replace(/_/g,' ').toUpperCase()}${a.ticker ? ' — ' + a.ticker : ''}`,
          description: a.message,
          color,
          timestamp: a.timestamp,
        }],
      }),
    });
  }
};

Slack — incoming webhook

Same approach — adapter translates into Slack's blocks format:

body: JSON.stringify({
  text: `Swing Deck: ${a.event} — ${a.ticker || ''}`,
  blocks: [
    { type: 'header', text: { type: 'plain_text',
      text: `${a.event.toUpperCase()} — ${a.ticker || '—'}` }},
    { type: 'section', text: { type: 'mrkdwn', text: a.message }},
  ]
})

Zapier / n8n — no-code

Both platforms have built-in "Webhook" triggers that accept any JSON body. Paste your Swing Deck endpoint URL from the integration trigger, then filter/branch on event, severity, or ticker in the next step.

⚠ NO-CODE & HMAC Zapier and n8n can compute HMAC in a Code step but it's awkward. For low-stakes use (alerts to yourself), skipping verification is usually fine — just don't expose the URL publicly. For production, put a Cloudflare Worker in front to verify, then forward the trusted payload.

Retry & delivery guarantees

Retry policy

On any network failure or 5xx response, Swing Deck retries:

AttemptDelay since first send
10s (immediate)
2+2s
3+8s
4+32s

On a 4xx response (except 408 and 429), retries abort immediately — those indicate your endpoint rejected the payload and won't accept it on retry.

Idempotency

Use X-SwingDeck-Delivery as a dedup key. If your endpoint sees the same delivery ID twice, ignore the second one — it means a prior attempt succeeded but we didn't see the 2xx response.

Ordering

Not guaranteed. If alert A fires at t=0 and alert B fires at t=1, alert B may arrive first if A's first attempt fails. Timestamp-sort in your handler if ordering matters.

Backlog

None. If all 4 attempts fail, the alert is logged locally (webhooks_send_log.json) and dropped. Swing Deck does NOT queue undelivered webhooks — that would leak user data to disk indefinitely. If your endpoint is down, you miss that alert.