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

# Message statuses and errors

> Every status a message can have, what each one means, and why a send fails.

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

A send is asynchronous. The API returns a `requestId`, and the message moves through a status lifecycle you track in the logs.

## How it works

### The status lifecycle

A message's status only advances, never backward. Two independent systems drive it. **Provider confirmation** produces `DELIVERED`. **Courier tracking** produces `OPENED` and `CLICKED`, so engagement can arrive before or without a delivery confirmation.

| Category              | Statuses                                         | Meaning                                                                       |
| --------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------- |
| Intermediate          | `ENQUEUED`, `ROUTED`, `SENT`                     | Still processing or awaiting provider confirmation. `SENT` is not terminal.   |
| Terminal (delivery)   | `DELIVERED`, `UNDELIVERABLE`                     | Provider accepted, or rejected/failed.                                        |
| Terminal (engagement) | `OPENED`, `CLICKED`                              | The recipient interacted.                                                     |
| Terminal (routing)    | `UNROUTABLE`, `CANCELED`, `FILTERED`, `UNMAPPED` | The message never reached a provider.                                         |
| Processing            | `DELAYED`, `DIGESTED`, `THROTTLED`, `SIMULATED`  | Held for later, batched into a digest, rate-limited, or sent with a mock key. |

Open a message in the <Doc href="/docs/monitor/overview">logs</Doc> to see its provider response.

### Send API responses

A successful send returns a `requestId` you use to track the message:

```json theme={null}
{ "requestId": "87e7c05b-4f46-fda24e356e23" }
```

A failed request returns an HTTP error with a `message` and a `type`:

| Type                    | Status             | Meaning                                                          |
| ----------------------- | ------------------ | ---------------------------------------------------------------- |
| `invalid_request_error` | 400, 404, 409, 413 | Malformed request or a missing resource. Fix it before retrying. |
| `authentication_error`  | 401                | The API key is missing, invalid, or for the wrong environment.   |
| `authorization_error`   | 403                | The key is valid but lacks permission.                           |
| `rate_limit_error`      | 429                | A rate limit was exceeded. Back off and retry.                   |
| `api_error`             | 500, 503           | A Courier-side error. Retry with exponential backoff.            |

The most common `400` on `/send` is `Either 'content' or 'template' must be defined`. Send one, not both. Next most common are a missing `to`, or a `list_id`/`audience_id` that does not exist. A `413` means the request exceeded the **6 MB** payload limit. Host attachments externally and pass URLs in `data` rather than base64.

### Limits

<Endpoint method="POST" path="/send" name="Send a message" href="/docs/api-reference/send/send-a-message" /> is **not rate-limited by request count**, so you can send at volume. Its request body caps at **6 MB**, as does <Endpoint method="POST" path="/automations/invoke" name="Invoke an ad hoc Automation" href="/docs/api-reference/automations/invoke-an-ad-hoc-automation" />. Every other endpoint caps at 1 MB.

A few management endpoints are rate-limited per workspace. Their responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests. Exceeding a limit returns `429 Too Many Requests`:

| Endpoint                                                                                                                                                                                | Limit                              |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| <Endpoint method="PUT" path="/lists/{list_id}" name="Update a List" href="/docs/api-reference/lists/update-a-list" />                                                                        | 60 requests / minute               |
| `POST` or <Endpoint method="PUT" path="/users/{user_id}/preferences" name="Replace user Preferences in bulk" href="/docs/api-reference/user-preferences/replace-user-preferences-in-bulk" /> | 50 requests / second               |
| <Endpoint method="POST" path="/messages/{message_id}/resend" name="Resend message" href="/docs/api-reference/messages/resend-message" />                                                     | 1 request / 2 seconds, per message |

Endpoints not listed here are not request-count limited today.

### Send limits

**Send limits** cap how many messages a user, topic, or tenant receives in a window. They are a separate feature from API rate limits. A message blocked by one shows as `THROTTLED` in the log instead of delivering.

The Developer plan's 10,000-sends-per-period cap produces the same `THROTTLED` status. That drop happens after the API has already returned `202`, so there is no synchronous error and the `requestId` looks healthy. A `THROTTLED` message you did not configure a limit for usually means the plan cap.

### Why a message is UNROUTABLE

`UNROUTABLE` is one status with several causes. Open the message in <AppLink href="https://app.courier.com/logs">Logs</AppLink> and read its `reason`:

