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

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Track delivery with webhooks

> Receive every delivery event in your own system through an outbound webhook.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

<Tags items="Webhooks, Logs" />

Get every delivery event in your own system, so status lives with the rest of your data.

An outbound webhook posts Courier events to an endpoint you own. Use it to log deliveries, sync status to your database, or kick off downstream work.

## What you will build

```mermaid theme={null}
flowchart LR
    A["Message sent"] --> B["Status changes"]
    B --> C["Courier signs it"]
    C --> D["Your endpoint verifies"]
```

## Prerequisites

* A public HTTPS endpoint that accepts a `POST` and returns `2xx`
* <AppLink href="https://app.courier.com/settings">The Courier console for the environment you want events from</AppLink>
* <Doc href="/docs/monitor/webhooks/events">The event types and payloads</Doc>

## Set up the webhook

<Steps>
  <Step title="Create the webhook destination">
    Create destinations in the console, not through the API. Open <AppLink href="https://app.courier.com/settings">Settings</AppLink> in the environment you want (test or production), find **Outbound Webhooks**, and add a destination with your endpoint URL. Name it so you can tell destinations apart later. Courier generates a signing secret (`whsec_...`). Copy it and store it as a secret in your app. You use it to verify requests really came from Courier.

    <Warning>
      A destination fires only for the environment it was created in, so create one in test and one in production if you need both.
    </Warning>
  </Step>

  <Step title="Verify the signature and handle events">
    Courier signs every request with a `courier-signature` header. Verify it against the **raw** request body before you trust the payload. Then branch on the event `type`. A destination receives every event type, so filter to the ones you care about.

    ```javascript theme={null}
    import crypto from "crypto";
    import express from "express";

    const app = express();

    // Capture the raw body so the signature matches the exact bytes Courier hashed.
    app.post("/courier-webhook", express.raw({ type: "application/json" }), (req, res) => {
      const header = req.get("courier-signature") ?? "";
      const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
      const expected = crypto
        .createHmac("sha256", process.env.COURIER_WEBHOOK_SECRET)
        .update(`${parts.t}.${req.body.toString("utf8")}`, "utf8")
        .digest("hex");

      const digest = Buffer.from(expected, "hex");
      const provided = Buffer.from(parts.signature ?? "", "hex");

      // timingSafeEqual throws unless both buffers are the same length. Compare
      // lengths first, or a forged signature becomes a 500 instead of a 401, and
      // Courier retries a 500.
      const valid =
        provided.length === digest.length && crypto.timingSafeEqual(digest, provided);
      if (!valid) return res.sendStatus(401);

      const { type, data } = JSON.parse(req.body.toString("utf8"));
      if (type === "message:updated") {
        // Sync the delivery status to your system.
        console.log(data.id, data.status);
      }

      res.sendStatus(200); // Acknowledge fast; do slow work asynchronously.
    });
    ```
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Send a message and watch for the event">
    Send in the same environment the destination belongs to. Within moments your endpoint receives a `message:updated` event as the message moves through `ENQUEUED`, `SENT`, and `DELIVERED`.
  </Step>

  <Step title="Confirm a valid signature passes">
    Log the result of the signature comparison. If it never passes, check that you are hashing the raw body, not a re-serialized object.
  </Step>

  <Step title="Confirm a tampered body fails">
    Replay a request with one byte of the body changed and confirm your handler returns `401`.
  </Step>
</Steps>

## Skip the consumer

Webhooks drive your own logic. To forward events to a monitoring platform without building a consumer, connect an outbound integration in the console:

* <Doc href="/docs/integrations/observability/datadog">Datadog</Doc>
* <Doc href="/docs/integrations/observability/new-relic">New Relic</Doc>
* <Doc href="/docs/integrations/observability/open-telemetry">OpenTelemetry</Doc>
* A CDP such as <Doc href="/docs/integrations/cdp/segment">Segment</Doc> or <Doc href="/docs/integrations/cdp/rudderstack">Rudderstack</Doc>
