Courier
Send transactional notifications including email with Courier. Use this skill when
You are an expert in Courier for transactional and marketing email. You understand
Courier's notification orchestration layer, the Send API, visual template designer,
multi-provider routing, user preference management, automation workflows, and how
Courier abstracts over underlying email providers like SendGrid, Postmark, and SES.
## Key Points
- **Use templates over inline content.** Courier's visual template designer supports
- **Configure provider failover.** Set at least two email providers with routing
- **Define notification topics for preference management.** Map each notification type
- **Use user_id everywhere, not raw email.** Store user profiles in Courier and
- **Track delivery with requestId.** Every send returns a requestId. Use it to poll
- **Use bulk jobs for large sends.** The bulk API handles rate limiting and queuing
- **Not creating user profiles before sending.** If you send to a user_id that has
- **Confusing routing method "all" vs "single".** "all" sends to every listed channel.
- **Hardcoding provider-specific features.** Courier abstracts providers. If you use
- **Ignoring the Courier logs page.** The Courier dashboard shows a full timeline for
- **Not setting required on transactional topics.** If a transactional topic (like
## Quick Example
```bash
npm install @trycourier/courier
```
```typescript
import { CourierClient } from "@trycourier/courier";
const courier = new CourierClient({
authorizationToken: process.env.COURIER_AUTH_TOKEN!,
});
```skilldb get email-services-skills/CourierFull skill: 318 linesCourier — Email Service
You are an expert in Courier for transactional and marketing email. You understand Courier's notification orchestration layer, the Send API, visual template designer, multi-provider routing, user preference management, automation workflows, and how Courier abstracts over underlying email providers like SendGrid, Postmark, and SES.
Core Philosophy
Courier exists to abstract the notification layer so your application never depends on a single email provider. The whole point of adding Courier is provider failover: if SendGrid goes down, Courier automatically retries through SES or Postmark without any code changes. If you are using Courier with only one provider configured and no fallback, you are paying for an abstraction layer without getting its primary benefit.
Notifications should be addressed to users, not to email addresses. Store user profiles in Courier and reference users by ID in every send call. This decouples your notification logic from contact details and unlocks multi-channel delivery -- a single send call can reach a user via email, push, SMS, and in-app based on their preferences and available channels. Addressing users by raw email forces you back into single-channel thinking.
Preference management is not a nice-to-have -- it is a compliance requirement and a user experience fundamental. Map every notification type to a Courier topic, mark transactional topics (password resets, security alerts) as required, and let users opt out of everything else through the preference center. This keeps you CAN-SPAM and GDPR compliant while giving users control over their notification experience.
Anti-Patterns
- Using Courier with a single provider and no failover -- The primary value of Courier is provider redundancy. Without at least two providers configured, you have added complexity without gaining resilience.
- Sending to user_ids without creating profiles first -- If the user has no profile in Courier (no email address), the message silently fails with no error. Always upsert the user profile before the first send.
- Using routing method "all" when you mean "single" -- "All" sends to every listed channel simultaneously, causing duplicate notifications across email, push, and SMS. Use "single" to send to the first available channel only.
- Building provider-specific logic into your integration -- Using SendGrid-specific template features or Postmark message streams directly defeats the provider abstraction. Keep all provider-specific configuration in the Courier dashboard.
- Not marking transactional topics as required -- If a transactional topic (password reset, payment confirmation) is not marked required, users can opt out via the preference center, breaking critical application functionality.
Overview
Courier is a notification infrastructure platform that sits above email providers. Instead of integrating directly with SendGrid or SES, you integrate once with Courier and configure providers in the dashboard. Courier provides a visual template designer, routing logic (send via provider A, fall back to provider B), user notification preferences, multi-channel delivery (email, push, SMS, Slack, in-app), and automations. This makes it ideal for applications that need reliable email delivery with provider failover and user-controlled preferences.
Setup & Configuration
Installation
npm install @trycourier/courier
Client setup
import { CourierClient } from "@trycourier/courier";
const courier = new CourierClient({
authorizationToken: process.env.COURIER_AUTH_TOKEN!,
});
Environment variables
COURIER_AUTH_TOKEN=your-auth-token
COURIER_INBOUND_WEBHOOK_SECRET=your-webhook-secret
Provider configuration
Configure email providers in the Courier dashboard under Channels > Email. Add your SendGrid, Postmark, SES, or other provider credentials. Set routing priority so Courier fails over automatically if the primary provider is down.
Core Patterns
Send a notification by template
async function sendNotification(
userId: string,
templateId: string,
data: Record<string, unknown>
) {
const { requestId } = await courier.send({
message: {
to: { user_id: userId },
template: templateId,
data,
},
});
return requestId; // track delivery status with this ID
}
// Usage
await sendNotification("user_123", "WELCOME_EMAIL", {
firstName: "Alice",
loginUrl: "https://app.example.com/login",
});
Send with inline content (no template)
async function sendInlineEmail(
email: string,
subject: string,
body: string
) {
const { requestId } = await courier.send({
message: {
to: { email },
content: {
title: subject,
body,
},
routing: {
method: "single",
channels: ["email"],
},
},
});
return requestId;
}
Send with provider failover
async function sendWithFailover(
userId: string,
templateId: string,
data: Record<string, unknown>
) {
const { requestId } = await courier.send({
message: {
to: { user_id: userId },
template: templateId,
data,
routing: {
method: "single",
channels: ["email"],
providers: ["sendgrid", "ses"], // try SendGrid first, fall back to SES
},
},
});
return requestId;
}
Multi-channel delivery (email + push + in-app)
async function sendMultiChannel(
userId: string,
templateId: string,
data: Record<string, unknown>
) {
return courier.send({
message: {
to: { user_id: userId },
template: templateId,
data,
routing: {
method: "all", // send to all channels
channels: ["email", "push", "inbox"],
},
},
});
}
Manage user profiles and preferences
// Create or update a user profile
async function upsertUser(
userId: string,
profile: { email: string; name?: string; phone?: string }
) {
await courier.users.put(userId, {
profile: {
email: profile.email,
name: profile.name,
phone_number: profile.phone,
},
});
}
// User preference management — let users control what they receive
async function getUserPreferences(userId: string) {
return courier.users.preferences.list(userId);
}
async function updateUserPreference(
userId: string,
topicId: string,
status: "OPTED_IN" | "OPTED_OUT"
) {
await courier.users.preferences.put(userId, topicId, { status });
}
Bulk send to many recipients
async function sendBulk(
userIds: string[],
templateId: string,
data: Record<string, unknown>
) {
const { jobId } = await courier.bulk.createJob({
message: {
template: templateId,
data,
},
});
// Add users in batches
const batchSize = 1000;
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize);
await courier.bulk.ingestUsers(jobId, {
users: batch.map((id) => ({ user_id: id })),
});
}
await courier.bulk.runJob(jobId);
return jobId;
}
Track delivery status
async function getDeliveryStatus(requestId: string) {
const tracking = await courier.messages.get(requestId);
return {
status: tracking.status, // SENT, DELIVERED, OPENED, CLICKED, UNDELIVERABLE
provider: tracking.providers?.[0]?.provider,
channel: tracking.providers?.[0]?.channel,
events: tracking.events, // full event timeline
};
}
Automations (drip sequences)
async function triggerAutomation(
automationTemplateId: string,
userId: string,
data: Record<string, unknown>
) {
await courier.automations.invokeAutomationTemplate({
templateId: automationTemplateId,
data,
recipient: userId,
});
}
// Ad-hoc automation (delay then send)
await courier.automations.invokeAdHocAutomation({
automation: {
steps: [
{ action: "delay", duration: "1 day" },
{
action: "send",
message: {
to: { user_id: "user_123" },
template: "FOLLOW_UP_EMAIL",
data: { firstName: "Alice" },
},
},
],
},
});
Best Practices
- Use templates over inline content. Courier's visual template designer supports brand consistency, localization, and non-developer editing. Reserve inline content for simple, developer-only messages.
- Configure provider failover. Set at least two email providers with routing priority. If your primary provider has an outage, Courier automatically retries through the fallback. This is the main reason to use Courier.
- Define notification topics for preference management. Map each notification type to a Courier topic. Users can then opt out of specific topics without losing critical transactional messages. Mark transactional topics as required so users cannot disable them.
- Use user_id everywhere, not raw email. Store user profiles in Courier and reference users by ID. This decouples notification logic from contact details and supports multi-channel delivery from a single send call.
- Track delivery with requestId. Every send returns a requestId. Use it to poll delivery status or display delivery confirmations in your UI. Log it for debugging.
- Use bulk jobs for large sends. The bulk API handles rate limiting and queuing internally. Looping individual send calls for thousands of recipients is slower and risks rate limit errors.
Common Pitfalls
- Not creating user profiles before sending. If you send to a user_id that has no profile in Courier, the message silently fails. Always upsert the user profile (with email) before the first send.
- Confusing routing method "all" vs "single". "all" sends to every listed channel. "single" sends to the first available channel only. Using "all" when you meant "single" causes duplicate notifications across email, push, and SMS.
- Hardcoding provider-specific features. Courier abstracts providers. If you use SendGrid-specific template features or Postmark message streams directly, you lose the ability to fail over. Keep provider-specific logic out of your integration.
- Ignoring the Courier logs page. The Courier dashboard shows a full timeline for every message: which provider was used, delivery status, user preferences applied. Debug delivery issues there before digging into provider dashboards.
- Not setting required on transactional topics. If a transactional topic (like password reset) is not marked required, users can opt out of it through the preference center, breaking critical functionality.
Install this skill directly: skilldb add email-services-skills
Related Skills
AWS Ses
Send email at scale with Amazon SES (Simple Email Service). Use this skill when
Brevo
Send transactional and marketing email with Brevo (formerly Sendinblue). Use this
Customerio
Send transactional and marketing email with Customer.io. Use this skill when the
Email Deliverability
Optimize email deliverability across any provider. Use this skill when the project
Loops
Send transactional and marketing email with Loops. Use this skill when the project
Mailchimp Transactional
Send transactional email with Mailchimp Transactional (formerly Mandrill). Use