Prerequisites
- A MagicBell account with a project
- Your MagicBell API key and API secret
Setup
1
Get your MagicBell credentials
In MagicBell, open your project at app.magicbell.com and copy the API key and API secret.
2
Configure in Courier
Open the in Courier, enter your API key and API secret, then click “Save.”
Profile requirements
Device tokens live on the user profile rather than on the send, so one profile can hold tokens for several devices and every push provider reads them from the same place. covers the model. MagicBell addresses the recipient by email address or by external ID. Store either one once with , which merges into the profile and creates it if it does not exist:- Email
- External ID
MagicBell matches the address against the user it holds.
const profile = await client.profiles.create('user_123', {
profile: {
email: 'sarah@acme-corp.com',
},
});
profile = client.profiles.create(
user_id="user_123",
profile={
"email": "sarah@acme-corp.com",
},
)
curl --request POST \
--url https://api.courier.com/profiles/user_123 \
--header "Authorization: Bearer $COURIER_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"profile": {
"email": "sarah@acme-corp.com"
}
}'
profile = courier.profiles.create(
"user_123",
profile: {
email: "sarah@acme-corp.com"
}
)
profile, err := client.Profiles.New(
context.TODO(),
"user_123",
courier.ProfileNewParams{
Profile: map[string]any{
"email": "sarah@acme-corp.com",
},
},
)
ProfileCreateParams params = ProfileCreateParams.builder()
.userId("user_123")
.profile(ProfileCreateParams.Profile.builder()
.putAdditionalProperty("email", JsonValue.from("sarah@acme-corp.com"))
.build())
.build();
ProfileCreateResponse profile = client.profiles().create(params);
$profile = $client->profiles->create('user_123', profile: [
'email' => 'sarah@acme-corp.com',
]);
ProfileCreateParams parameters = new()
{
UserID = "user_123",
Profile = new Dictionary<string, JsonElement>()
{
{ "email", JsonSerializer.SerializeToElement("sarah@acme-corp.com") },
},
};
var profile = await client.Profiles.Create(parameters);
courier profiles create \
--api-key "$COURIER_API_KEY" \
--user-id user_123 \
--profile '{"email":"sarah@acme-corp.com"}'
With Courier MCP, save the email sarah@acme-corp.com on user_123.
The id your own system uses for that person.
const profile = await client.profiles.create('user_123', {
profile: {
magicbell: {
external_id: 'user123',
},
},
});
profile = client.profiles.create(
user_id="user_123",
profile={
"magicbell": {
"external_id": "user123",
},
},
)
curl --request POST \
--url https://api.courier.com/profiles/user_123 \
--header "Authorization: Bearer $COURIER_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"profile": {
"magicbell": {
"external_id": "user123"
}
}
}'
profile = courier.profiles.create(
"user_123",
profile: {
magicbell: {
external_id: "user123"
}
}
)
profile, err := client.Profiles.New(
context.TODO(),
"user_123",
courier.ProfileNewParams{
Profile: map[string]any{
"magicbell": map[string]any{
"external_id": "user123",
},
},
},
)
ProfileCreateParams params = ProfileCreateParams.builder()
.userId("user_123")
.profile(ProfileCreateParams.Profile.builder()
.putAdditionalProperty("magicbell", JsonValue.from(java.util.Map.of(
"external_id", "user123")))
.build())
.build();
ProfileCreateResponse profile = client.profiles().create(params);
$profile = $client->profiles->create('user_123', profile: [
'magicbell' => [
'external_id' => 'user123',
],
]);
ProfileCreateParams parameters = new()
{
UserID = "user_123",
Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"magicbell": {
"external_id": "user123"
}
}
"""
),
};
var profile = await client.Profiles.Create(parameters);
courier profiles create \
--api-key "$COURIER_API_KEY" \
--user-id user_123 \
--profile '{"magicbell":{"external_id":"user123"}}'
With Courier MCP, save the MagicBell external id user123 on user_123.
user_id and Courier resolves the address.
For a one-off with no stored profile, pass it inline instead: "to": { "magicbell": { "external_id": "user123" } }.
Send by user id
The call in every language, and the rest of the profile object.
Send to a recipient
- Send to user id
- Send to an email
Courier reads
email off the saved profile, so preferences apply and the value can change without touching this code.const { requestId } = await courier.send.message({
message: {
to: {
user_id: "user_123",
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
},
});
response = client.send.message(
message={
"to": {
"user_id": "user_123",
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
},
)
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"
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40"
}
}'
response = courier.send_.message(
message: {
to: {
user_id: "user_123"
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40"
}
)
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{
OfUserRecipient: &shared.UserRecipientParam{
UserID: courier.String("user_123"),
},
},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(UserRecipient.builder().userId("user_123").build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'to' => [
'user_id' => 'user_123',
],
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
],
);
SendMessageParams parameters = new()
{
Message = new()
{
To = new UserRecipient { UserID = "user_123" },
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"user_id": "user_123"}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40
With Courier MCP, send my template to user_123 by email.
Pass it inline instead and nothing is stored. Swap this
to object into the call on the other tab.const { requestId } = await courier.send.message({
message: {
to: {
email: "sarah@acme-corp.com",
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
},
});
response = client.send.message(
message={
"to": {
"email": "sarah@acme-corp.com",
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
},
)
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"to": {
"email": "sarah@acme-corp.com"
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40"
}
}'
response = courier.send_.message(
message: {
to: {
email: "sarah@acme-corp.com"
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40"
}
)
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{
OfUserRecipient: &shared.UserRecipientParam{
Email: courier.String("sarah@acme-corp.com"),
},
},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(UserRecipient.builder().email("sarah@acme-corp.com").build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'to' => [
'email' => 'sarah@acme-corp.com',
],
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
],
);
SendMessageParams parameters = new()
{
Message = new()
{
To = new UserRecipient { Email = "sarah@acme-corp.com" },
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"email": "sarah@acme-corp.com"}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40
With Courier MCP, send my template to sarah@acme-corp.com by email.
Overrides
covers the two levels and which one wins. lists the fields every push provider takes. Overrides change the request body Courier sends to MagicBell. This example overrides the MagicBell category and action URL:const { requestId } = await courier.send.message({
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
email: "sarah@acme-corp.com",
},
data: {},
providers: {
magicbell: {
override: {
body: {
notification: {
category: "new_message",
action_url: "https://example.com/example_link",
},
},
},
},
},
},
});
response = client.send.message(
message={
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
"to": {
"email": "sarah@acme-corp.com",
},
"data": {},
"providers": {
"magicbell": {
"override": {
"body": {
"notification": {
"category": "new_message",
"action_url": "https://example.com/example_link",
},
},
},
},
},
},
)
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
"to": {
"email": "sarah@acme-corp.com"
},
"data": {},
"providers": {
"magicbell": {
"override": {
"body": {
"notification": {
"category": "new_message",
"action_url": "https://example.com/example_link"
}
}
}
}
}
}
}'
response = courier.send_.message(
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
email: "sarah@acme-corp.com"
},
data: {},
providers: {
magicbell: {
override: {
body: {
notification: {
category: "new_message",
action_url: "https://example.com/example_link"
}
}
}
}
}
}
)
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{
OfUserRecipient: &shared.UserRecipientParam{
Email: courier.String("sarah@acme-corp.com"),
},
},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
Providers: shared.MessageProvidersParam{
"magicbell": shared.MessageProvidersTypeParam{
Override: map[string]any{
"body": map[string]any{
"notification": map[string]any{
"category": "new_message",
"action_url": "https://example.com/example_link",
},
},
},
},
},
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(UserRecipient.builder().email("sarah@acme-corp.com").build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.providers(MessageProviders.builder()
.putAdditionalProperty("magicbell", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
"body", java.util.Map.of(
"notification", java.util.Map.of(
"category", "new_message",
"action_url", "https://example.com/example_link"
)
)
))))
.build())
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
'to' => [
'email' => 'sarah@acme-corp.com',
],
'data' => [],
'providers' => [
'magicbell' => [
'override' => [
'body' => [
'notification' => [
'category' => 'new_message',
'action_url' => 'https://example.com/example_link',
],
],
],
],
],
],
);
SendMessageParams parameters = new()
{
Message = new()
{
To = new UserRecipient { Email = "sarah@acme-corp.com" },
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
Providers = new Dictionary<string, MessageProvidersType>()
{
{
"magicbell",
new()
{
Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"body": {
"notification": {
"category": "new_message",
"action_url": "https://example.com/example_link"
}
}
}
"""
),
}
},
},
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"email": "sarah@acme-corp.com"}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
--message.providers '{"magicbell": {"override": {"body": {"notification": {"category": "new_message", "action_url": "https://example.com/example_link"}}}}}'
With Courier MCP, send my template to sarah@acme-corp.com with a MagicBell category and action URL.
Provider details
magicbell
routing.channels instead is supported, and sends through just this provider.
Send to a specific provider
When that is worth doing, and what you give up: failover, channel priority, and providers you add later.