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

# Debug email delivery

> Trace an email that never arrived from Courier logs to the provider to the inbox.

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 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, Logs" />

A recipient says the email never arrived. Start in Logs and work outward, from Courier to the provider to the inbox.

## Prerequisites

* A message you can point to, or the recipient and roughly when you sent it.
* Access to your email provider's dashboard, for everything past the handoff.

## Read the status first

The status tells you which half of the trip failed. <Doc href="/docs/send/statuses">Message statuses</Doc> covers the full lifecycle. Two of them cause most of the confusion:

* **`SENT`** means the provider accepted the message. It is not a delivery confirmation.
* **`DELIVERED`** means the recipient's mail server accepted it. It is not proof the message reached the inbox.

<Warning>
  A corporate mail gateway (Proofpoint, Mimecast, Barracuda) can quarantine a message after the server accepts it. `DELIVERED` with an empty inbox usually means the message is held on the recipient's side.
</Warning>

## Find where it stopped

<Steps>
  <Step title="Open the message">
    Filter <AppLink href="https://app.courier.com/logs">Logs</AppLink> by recipient, template, or time range, then open the message.
  </Step>

  <Step title="Read the timeline">
    The timeline lists every step the message passed, from the request through routing, rendering, and the provider handoff. The last step that succeeded tells you who owns the problem.
  </Step>

  <Step title="Open the provider response">
    Select the send event to see what the provider returned, including its status code and its own message id. Use that id to find the same message in the provider's dashboard.
  </Step>
</Steps>

## See exactly what was rendered

The timeline tells you where a message stopped. It does not tell you what the recipient actually got, which is the question behind most "the notification looked wrong" reports.

<Endpoint method="GET" path="/messages/{message_id}/output" name="Get message content" href="/docs/api-reference/messages/get-message-content" /> returns the rendered content Courier delivered, broken out per channel:

<CodeGroup>
  ```javascript Node.js theme={null}
  const { results } = await client.messages.content("1-61e9e2f0-1f8b0d5e");

  for (const output of results) {
    console.log(output.channel, output.content.subject);
  }
  ```

  ```python Python theme={null}
  response = client.messages.content("1-61e9e2f0-1f8b0d5e")

  for output in response.results:
      print(output.channel, output.content.subject)
  ```

  ```bash cURL theme={null}
  curl "https://api.courier.com/messages/1-61e9e2f0-1f8b0d5e/output" \
    -H "Authorization: Bearer $COURIER_API_KEY"
  ```

  ```ruby Ruby theme={null}
  response = courier.messages.content("1-61e9e2f0-1f8b0d5e")

  response.results.each do |output|
    puts("#{output.channel} #{output.content.subject}")
  end
  ```

  ```go Go theme={null}
  response, err := client.Messages.Content(context.TODO(), "1-61e9e2f0-1f8b0d5e")
  if err != nil {
  	panic(err.Error())
  }

  for _, output := range response.Results {
  	fmt.Println(output.Channel, output.Content.Subject)
  }
  ```

  ```java Java theme={null}
  MessageContentResponse response = client.messages().content("1-61e9e2f0-1f8b0d5e");

  response.results().forEach(output ->
      System.out.println(output.channel() + " " + output.content().subject()));
  ```

  ```php PHP theme={null}
  $response = $client->messages->content('1-61e9e2f0-1f8b0d5e');

  foreach ($response->results as $output) {
      echo $output->channel, ' ', $output->content->subject, PHP_EOL;
  }
  ```

  ```csharp C# theme={null}
  var response = await client.Messages.Content("1-61e9e2f0-1f8b0d5e", new());

  foreach (var output in response.Results)
  {
      Console.WriteLine($"{output.Channel} {output.Content.Subject}");
  }
  ```

  ```bash CLI theme={null}
  courier messages content 1-61e9e2f0-1f8b0d5e \
    --api-key "$COURIER_API_KEY"
  ```

  ```text MCP theme={null}
  With Courier MCP, show me the rendered content for message 1-61e9e2f0-1f8b0d5e.
  ```
</CodeGroup>

Each result carries the `channel` it was rendered for and a `content` object holding whichever fields that channel uses, such as `subject` and `html` for email or `text` for SMS:

```json theme={null}
{
  "results": [
    {
      "channel": "email",
      "channel_id": "abc-123",
      "content": {
        "subject": "Your report is ready",
        "title": "Your report is ready",
        "html": "<html>…</html>",
        "text": "Your report is ready…"
      }
    }
  ]
}
```

This is what separates the three explanations that all look the same from the outside:

| What you see                           | What it means                                          |
| :------------------------------------- | :----------------------------------------------------- |
| A variable rendered as an empty string | The template is fine and `data` was missing a key      |
| The variable name rendered literally   | The template references a variable that does not exist |
| Content correct, provider rejected it  | Neither one. Take the payload to the provider          |

