> ## Documentation Index
> Fetch the complete documentation index at: https://docs.simplefunctions.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook receiver

> Receiver patterns in TypeScript, Python, and Go — verify HMAC signature, idempotency-key on delivery id, replay failures.

This page covers the receiver side: how to write code that accepts SimpleFunctions webhook deliveries, verifies them, and handles retries safely.

## Headers

Every delivery includes:

```http theme={null}
X-SF-Signature: t=<unix_ts>,v1=<hex_hmac>
X-SF-Endpoint-Id: we_...
X-SF-Delivery-Id: del_...
X-SF-Event: alert.fired
Content-Type: application/json
```

## Receiver implementations

<CodeGroup>
  ```ts TypeScript / Express theme={null}
  import express from 'express'
  import crypto from 'crypto'

  const app = express()
  const SECRET = process.env.SF_WEBHOOK_SECRET!
  const seen = new Set<string>()  // replace with Redis in prod

  app.post('/sf-hook', express.raw({ type: 'application/json' }), (req, res) => {
    const sig = req.header('x-sf-signature') ?? ''
    const deliveryId = req.header('x-sf-delivery-id') ?? ''
    if (!verify(SECRET, sig, req.body)) return res.status(401).send('bad sig')
    if (seen.has(deliveryId)) return res.status(200).send('ok (dedupe)')
    seen.add(deliveryId)

    const event = JSON.parse(req.body.toString('utf8'))
    console.log(event.type, event.data)
    res.status(200).send('ok')
  })

  function verify(secret: string, sigHeader: string, body: Buffer): boolean {
    const [tPart, v1Part] = sigHeader.split(',')
    const t = tPart?.split('=')[1]
    const v1 = v1Part?.split('=')[1]
    if (!t || !v1) return false
    const expected = crypto.createHmac('sha256', secret).update(`${t}.${body.toString()}`).digest('hex')
    if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false
    return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))
  }
  ```

  ```python Python / Flask theme={null}
  import hmac, hashlib, time, os
  from flask import Flask, request, abort

  app = Flask(__name__)
  SECRET = os.environ['SF_WEBHOOK_SECRET'].encode()
  seen = set()

  @app.post('/sf-hook')
  def hook():
      sig = request.headers.get('x-sf-signature', '')
      delivery_id = request.headers.get('x-sf-delivery-id', '')
      body = request.get_data()
      if not verify(sig, body): abort(401)
      if delivery_id in seen: return ('ok (dedupe)', 200)
      seen.add(delivery_id)
      event = request.get_json()
      print(event['type'], event['data'])
      return 'ok'

  def verify(sig_header: str, body: bytes) -> bool:
      parts = dict(p.split('=', 1) for p in sig_header.split(','))
      t, v1 = parts.get('t'), parts.get('v1')
      if not t or not v1: return False
      if abs(time.time() - int(t)) > 300: return False
      expected = hmac.new(SECRET, f'{t}.{body.decode()}'.encode(), hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, v1)
  ```

  ```go Go / net/http theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "fmt"
      "io"
      "net/http"
      "os"
      "strconv"
      "strings"
      "sync"
      "time"
  )

  var (
      secret = []byte(os.Getenv("SF_WEBHOOK_SECRET"))
      seen   = sync.Map{}
  )

  func handle(w http.ResponseWriter, r *http.Request) {
      body, _ := io.ReadAll(r.Body)
      if !verify(r.Header.Get("X-SF-Signature"), body) {
          w.WriteHeader(401); return
      }
      deliveryID := r.Header.Get("X-SF-Delivery-Id")
      if _, dupe := seen.LoadOrStore(deliveryID, true); dupe {
          fmt.Fprintln(w, "ok (dedupe)"); return
      }
      fmt.Fprintln(w, "ok")
  }

  func verify(sigHeader string, body []byte) bool {
      parts := map[string]string{}
      for _, p := range strings.Split(sigHeader, ",") {
          kv := strings.SplitN(p, "=", 2)
          if len(kv) == 2 { parts[kv[0]] = kv[1] }
      }
      t, v1 := parts["t"], parts["v1"]
      if t == "" || v1 == "" { return false }
      ts, _ := strconv.ParseInt(t, 10, 64)
      if abs(time.Now().Unix()-ts) > 300 { return false }
      mac := hmac.New(sha256.New, secret)
      mac.Write([]byte(fmt.Sprintf("%s.%s", t, string(body))))
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(expected), []byte(v1))
  }

  func abs(n int64) int64 { if n < 0 { return -n }; return n }
  ```
</CodeGroup>

## Replay

If your endpoint went down, SimpleFunctions persists failed deliveries in `alert_deliveries`. Replay:

```bash theme={null}
sf deliveries --json --status failed
# pick a delivery id
curl -X POST -H "Authorization: Bearer $SF_API_KEY" \
  https://simplefunctions.dev/api/alert-deliveries/<delivery-id>/retry
```

## Idempotency

Always idempotency-key on `X-SF-Delivery-Id`. SimpleFunctions retries on 5xx and timeouts up to 5 attempts; same delivery id is reused.

## Auto-pause

Endpoints that 5xx more than 50% across a 24h window get auto-paused. Re-enable:

```bash theme={null}
sf webhooks resume <endpoint-id>
```

## See also

<CardGroup cols={2}>
  <Card title="Webhook events" href="/reference/webhook-events">
    Payload schemas per event type.
  </Card>

  <Card title="Watch + alerts API" href="/api-reference/watch-alerts">
    Endpoints for managing watched objects, rules, endpoints.
  </Card>
</CardGroup>
