docs/guides/migrate-from-courier-to-novu.mdx
This guide helps you plan and execute a migration from Courier to Novu. It covers how the two platforms compare, what changes when moving to Novu, and a step-by-step approach for migrating your notification workflows, templates, providers, and user data.
Before you dive into the migration steps, here is a quick cheat sheet for translating your Courier setup into Novu's terminology.
| Courier Concept | Novu Equivalent | How they compare |
|---|---|---|
| Automations + Templates | Workflows | Novu combines your message content and delivery logic (like delays or digests) into a single, easy-to-manage Workflow. |
| Profiles / Users | Subscribers | The core entity receiving the notification. Just like Courier, Novu allows you to pass user data inline, meaning you don't have to sync users beforehand. |
| Lists / Audiences | Topics | Used for broadcasting. In Novu, you trigger a workflow to a Topic, and Novu automatically fans it out to all subscribed users. |
| Preferences | Preferences | Novu categorizes user opt-in/opt-out states natively by Workflow or via global channel settings. End user can manage their preference for each workflow or globally. |
| Integrations | Integrations | Both platforms connect to external providers (Twilio, SendGrid, etc.). Novu lets you define multiple active integrations for each channel provider and set a primary integration per environment. |
| Inbox | Inbox | Novu's in-app Inbox comes with pre-built React components for a drop-in experience, plus powerful headless hooks, javascript and react native support if you want to build custom Inbox UI. |
| Tenants | Context | Novu supports multitenancy out of the box, the most simple way to implement tenant separation sending context information while triggering a workflow. Novu will automatically filter the notifications for the tenant and the end user will see only the notifications relevant to their tenant. |
We recommend a phased approach to migrate safely without dropping any messages.
<Steps> <Step title="Phase 1: Set up your workspace and providers"> 1. **Create your workspace:** Sign up for [Novu Cloud](https://dashboard.novu.co/auth/signup). You'll automatically get isolated **Development** and **Production** environments. 2. **Connect your providers:** In the Novu Dashboard, navigate to the **Integrations Store**. Reconnect the ESPs (SendGrid, Postmark), SMS gateways (Twilio), and Push providers (FCM, APNS) you were using in Courier. </Step> <Step title="Phase 2: Migrate audience data"> You need to sync your Courier `Profiles` to Novu `Subscribers`. The easiest way to handle this is a **Lazy Migration**:Instead of exporting and importing thousands of users manually, simply update your backend to pass the user's details (email, phone, name) inline when you trigger a notification. Novu will automatically create or update the Subscriber on the fly.
*If you prefer to sync ahead of time, you can easily script it:*
*If you prefer to sync ahead of time, you can easily script it:*
<Tabs>
<Tab title="Node.js">
```typescript
import { Novu } from '@novu/api';
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });
await novu.subscribers.create({
subscriberId: 'usr_123',
email: '[email protected]',
phone: '+15551234567',
data: { subscriptionTier: 'premium' },
});
```
</Tab>
<Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
novu.subscribers.create(
create_subscriber_request_dto=novu_py.CreateSubscriberRequestDto(
subscriber_id="usr_123",
email="[email protected]",
phone="+15551234567",
data={"subscriptionTier": "premium"},
)
)
```
</Tab>
<Tab title="Go">
```go
import (
"context"
"os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
_, err := s.Subscribers.Create(context.Background(), components.CreateSubscriberRequestDto{
SubscriberID: "usr_123",
Email: "[email protected]",
Phone: "+15551234567",
Data: map[string]any{
"subscriptionTier": "premium",
},
}, nil)
```
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()
->setSecurity('<YOUR_SECRET_KEY_HERE>')
->build();
$sdk->subscribers->create(
createSubscriberRequestDto: new Components\CreateSubscriberRequestDto(
subscriberId: 'usr_123',
email: '[email protected]',
phone: '+15551234567',
data: ['subscriptionTier' => 'premium'],
),
);
```
</Tab>
<Tab title=".NET">
```csharp
using Novu;
using Novu.Models.Components;
using System.Collections.Generic;
var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");
await sdk.Subscribers.CreateAsync(createSubscriberRequestDto: new CreateSubscriberRequestDto() {
SubscriberId = "usr_123",
Email = "[email protected]",
Phone = "+15551234567",
Data = new Dictionary<string, object>() {
{ "subscriptionTier", "premium" },
},
});
```
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder()
.secretKey("<YOUR_SECRET_KEY_HERE>")
.build();
novu.subscribers().create()
.body(CreateSubscriberRequestDto.builder()
.subscriberId("usr_123")
.email("[email protected]")
.phone("+15551234567")
.data(Map.of("subscriptionTier", "premium"))
.build())
.call();
```
</Tab>
<Tab title="cURL">
```bash
curl -X POST 'https://api.novu.co/v2/subscribers' \
-H 'Content-Type: application/json' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-d '{
"subscriberId": "usr_123",
"email": "[email protected]",
"phone": "+15551234567",
"data": { "subscriptionTier": "premium" }
}'
```
</Tab>
</Tabs>
You can also use the [Bulk Import API](/api-reference/subscribers/bulk-create-subscribers) to import your subscribers in bulk.
<Tabs>
<Tab title="Dashboard">
**Option A: The Dashboard Visual Builder**
If your product or marketing team loves managing notifications visually, they will feel right at home in the Novu Dashboard. You can drag and drop Email, SMS, and In-App steps, add Delays and Digests, and edit message content directly in the browser. Read more on how to [manage workflows](/platform/workflow/overview) in the Novu Dashboard.
</Tab>
<Tab title="Code-first">
**Option B: Code-First with Novu Framework**
If your engineering team wants more control, you can use `@novu/framework` to define the same workflows directly in your codebase using TypeScript.
*A simple code-first example:*
```typescript
export const weeklyDigestWorkflow = workflow('weekly-digest', async ({ step }) => {
// Replaces Courier's digest node
const digestResult = await step.digest('batch-events', () => ({ amount: 7, unit: 'days' }));
// Replaces Courier's send node + template
await step.email('send-summary', async (controls) => {
return {
subject: `Your Weekly Activity Summary`,
body: `You have ${digestResult.events.length} new updates this week.`,
};
});
});
```
</Tab>
</Tabs>
```bash
npm install @novu/react
```
```tsx
export default function NotificationCenter() {
return (
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="usr_123"
// We recommend using HMAC encryption in production for security
subscriberHash="HMAC_HASH_GENERATED_ON_BACKEND"
/>
);
}
```
<Tabs>
<Tab title="Node.js">
```typescript
import { Novu } from '@novu/api';
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });
await novu.trigger({
workflowId: 'weekly-digest',
to: {
subscriberId: 'usr_123',
email: '[email protected]',
},
payload: {
activityType: 'login',
},
});
```
</Tab>
<Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
workflow_id="weekly-digest",
to={"subscriber_id": "usr_123", "email": "[email protected]"},
payload={"activityType": "login"},
))
```
</Tab>
<Tab title="Go">
```go
import (
"context"
"os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
_, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{
WorkflowID: "weekly-digest",
To: components.CreateToSubscriberPayloadDto(components.SubscriberPayloadDto{
SubscriberID: "usr_123",
Email: "[email protected]",
}),
Payload: map[string]any{"activityType": "login"},
}, nil)
```
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()
->setSecurity('<YOUR_SECRET_KEY_HERE>')
->build();
$sdk->trigger(
triggerEventRequestDto: new Components\TriggerEventRequestDto(
workflowId: 'weekly-digest',
to: new Components\SubscriberPayloadDto(
subscriberId: 'usr_123',
email: '[email protected]',
),
payload: ['activityType' => 'login'],
),
);
```
</Tab>
<Tab title=".NET">
```csharp
using Novu;
using Novu.Models.Components;
using System.Collections.Generic;
var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");
await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() {
WorkflowId = "weekly-digest",
To = To.CreateSubscriberPayloadDto(new SubscriberPayloadDto() {
SubscriberId = "usr_123",
Email = "[email protected]",
}),
Payload = new Dictionary<string, object>() {
{ "activityType", "login" },
},
});
```
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder()
.secretKey("<YOUR_SECRET_KEY_HERE>")
.build();
novu.trigger()
.body(TriggerEventRequestDto.builder()
.workflowId("weekly-digest")
.to(To2.of(SubscriberPayloadDto.builder()
.subscriberId("usr_123")
.email("[email protected]")
.build()))
.payload(Map.of("activityType", "login"))
.build())
.call();
```
</Tab>
<Tab title="cURL">
```bash
curl -X POST 'https://api.novu.co/v1/events/trigger' \
-H 'Content-Type: application/json' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-d '{
"name": "weekly-digest",
"to": { "subscriberId": "usr_123", "email": "[email protected]" },
"payload": { "activityType": "login" }
}'
```
</Tab>
</Tabs>
**Final Validation:** Send test events using your Novu Development API key. Monitor the **Execution Logs** in the Dashboard to verify everything works flawlessly. Once you're confident, switch to your Production API key and you're fully migrated!
Need to update your backend API requests? Use this mapping to find the Novu equivalent of your Courier endpoints.
| Action | Courier Endpoint | Novu Equivalent |
|---|---|---|
| Trigger a Message | POST /send | POST /v1/events/trigger |
| Send to Many | POST /bulk | POST /v1/events/trigger/bulk |
| Update User | PUT /profiles/:id | PUT /v2/subscribers/:subscriberId |
| Fetch User Data | GET /profiles/:id | GET /v2/subscribers/:subscriberId |
| Modify Opt-Ins | PUT /users/:id/preferences/:topic | PATCH /v2/subscribers/:subscriberId/preferences |
| Manage Lists/Groups | PUT /lists/:id | POST /v2/topics |
| View Logs | GET /messages | GET /v1/notifications |