> ## 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.

# Migrate from an in-house system

> Replace homegrown notification code with Courier one notification type at a time.

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>;
};

Most teams do not arrive from another vendor. They arrive from a `sendEmail()` helper that grew for three years.

This guide maps that code to Courier and moves it across one notification type at a time, with both systems running until the logs agree.

## What you already built

A homegrown system is rarely one component. It is a provider SDK, a template directory, a preferences table, and a cron job, each added when something broke.

| What you maintain                                 | What replaces it                                                                      |
| :------------------------------------------------ | :------------------------------------------------------------------------------------ |
| `sendEmail()` wrapping a provider SDK             | <Doc href="/docs/send/overview">One send call</Doc>, with the provider chosen by config    |
| A second wrapper for SMS, and a third for push    | The same call, with <Doc href="/docs/send/routing">routing</Doc> deciding the channel      |
| Hardcoded HTML, or a templates directory          | <Doc href="/docs/design/templates/overview">Templates</Doc>, editable without a deploy     |
| A `notification_preferences` table and its checks | <Doc href="/docs/recipients/preferences/overview">Preferences</Doc>, enforced at send time |
| A cron job that batches the nightly digest        | A <Doc href="/docs/journeys/nodes/digest">digest node</Doc>                                |
| Retry, backoff, and dead-letter handling          | <Doc href="/docs/send/routing#failover">Retries and provider failover</Doc>                |
| `user_id` to email and phone lookups              | <Doc href="/docs/recipients/overview">Profiles</Doc>                                       |
| A WebSocket feed, unread counts, and read state   | <Doc href="/docs/in-app/overview">Inbox</Doc>                                              |

The preferences row is the one teams underestimate. A `notification_preferences` table is easy to add and hard to finish, because every new notification type needs a column, a migration, and a check at every call site.

## What this actually buys you

Frame the project as deleting code you maintain, not as adopting a platform. That is what it is, and it is the honest way to size it.

**Provider changes stop being deploys.** Swapping SendGrid for SES becomes configuration. Adding a backup provider becomes configuration.

**Copy changes stop being tickets.** A PM edits a template and publishes. Your queue does not know the difference.

**The failure modes get names.** A homegrown send that vanishes leaves you grepping logs. Courier gives every message a <Doc href="/docs/send/statuses">status</Doc> and a reason, so `UNROUTABLE` and `BOUNCED` and `OPT_IN_REQUIRED` are distinguishable.

Cost is worth checking early rather than at the end. <Doc href="/docs/workspaces/overview#what-counts-as-a-billable-event">What counts as a billable event</Doc> is the number to model against, and it counts sends rather than seats or channels.

## Migrate incrementally

Nobody rewrites every notification in one release. Move one type, prove it, then move the next.

<Steps>
  <Step title="Inventory what you send">
    List every notification your code can produce, and for each one record the trigger, the channel, and the audience.

    The list is usually longer than anyone expects, and it is the artefact the rest of the migration runs on. Sort it by volume. The lowest-volume, least-critical notification is the one to move first.
  </Step>

  <Step title="Connect the providers you already use">
    Add your existing SendGrid, Twilio, or FCM credentials as <Doc href="/docs/integrations/overview">integrations</Doc>. You keep the same accounts, the same sending domains, and the same sender reputation.

    Nothing about deliverability changes at this step, which is what makes the cutover reversible.
  </Step>

  <Step title="Move your users in">
    Send your existing user records to <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile" /> and keep the id your database already uses. <Guide href="/docs/guides/import-your-users">Import your users</Guide> covers the bulk path.

    Reusing your own `user_id` matters more than it looks. It means no mapping table, and it means a send from anywhere in your code can address a user with the id it already has.
  </Step>

  <Step title="Rebuild one notification">
    Take the lowest-stakes item from your inventory. Recreate its content as a <Doc href="/docs/design/templates/overview">template</Doc>, then replace that one call site with a Courier send.

    Leave every other call site alone.
  </Step>

  <Step title="Run both, and compare">
    Keep your old path sending, and have Courier send to a test recipient in the <Doc href="/docs/workspaces/overview#environments-and-api-keys">Test environment</Doc>. Compare what arrives.

    You are checking three things: the content renders the same, the recipient resolves to the same person, and the timing is what you expect.
  </Step>

  <Step title="Cut over, then delete">
    Point production at the Courier send and remove the old code path. Not commented out, removed. A dead path that still compiles is one someone reintroduces in six months.

    Then take the next item from the inventory.
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Confirm delivery in the logs">
    Open <AppLink href="https://app.courier.com/logs">Logs</AppLink> and find the message. The timeline shows routing, rendering, and the provider handoff.
  </Step>

  <Step title="Check a preference is honored">
    Opt a test user out of the topic, send again, and confirm the message is `FILTERED` rather than delivered. That proves your preferences moved, which is the part most likely to be missed.
  </Step>

  <Step title="Compare against the old system">
    For the notification you moved, confirm the volume in Courier matches what your old path used to send over the same period. A gap means a call site you have not found yet.
  </Step>
</Steps>

## What you can delete at the end

The point of the inventory is that it doubles as a deletion list.

* Provider SDK wrappers, and the per-provider error handling around them
* The template directory, and whatever renders it
* The preferences table, its migrations, and every call-site check
* Retry, backoff, and dead-letter queues for notification sends
* The cron jobs that batch digests
* The WebSocket server behind your in-app feed, if you adopt <Doc href="/docs/in-app/overview">Inbox</Doc>

Keep one thing: whatever decides *that* a notification should happen. That logic is your product, and it belongs in your code. Courier owns everything after that decision.
