> ## 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`, the default import of the v7 Node SDK. 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.

# Send a welcome email from Clerk

> Turn Clerk's user.created webhook into a Courier profile and a welcome email.

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 Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

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 Guide = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="guide" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">GUIDE</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="Email, Users" />

Clerk fires `user.created` the moment someone signs up. This guide turns that into a Courier profile and a welcome message.

## What you will build

```mermaid theme={null}
flowchart LR
    A["Sign-up"] --> B["Clerk webhook"]
    B --> C{"Signature valid?"}
    C -->|Yes| D["Create profile"]
    D --> E["Welcome email"]
    C -->|No| F["Reject"]
```

## Prerequisites

* <AppLink href="https://app.courier.com/~/test/platform/api-keys">A Courier Test API key</AppLink>
* A Clerk application, and somewhere to host an HTTPS route
* A published <Doc href="/docs/design/templates/overview">template</Doc> to send

## Why this needs a route in your app

Courier has an <Doc href="/docs/monitor/webhooks/inbound">inbound webhook</Doc> that many sources can post to directly. Clerk is not one of them, for three reasons worth knowing before you start.

**The field names do not line up.** Courier reads `event` and `userId` from a payload. Clerk sends `type` and `data.id`. Posted straight through, the event arrives named `custom` with no recipient attached.

**The user does not exist yet.** An inbound event only starts a journey when its `userId` matches an existing Courier user. On `user.created` there is nothing to match, so the profile has to be written first.

**Courier does not verify Clerk's signature.** Courier's inbound URL is its own credential and accepts any JSON sent to it. Verify the signature in your own endpoint before forwarding the event, because that is what stops a forged sign-up.

So the shape is Clerk to your route to Courier. Your route is where verification happens and where the profile is created.

## Set it up

<Steps>
  <Step title="Install the packages">
    ```bash theme={null}
    npm install @clerk/backend @trycourier/courier
    ```

    Put both secrets in your environment. Clerk shows the signing secret when you create the endpoint in the next step.

    ```bash .env.local theme={null}
    COURIER_API_KEY=YOUR_COURIER_API_KEY
    CLERK_WEBHOOK_SIGNING_SECRET=whsec_...
    ```
  </Step>

  <Step title="Write the route handler">
    `verifyWebhook` checks the Svix signature Clerk sends and throws when it fails. Verify before you read anything out of the body.

    ```tsx app/api/clerk/route.ts lines theme={null}
    import { verifyWebhook } from "@clerk/backend/webhooks";
    import Courier from "@trycourier/courier";

    const courier = new Courier({ apiKey: process.env.COURIER_API_KEY! });

    export async function POST(request: Request) {
      let evt;
      try {
        // Throws on a bad signature or a replayed request.
        evt = await verifyWebhook(request);
      } catch {
        return new Response("Invalid signature", { status: 400 });
      }

      if (evt.type !== "user.created") {
        return new Response("Ignored", { status: 200 });
      }

      const { id, email_addresses, primary_email_address_id, first_name } = evt.data;

      // Match the primary id rather than taking [0]. A user with several
      // addresses can have the primary at any index.
      const email = email_addresses?.find((e) => e.id === primary_email_address_id)
        ?.email_address;

      if (!email) {
        return new Response("No email on user", { status: 200 });
      }

      // Use Clerk's id as the Courier user id so both systems agree.
      await courier.profiles.create(id, {
        profile: { email, given_name: first_name ?? undefined },
      });

      await courier.send.message({
        message: {
          to: { user_id: id },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: { name: first_name ?? "there" },
        },
      });

      return new Response("OK", { status: 200 });
    }
    ```

    Two decisions in that file are worth stating.

    **Clerk's `id` becomes the Courier `user_id`.** One identifier across both systems means every later send addresses the user without a lookup table.

    **A non-matching event returns `200`.** Clerk retries anything that is not a success, so returning an error for an event you deliberately ignore creates a retry loop.
  </Step>

  <Step title="Point Clerk at the route">
    In the Clerk dashboard, under **Configure → Webhooks**, add an endpoint for your route's public URL and subscribe it to **`user.created`**.

    Copy the signing secret Clerk shows into `CLERK_WEBHOOK_SIGNING_SECRET`.

    <Note>
      Clerk needs a public HTTPS URL, so `localhost` will not receive events. Use a tunnel such as `ngrok` while developing, and Clerk's **Testing** tab to replay an event without signing up each time.
    </Note>
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Send a test event">
    Use the **Testing** tab in Clerk's webhook settings to send a `user.created` event. Your route should answer `200`.
  </Step>

  <Step title="Check the profile exists">
    Read it back with <Endpoint method="GET" path="/profiles/{user_id}" name="Get a Profile" href="/docs/api-reference/user-profiles/get-a-profile" />, using the Clerk user id. The email you expect should be on it.
  </Step>

  <Step title="Check the message went out">
    Open <AppLink href="https://app.courier.com/logs">Logs</AppLink> and confirm the send. A `SENT` status means the provider accepted it, which is what a healthy send looks like.
  </Step>
</Steps>

If the profile exists but nothing sent, the send is the problem rather than the webhook. <Guide href="/docs/guides/debug-delivery">Debug delivery</Guide> works that half.

## Adapt it for other events

The same handler shape covers the rest of Clerk's catalog. Switch on `evt.type` and keep one route:

| Clerk event                      | What to do                                                                                    |
| :------------------------------- | :-------------------------------------------------------------------------------------------- |
| `user.created`                   | Create the profile, send a welcome                                                            |
| `user.updated`                   | Update the profile so an email change reaches the right inbox                                 |
| `user.deleted`                   | Delete the profile, per <Doc href="/docs/workspaces/security#gdpr-and-rgpd-requests">erasure</Doc> |
| `organizationMembership.created` | Add the user to a <Doc href="/docs/tenants/overview">tenant</Doc>                                  |

That last row is the one to reach for in a B2B app. Clerk organizations and Courier tenants model the same thing, so keeping them in step means branding and preferences follow the user automatically.