| `reason`          | Cause                                                                                                                                       | Fix                                                                                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NO_PROVIDERS`    | No provider is configured for the routed channel in this environment, or none of the configured ones is allowed by the send's channel list. | Configure a provider for the channel in <AppLink href="https://app.courier.com/integrations">Integrations</AppLink>, or drop the channel from `routing`. |
| `NO_CHANNELS`     | No channel resolved for the send.                                                                                                           | Add a channel to `routing`, or to the template's routing strategy.                                                                                       |
| `UNPUBLISHED`     | The template has never been published.                                                                                                      | Publish it.                                                                                                                                              |
| `OPT_IN_REQUIRED` | The topic needs an opt-in the recipient has not given.                                                                                      | See <Doc href="/docs/recipients/preferences/overview#status-precedence">preference resolution</Doc>.                                                          |
| `UNSUBSCRIBED`    | The recipient opted out of the topic.                                                                                                       | Expected. Opt-outs are per tenant, so check the tenant context too.                                                                                      |
| `FILTERED`        | A channel or provider condition evaluated false.                                                                                            | Check the `if` conditions on the channel and provider.                                                                                                   |

The timeline then shows one attempt per provider, each with its own `reason` for being skipped:

| Provider `reason`          | Meaning                                                                                                        |
| -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `MISSING_CONFIGURATION`    | The provider is named on the send or template but is not configured in this environment.                       |
| `MISSING_PROVIDER_SUPPORT` | The provider does not serve that channel.                                                                      |
| `INCOMPLETE_PROFILE_DATA`  | The recipient's profile has no address this provider can use (no `email`, no `phone_number`, no device token). |
| `FILTERED_AT_PROVIDER`     | The provider's own `if` condition evaluated false.                                                             |

To audit in bulk, list the failures over the API:

<CodeGroup>
  ```javascript Node.js highlight={1} theme={null}
  const messages = await client.messages.list({ status: ['UNROUTABLE'] });

  for (const message of messages.results) {
    console.log(message.messageId, message.reason);
  }
  ```

  ```python Python highlight={1} theme={null}
  messages = client.messages.list(status=["UNROUTABLE"])

  for message in messages.results:
      print(message.message_id, message.reason)
  ```

  ```bash cURL theme={null}
  curl "https://api.courier.com/messages?status=UNROUTABLE" \
    -H "Authorization: Bearer $COURIER_API_KEY"
  ```

  ```ruby Ruby highlight={1} theme={null}
  messages = courier.messages.list(status: ["UNROUTABLE"])

  messages.results.each do |message|
    puts "#{message.message_id} #{message.reason}"
  end
  ```

  ```go Go highlight={2} theme={null}
  messages, err := client.Messages.List(context.TODO(), courier.MessageListParams{
  	Status: []string{"UNROUTABLE"},
  })
  if err != nil {
  	panic(err)
  }

  for _, message := range messages.Results {
  	fmt.Println(message.MessageID, message.Reason)
  }
  ```

  ```java Java highlight={2} theme={null}
  MessageListResponse messages =
      client.messages().list(MessageListParams.builder().addStatus("UNROUTABLE").build());

  messages.results().forEach(message ->
      System.out.println(message.messageId() + " " + message.reason()));
  ```

  ```php PHP highlight={1} theme={null}
  $messages = $client->messages->list(status: ['UNROUTABLE']);

  foreach ($messages->results as $message) {
    echo $message->messageId . ' ' . $message->reason . PHP_EOL;
  }
  ```

  ```csharp C# highlight={1} theme={null}
  var messages = await client.Messages.List(new() { Status = ["UNROUTABLE"] });

  foreach (var message in messages.Results)
  {
      Console.WriteLine($"{message.MessageID} {message.Reason}");
  }
  ```

  ```bash CLI highlight={3} theme={null}
  courier messages list \
    --api-key "$COURIER_API_KEY" \
    --status UNROUTABLE
  ```

  ```text MCP theme={null}
  With Courier MCP, list my messages with the UNROUTABLE status.
  ```
</CodeGroup>

### Debug other statuses

* **Stuck at `SENT`**: delivery tracking is not configured for that channel, not a failure. See [delivery tracking](#delivery-tracking-is-per-provider).
* **`FILTERED`**: preferences, send conditions, or routing removed the message. Check the recipient's <Doc href="/docs/recipients/preferences/overview">preferences</Doc>.
* **`UNDELIVERABLE`**: the provider rejected it. The timeline shows the provider's error.

For real-time status without polling, receive the `message:updated` <Doc href="/docs/monitor/webhooks/outbound">outbound webhook</Doc>.

### Delivery tracking is per provider

`DELIVERED` only appears when the provider tells Courier the message landed. That reporting is **off until you set it up, per provider**. Until then a healthy send stops at `SENT`. This is the most common reason a working integration looks broken.

A provider reports in one of four ways:

| How it reports           | What you do                                                                                           | Providers                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------ | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Webhook**              | Paste Courier's webhook URL into the provider, and it pushes status in real time.                     | <Doc href="/docs/integrations/email/sendgrid#delivery-tracking">SendGrid</Doc>, <Doc href="/docs/integrations/email/aws-ses#delivery-tracking">AWS SES</Doc>, <Doc href="/docs/integrations/email/mailgun">Mailgun</Doc>, <Doc href="/docs/integrations/email/resend">Resend</Doc>, <Doc href="/docs/integrations/sms/messagemedia">MessageMedia</Doc>                                                                                                                                                |
| **Polling**              | Turn the toggle on in the integration's settings. Courier then asks the provider's API on a schedule. | SendGrid, Mailgun, <Doc href="/docs/integrations/sms/twilio">Twilio</Doc>, <Doc href="/docs/integrations/email/postmark">Postmark</Doc>, <Doc href="/docs/integrations/email/mandrill">Mandrill</Doc>, <Doc href="/docs/integrations/sms/plivo">Plivo</Doc>                                                                                                                                                                                                                                      |
| **Immediate**            | Nothing. The provider acknowledges synchronously, so Courier marks the message `DELIVERED` on accept. | <Doc href="/docs/integrations/direct-message/slack">Slack</Doc>, <Doc href="/docs/integrations/direct-message/microsoft-teams">Microsoft Teams</Doc>, <Doc href="/docs/integrations/direct-message/discord">Discord</Doc>, <Doc href="/docs/integrations/direct-message/stream-chat">Stream Chat</Doc>, <Doc href="/docs/integrations/direct-message/viber">Viber</Doc>, <Doc href="/docs/integrations/other/pagerduty">PagerDuty</Doc>, <Doc href="/docs/integrations/other/webhook-integration">Webhook</Doc> |
| **Reported by your app** | Use a Courier mobile SDK, or `POST` the `trackingUrl` from the payload yourself.                      | <Doc href="/docs/integrations/push/overview#tracking">APNs, FCM, Expo, OneSignal</Doc>                                                                                                                                                                                                                                                                                                                                                                                            |

Providers absent from this table send no delivery confirmation, so their messages stop at `SENT` by design. Where both a webhook and polling are offered, prefer the webhook. It is real time and needs no extra provider permissions.

## Limits & behavior

* **Status is monotonic.** A message never moves backward. Engagement can still follow a terminal delivery status.
* **`/send` is not count-rate-limited.** A few management endpoints are. Send limits govern message volume and produce `THROTTLED`.
* **The `/send` payload cap is 6 MB.** Other endpoints cap at 1 MB. Pass attachment URLs in `data` instead of inlining large files.

## FAQ

<AccordionGroup>
  <Accordion title="What does UNROUTABLE mean?">
    Courier found no valid route: the recipient had no address for the routed channel, or no channel was configured for the send. Check the recipient's profile and the send's routing.
  </Accordion>

  <Accordion title="My message is stuck at SENT. Did it fail?">
    Usually not. `SENT` means Courier handed it to the provider and got no delivery confirmation back. That usually means delivery tracking is not set up for that channel.
  </Accordion>

  <Accordion title="Is the Send API rate-limited?">
    Not by request count. You can send at volume. A few management endpoints are limited (see the table above). Send limits (per user, topic, or tenant) govern message volume and mark blocked messages `THROTTLED`.
  </Accordion>

  <Accordion title="Why did I get a 413?">
    The request exceeded the body-size cap: 6 MB on `/send` and `/automations/invoke`, 1 MB on every other endpoint. Host attachments externally and pass their URLs in `data`. Base64 inflates size by about a third.
  </Accordion>
</AccordionGroup>
