Skip to main content
This page is written for AI coding agents. It gives you everything you need to send email and in-app inbox notifications with Courier without any prior context. Read it top to bottom before writing any code.

What Courier is

Courier is infrastructure that powers product-to-user communication. It provides a unified system for designing, sending, and managing notifications across email, SMS, push, chat, and in-app channels. Your app triggers an event, Courier determines the right channels and providers, renders the message, enforces user preferences, and delivers it in real time. One API, every channel.

What you need before writing any code

  1. A Courier accountsign up here if the user hasn’t already
  2. An API key — find it at Settings > API Keys
  3. A provider connected (optional for first test) — Courier’s built-in email provider and Courier Inbox work with no setup in Test mode. Additional providers (SendGrid, Twilio, etc.) can be connected later at Integrations
Set the API key as an environment variable. All examples below assume this:
export COURIER_API_KEY="your-api-key-here"

Core concepts

Environments

Every workspace has two environments: Test and Production. They use different API keys. The key from Settings > API Keys defaults to Test. Sending to your own email from Test mode is fine for validation; do not send to end users from Test.

Recipients

The to field accepts five alternative shapes. Use whichever the user’s system already has:
  • { "user_id": "user-123" } — looks up a stored profile for contact details (preferred)
  • { "email": "alice@example.com" } — ad-hoc email, no profile needed
  • { "phone_number": "+14155550123" } — ad-hoc SMS (E.164 format required)
  • { "list_id": "weekly-digest" } — sends to all list subscribers
  • { "audience_id": "churned-users" } — sends to a dynamic audience

Routing

The routing object controls which channels Courier tries:
{ "method": "all", "channels": ["email", "inbox"] }
  • "all" — delivers to every channel in the list simultaneously
  • "single" — tries channels left to right, stops at first success (fallback chain)
If you omit routing, Courier uses the notification template’s configured routing.

Inline content vs templates

Two ways to provide message content: Inline — content is in the API call itself. Good for dynamic or one-off messages:
"content": { "title": "Your order shipped", "body": "Order #{{orderId}} is on its way." }
Template — content lives in the Courier dashboard, referenced by ID. Good for messages that need consistent branding or visual design:
"template": "order-shipped"
Both work the same way at the API level. Templates give non-engineers control over copy and design without code changes.

Profiles

A profile is a JSON object stored per user_id that holds contact info and attributes:
{
  "email": "alice@example.com",
  "phone_number": "+14155550123",
  "name": "Alice"
}
Courier merges send-time data with stored profiles. If you send to user_id: "user-123" and that user has a stored email, Courier uses it automatically.
Use POST /profiles/{user_id} (merge) to create or update profiles. PUT /profiles/{user_id} replaces the entire profile; any field not included in the PUT body is deleted.

Idempotency

For transactional notifications (OTPs, order confirmations, billing alerts), always pass an Idempotency-Key header. If the same key is sent twice, Courier returns the stored response without re-sending. This includes error responses; replaying a key that failed does not retry with a corrected payload.
-H "Idempotency-Key: order-shipped-order-456-user-123"

Step 1: Send your first message

This sends an email via the Courier Send API. In Test mode, Courier’s built-in email provider is available with no integration setup, but delivery depends on a valid recipient address and workspace email configuration (from address, brand).
Use your own email address, not test@example.com. Reserved domain addresses are blocked by email providers and will result in an UNDELIVERABLE status.
curl -X POST https://api.courier.com/send \
  -H "Authorization: Bearer $COURIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "to": { "email": "you@yourdomain.com" },
      "content": {
        "title": "Hello from Courier",
        "body": "Your first notification."
      },
      "routing": {
        "method": "single",
        "channels": ["email"]
      }
    }
  }'
A 200 response with a requestId means Courier accepted the job; it does not mean the message was delivered. Use GET /messages/{requestId} or Message Logs to confirm per-provider delivery status.
{ "requestId": "1-abc123-def456" }

Step 2: Store a user profile

Store contact information so you can send to user_id without repeating details in every call:
curl -X POST https://api.courier.com/profiles/user-123 \
  -H "Authorization: Bearer $COURIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "profile": { "email": "alice@example.com", "phone_number": "+14155550123", "name": "Alice" } }'
Then send to that user by ID: "to": { "user_id": "user-123" }. Courier looks up the stored profile and routes automatically. In SDKs, use client.profiles.create(userId, { profile: {...} }).

Step 3: Send with a template, data, and idempotency