For the ordered list of status transitions and their timestamps, use <Endpoint method="GET" path="/messages/{message_id}/history" name="Get message history" href="/docs/api-reference/messages/get-message-history" />.

## Work the scenario

<AccordionGroup>
  <Accordion title="Stuck at SENT">
    Usually not a failure. Delivery confirmation is off until you configure it, per provider, so a healthy send stops at `SENT`. Check <Doc href="/docs/send/statuses#delivery-tracking-is-per-provider">how your provider reports</Doc> and set up its webhook or polling toggle.

    If tracking is already configured, the provider may be retrying a deferral, or Courier may not have polled yet. Confirm the outcome in the provider's dashboard.
  </Accordion>

  <Accordion title="UNDELIVERABLE">
    The provider rejected the message, and Courier does not retry. Open the event for the provider's error, then match it to a cause:

    * **Hard bounce.** The address does not exist, or the domain does not resolve.
    * **Soft bounce.** The mailbox is full or temporarily unavailable. Some providers retry before reporting failure.
    * **Block.** The receiving server refused the message on reputation, content, or policy.
    * **Suppression.** The address sits on the provider's suppression list from an earlier bounce or complaint. Nothing you send reaches it until you remove it there.

    A valid address that bounces on reputation points at [sender authentication](#check-sender-authentication).
  </Accordion>

  <Accordion title="DELIVERED, but not in the inbox">
    The message reached the mail server. Three things commonly take it from there:

    * **Spam or junk.** Ask the recipient to check both folders.
    * **A security gateway.** When every affected recipient shares one domain, their IT team has to check the quarantine and allowlist your sending domain.
    * **An inbox rule.** A filter the recipient set may have moved or deleted the message.

    Send the same template to a personal address as a control. If that copy arrives, the problem is the recipient's domain, not your send.
  </Accordion>

  <Accordion title="FILTERED or UNROUTABLE">
    The message never reached a provider, so the cause is inside Courier. `FILTERED` means preferences, a send condition, or a channel condition removed it. `UNROUTABLE` means no channel could be addressed, usually a missing email address on the profile. <Doc href="/docs/send/statuses#why-a-message-is-unroutable">Why a message is UNROUTABLE</Doc> lists each `reason` and its fix.
  </Accordion>
</AccordionGroup>

## Check sender authentication

Weak authentication causes failures that look random and hit many recipients at once. Confirm all three records for your sending domain:

| Record    | What it does                                                   | How to check                                                                      |
| --------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **SPF**   | Names the servers allowed to send for your domain              | Look for a `v=spf1` TXT record that includes your provider                        |
| **DKIM**  | Signs the message so the receiver can prove nothing altered it | Your provider's dashboard shows DKIM status and the record to add                 |
| **DMARC** | Tells receivers what to do when SPF or DKIM fails              | Look for a TXT record at `_dmarc.yourdomain.com`, starting at `p=none` to monitor |

Each provider's setup page walks through its own records. See the <Doc href="/docs/integrations/email/overview">email integrations</Doc>.

<Tip>
  Tracked links use a shared `ct0.app` domain by default. A <Doc href="/docs/monitor/tracking">custom tracking domain</Doc> puts them on your own domain, which some filters treat more kindly. It is available on Business and above.
</Tip>

## Ask the provider

Courier's logs end where the provider takes over, so the delivery event itself lives there:

* **SendGrid**: the Activity Feed, per-message delivery events.
* **Postmark**: Activity, with bounce and complaint detail.
* **AWS SES**: the Reputation Dashboard, for bounce and complaint rates.
* **Mailgun**: Logs, for a message's full event history.
* **Mandrill**: Outbound Activity.

## Debug from an AI agent

<Doc href="/docs/resources/mcp">Courier MCP</Doc> exposes the same message data as the console, so you can ask instead of click:

| What you want                      | Tool                     |
| ---------------------------------- | ------------------------ |
| Find the message                   | `list_messages`          |
| Read its status and provider       | `get_message`            |
| Walk the timeline                  | `get_message_history`    |
| See what was actually sent         | `get_message_content`    |
| Check the recipient's contact info | `get_user_profile_by_id` |
| Check their preferences            | `get_user_preferences`   |

```text MCP theme={null}
With Courier MCP, show me every undeliverable message from the last 24 hours and what each provider said.
```

An agent chains these, so one prompt can check a profile, confirm preferences, find the message, and read its timeline. The <Doc href="/docs/resources/cli">CLI</Doc> covers the same ground in a script.

## Verify

Send the template to a test recipient and watch the status pass `SENT` in Logs. After a suppression or authentication fix, confirm the provider's own dashboard shows the new message as delivered.
