> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.teekrr.com/llms.txt.
> For full documentation content, see https://docs.teekrr.com/llms-full.txt.

# Webhooks guide

When a message reaches a terminal state (delivered, failed, billed, or bounced) Teekrr POSTs a JSON event to every active webhook endpoint you've registered for that event type. Webhooks are managed in-app at `/api-management → Webhooks`.

## Headers Teekrr sends

| Header               | Value                                                                           |
| -------------------- | ------------------------------------------------------------------------------- |
| `Content-Type`       | `application/json`                                                              |
| `X-Teekrr-Signature` | HMAC-SHA256 hex of the raw request body, signed with your webhook's secret hash |
| `X-Teekrr-Event`     | The event name: `delivered` \| `failed` \| `billed` \| `bounced`                |

## Payload envelope

```json
{
  "event": "delivered",
  "timestamp": "2026-05-06T10:15:00.123Z",
  "data": { "...event-specific fields..." }
}
```

The exact `data` shape depends on the event — see the [API Reference](/api-reference) section's "Webhooks" group for per-event schemas and examples.

## Verifying the signature

The signature is `HMAC-SHA256(secretHash, rawBody)`. The HMAC key is the **SHA-256 hash** of your plaintext secret, **not** the plaintext itself. You receive the plaintext once at creation; Teekrr stores only the hash.

<CodeBlocks>
  ```js title="Node.js (Express)"
  import express from "express";
  import { createHash, createHmac, timingSafeEqual } from "crypto";

  const app = express();

  // IMPORTANT: capture raw body. JSON parsing changes whitespace and breaks the signature.
  app.post(
    "/webhooks/teekrr",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const secret = process.env.TEEKRR_WEBHOOK_SECRET;
      const signature = req.header("X-Teekrr-Signature");
      const event = req.header("X-Teekrr-Event");

      const secretHash = createHash("sha256").update(secret).digest("hex");
      const expected = createHmac("sha256", secretHash)
        .update(req.body) // Buffer of raw bytes
        .digest("hex");

      const a = Buffer.from(expected, "hex");
      const b = Buffer.from(signature ?? "", "hex");
      if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.status(401).send("invalid signature");
      }

      const payload = JSON.parse(req.body.toString("utf8"));
      // payload.event === event (e.g. "delivered")
      // payload.data === { messageUuid, ... }

      // ... your business logic ...

      res.status(200).send("ok");
    }
  );
  ```

  ```python title="Python (FastAPI)"
  import hmac, hashlib
  from fastapi import FastAPI, Request, HTTPException

  app = FastAPI()
  SECRET = os.environ["TEEKRR_WEBHOOK_SECRET"]
  SECRET_HASH = hashlib.sha256(SECRET.encode()).hexdigest().encode()

  @app.post("/webhooks/teekrr")
  async def teekrr_webhook(request: Request):
      raw = await request.body()  # IMPORTANT: read raw before .json()
      signature = request.headers.get("X-Teekrr-Signature", "")
      expected = hmac.new(SECRET_HASH, raw, hashlib.sha256).hexdigest()
      if not hmac.compare_digest(expected, signature):
          raise HTTPException(401, "invalid signature")

      payload = await request.json()
      # payload["event"], payload["data"]

      # ... your business logic ...

      return {"ok": True}
  ```

  ```go title="Go (net/http)"
  package main

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

  var secretHash = sha256.Sum256([]byte(os.Getenv("TEEKRR_WEBHOOK_SECRET")))

  func handleTeekrrWebhook(w http.ResponseWriter, r *http.Request) {
      raw, _ := io.ReadAll(r.Body)
      sig := r.Header.Get("X-Teekrr-Signature")

      mac := hmac.New(sha256.New, []byte(hex.EncodeToString(secretHash[:])))
      mac.Write(raw)
      expected := hex.EncodeToString(mac.Sum(nil))

      if !hmac.Equal([]byte(expected), []byte(sig)) {
          http.Error(w, "invalid signature", http.StatusUnauthorized)
          return
      }
      // ... handle the event ...
      w.WriteHeader(http.StatusOK)
  }
  ```
</CodeBlocks>

<Warning>
  **Always use the raw, untouched request body** for verification. JSON parsing changes whitespace and breaks the signature. Express needs `express.raw({ type: "application/json" })`. FastAPI needs `await request.body()` *before* `request.json()`.
</Warning>

## Retries & timeouts

* Teekrr applies a **10-second connect/read timeout** per delivery.
* Teekrr does **not** automatically retry failed webhook deliveries today — design your endpoint to be **idempotent**, and use the dashboard's Message Logs as a backstop.
* Every delivery attempt is recorded at `/api-management → Webhooks → Delivery Logs`.

## Idempotency

Each event includes the message UUID inside `data` (e.g. `data.messageUuid`). De-dupe on `(event, messageUuid)` to be safe against future retry-on-failure rollouts.

## The four event types

| Event       | When                                                         | Channel                |
| ----------- | ------------------------------------------------------------ | ---------------------- |
| `delivered` | Provider confirms delivery to recipient device / inbox       | SMS · WhatsApp · Email |
| `failed`    | Provider returns permanent failure; reserved credit released | SMS · WhatsApp · Email |
| `billed`    | Reserved credit settled (provider accepted message)          | SMS · WhatsApp · Email |
| `bounced`   | SES bounce notification (hard or soft)                       | Email only             |

For full per-event payload shapes, see the **Webhooks** group in the [API Reference](/api-reference).