For real transactional sends, use a template ID instead of inline content, pass dynamic data, and include an idempotency key. Replace "order-shipped" with the ID of a template you’ve created in the Courier dashboard. If the template doesn’t exist, the API returns {"status":"UNMAPPED"} with no error message.
{
  "message": {
    "to": { "user_id": "user-123" },
    "template": "order-shipped",
    "data": { "orderId": "ORD-456", "trackingUrl": "https://track.example.com/ORD-456" },
    "routing": { "method": "all", "channels": ["email", "inbox"] }
  }
}
Add the header -H "Idempotency-Key: order-shipped-ORD-456-user-123". In Node.js, pass { idempotencyKey: "..." } as the second argument to client.send.message(). In Python, use extra_headers={"Idempotency-Key": "..."}.

Step 4: Send to Inbox (in-app notifications)

Courier Inbox is an in-app notification feed. Messages sent to the inbox channel appear in a real-time UI component embedded in the user’s app. No external provider is needed; the Courier Inbox provider is built in.

Send a message to Inbox

Target the inbox channel in routing. Inbox requires user_id (not ad-hoc email), because the message is tied to a user’s feed:
curl -X POST https://api.courier.com/send \
  -H "Authorization: Bearer $COURIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "to": { "user_id": "user-123" },
      "content": {
        "title": "New activity",
        "body": "Your report is ready to download."
      },
      "routing": {
        "method": "all",
        "channels": ["email", "inbox"]
      }
    }
  }'
This delivers to both email and inbox simultaneously. The user sees the message in their inbox feed and receives an email.

Authenticate users for Inbox

Inbox SDKs use JWTs to authenticate. Your backend issues a token per user via the Courier auth endpoint:
curl -X POST https://api.courier.com/auth/issue-token \
  -H "Authorization: Bearer $COURIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "user_id:user-123 inbox:read:messages inbox:write:events",
    "expires_in": "1 day"
  }'
The response contains a token field. Pass this JWT to the client-side SDK.

Embed the Inbox UI

Install a Courier Inbox SDK and render the component. The React example:
import { useCourier, CourierInbox } from "@trycourier/courier-react";

function NotificationCenter() {
  const courier = useCourier();

  useEffect(() => {
    courier.shared.signIn({ userId: "user-123", jwt: jwtFromBackend });
  }, []);

  return <CourierInbox />;
}
Inbox SDKs are available for React, Web Components, React Native, iOS, Android, and Flutter. See Inbox overview for full setup.

Step 5: Debug a delivery

Check why a message was or wasn’t delivered:
# Get delivery status
courier messages retrieve --message-id "1-abc123"

# Get full event timeline (enqueued → routed → rendered → sent → delivered)
courier messages history --message-id "1-abc123"
Common delivery failure causes:
  • No provider configured for the channel; add one at Integrations
  • Missing contact info; the user profile has no email/phone for the channel being attempted
  • Wrong environment; Test API key used with production user data, or vice versa
  • Provider credentials invalid; check the integration config in the dashboard

What NOT to do

  • Don’t use PUT /profiles/{user_id} unless you intend to replace the entire profile. Any field not included in the PUT body is deleted. Use POST /profiles/{user_id} (merge) for updates.
  • Don’t omit idempotency keys on transactional sends. Retries without idempotency keys cause duplicate notifications.
  • Don’t use method: "all" for transactional notifications (OTP, password reset, billing). Use "single" with fallback channels.
  • Don’t send to user_id without a stored profile if the channel requires contact info. Courier won’t know where to deliver.
  • Don’t commit API keys to source code. Use environment variables or a secrets manager.

Key API endpoints and SDKs

Install: npm install @trycourier/courier (Node), pip install trycourier (Python), go get github.com/trycourier/courier-go/v4 (Go), npm install -g @trycourier/cli (CLI)
OperationMethodPath
Send a messagePOST/send
Get message statusGET/messages/{message_id}
Get message historyGET/messages/{message_id}/history
Create/merge a profilePOST/profiles/{user_id}
Replace a profilePUT/profiles/{user_id}
Get a profileGET/profiles/{user_id}
Issue a JWT (for Inbox auth)POST/auth/issue-token
Get inbox messagesGET/inbox/messages
Subscribe user to listPUT/lists/{list_id}/subscriptions/{user_id}
List notification templatesGET/notifications
Cancel a pending messagePOST/messages/{message_id}/cancel
Full reference: API Reference
For MCP server, CLI, and Skills setup, see Build with AI.

What’s Next

Inbox Overview

Full setup guide for in-app notifications across web and mobile.

MCP Server

Structured tool access for AI agents in Cursor, Claude Code, and Windsurf.

Courier CLI

All API endpoints from the command line.

API Reference

Full REST API documentation.