aws-sns.
Amazon SNS also works as an SMS provider. See .
Prerequisites
- An AWS account with an IAM user for SNS
- Your Access Key ID and Secret Access Key
- The AWS region for your SNS topics
Setup
1
Create an AWS access key
In AWS, create an IAM user with SNS publish permission and copy its access key ID and secret access key.
2
Configure in Courier
Open the in Courier, enter your Access Key ID and Secret Access Key, choose your region, then click “Complete.”
Leave Topic ARN empty to push to individual devices. A saved Topic ARN outranks the Target ARN on a recipient’s profile, so every push goes to the topic instead. See Destination precedence.
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. SNS addresses the device by the Target ARN it is subscribed to, nested underaws_sns on the profile. Store it once with , which merges into the profile and creates it if it does not exist:
const profile = await client.profiles.create('user_123', {
profile: {
aws_sns: {
target_arn: 'your:target:arn',
},
},
});
profile = client.profiles.create(
user_id="user_123",
profile={
"aws_sns": {
"target_arn": "your:target:arn",
},
},
)
curl --request POST \
--url https://api.courier.com/profiles/user_123 \
--header "Authorization: Bearer $COURIER_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"profile": {
"aws_sns": {
"target_arn": "your:target:arn"
}
}
}'
profile = courier.profiles.create(
"user_123",
profile: {
aws_sns: {
target_arn: "your:target:arn"
}
}
)
profile, err := client.Profiles.New(
context.TODO(),
"user_123",
courier.ProfileNewParams{
Profile: map[string]any{
"aws_sns": map[string]any{
"target_arn": "your:target:arn",
},
},
},
)
ProfileCreateParams params = ProfileCreateParams.builder()
.userId("user_123")
.profile(ProfileCreateParams.Profile.builder()
.putAdditionalProperty("aws_sns", JsonValue.from(java.util.Map.of(
"target_arn", "your:target:arn")))
.build())
.build();
ProfileCreateResponse profile = client.profiles().create(params);
$profile = $client->profiles->create('user_123', profile: [
'aws_sns' => [
'target_arn' => 'your:target:arn',
],
]);
ProfileCreateParams parameters = new()
{
UserID = "user_123",
Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"aws_sns": {
"target_arn": "your:target:arn"
}
}
"""
),
};
var profile = await client.Profiles.Create(parameters);
courier profiles create \
--api-key "$COURIER_API_KEY" \
--user-id user_123 \
--profile '{"aws_sns":{"target_arn":"your:target:arn"}}'
With Courier MCP, save the SNS target ARN your:target:arn on user_123.
config override. It is not read from the recipient profile.
Destination precedence
Courier picks exactly one SNS destination per message, in this order:phone_numberon the profile, which sends an SMS rather than a push- Topic ARN, from the integration config or a
configoverride aws_sns.target_arnon the profile
user_id and Courier resolves the address.
For a one-off with no stored profile, pass it inline instead: "to": { "aws_sns": { "target_arn": "your:target:arn" } }.
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
aws_sns
Courier reads
aws_sns 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: {
// A provider recipient is a profile field, so it sits outside the typed union.
to: {
aws_sns: {
target_arn: "your:target:arn",
},
} as any,
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
},
});
response = client.send.message(
message={
"to": {
"aws_sns": {
"target_arn": "your:target:arn",
},
},
"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": {
"aws_sns": {
"target_arn": "your:target:arn"
}
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40"
}
}'
response = courier.send_.message(
message: {
to: {
aws_sns: {
target_arn: "your:target:arn"
}
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40"
}
)
// A provider recipient is a profile field, so it sits outside the typed union.
to := param.Override[shared.UserRecipientParam](
json.RawMessage(`{"aws_sns": {"target_arn": "your:target:arn"}}`),
)
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
},
})
// A provider recipient is a profile field, so it sits outside the typed union.
UserRecipient to = UserRecipient.builder()
.putAdditionalProperty("aws_sns", JsonValue.from(Map.of(
"target_arn", "your:target:arn")))
.build();
client.send().message(SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(to)
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.build())
.build());
$response = $client->send->message(
message: [
'to' => [
'aws_sns' => [
'target_arn' => 'your:target:arn',
],
],
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
],
);
// A provider recipient is a profile field, so it sits outside the typed union.
UserRecipient to = UserRecipient.FromRawUnchecked(
JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""{"aws_sns": {"target_arn": "your:target:arn"}}"""
)
);
var response = await client.Send.Message(new() { Message = new() { To = to, Template = "nt_01kx4h2jdafq8bk9aftxak4b40" } });
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"aws_sns": {"target_arn": "your:target:arn"}}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40
With Courier MCP, send my template to the SNS target arn your:target:arn.
Overrides
covers the two levels and which one wins. lists the fields every push provider takes. Theconfig override swaps credentials, region, or the Topic ARN at send time:
| Field | Effect |
|---|---|
accessKeyId | Replaces the saved Access Key ID |
secretAccessKey | Replaces the saved Secret Access Key |
region | Replaces the saved region. Defaults to us-east-1 if neither is set |
topicArn | Replaces the saved Topic ARN, and still outranks a profile’s target_arn |
const { requestId } = await courier.send.message({
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
aws_sns: {
target_arn: "your:target:arn",
},
},
providers: {
"aws-sns": {
override: {
config: {
accessKeyId: "RUNTIME_ACCESS_KEY_ID",
secretAccessKey: "RUNTIME_SECRET_ACCESS_KEY",
region: "eu-west-1",
},
},
},
},
},
});
response = client.send.message(
message={
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
"to": {
"aws_sns": {
"target_arn": "your:target:arn",
},
},
"providers": {
"aws-sns": {
"override": {
"config": {
"accessKeyId": "RUNTIME_ACCESS_KEY_ID",
"secretAccessKey": "RUNTIME_SECRET_ACCESS_KEY",
"region": "eu-west-1",
},
},
},
},
},
)
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": {
"aws_sns": {
"target_arn": "your:target:arn"
}
},
"providers": {
"aws-sns": {
"override": {
"config": {
"accessKeyId": "RUNTIME_ACCESS_KEY_ID",
"secretAccessKey": "RUNTIME_SECRET_ACCESS_KEY",
"region": "eu-west-1"
}
}
}
}
}
}'
response = courier.send_.message(
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
aws_sns: {
target_arn: "your:target:arn"
}
},
providers: {
"aws-sns": {
override: {
config: {
accessKeyId: "RUNTIME_ACCESS_KEY_ID",
secretAccessKey: "RUNTIME_SECRET_ACCESS_KEY",
region: "eu-west-1"
}
}
}
}
}
)
// This provider addresses the recipient with fields outside the typed
// UserRecipient model, so pass the recipient as raw JSON.
to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
"aws_sns": {
"target_arn": "your:target:arn"
}
}`))
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
Providers: shared.MessageProvidersParam{
"aws-sns": shared.MessageProvidersTypeParam{
Override: map[string]any{
"config": map[string]any{
"accessKeyId": "RUNTIME_ACCESS_KEY_ID",
"secretAccessKey": "RUNTIME_SECRET_ACCESS_KEY",
"region": "eu-west-1",
},
},
},
},
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
// This provider addresses the recipient with fields outside the
// typed UserRecipient model, so pass them as additional properties.
.to(UserRecipient.builder()
.putAdditionalProperty("aws_sns", JsonValue.from(java.util.Map.of(
"target_arn", "your:target:arn"
)))
.build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.providers(MessageProviders.builder()
.putAdditionalProperty("aws-sns", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
"config", java.util.Map.of(
"accessKeyId", "RUNTIME_ACCESS_KEY_ID",
"secretAccessKey", "RUNTIME_SECRET_ACCESS_KEY",
"region", "eu-west-1"
)
))))
.build())
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
'to' => [
'aws_sns' => [
'target_arn' => 'your:target:arn',
],
],
'providers' => [
'aws-sns' => [
'override' => [
'config' => [
'accessKeyId' => 'RUNTIME_ACCESS_KEY_ID',
'secretAccessKey' => 'RUNTIME_SECRET_ACCESS_KEY',
'region' => 'eu-west-1',
],
],
],
],
],
);
SendMessageParams parameters = new()
{
Message = new()
{
// This provider addresses the recipient with fields outside the
// typed UserRecipient model, so build it from raw JSON.
To = UserRecipient.FromRawUnchecked(
JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"aws_sns": {
"target_arn": "your:target:arn"
}
}
"""
)
),
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
Providers = new Dictionary<string, MessageProvidersType>()
{
{
"aws-sns",
new()
{
Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"config": {
"accessKeyId": "RUNTIME_ACCESS_KEY_ID",
"secretAccessKey": "RUNTIME_SECRET_ACCESS_KEY",
"region": "eu-west-1"
}
}
"""
),
}
},
},
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"aws_sns": {"target_arn": "your:target:arn"}}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
--message.providers '{"aws-sns": {"override": {"config": {"accessKeyId": "RUNTIME_ACCESS_KEY_ID", "secretAccessKey": "RUNTIME_SECRET_ACCESS_KEY", "region": "eu-west-1"}}}}'
With Courier MCP, send my template to this SNS target ARN in a different region.
body override merges into the SNS Publish request itself, so its fields use the AWS parameter names rather than Courier’s. See the SNS publish properties.
const { requestId } = await courier.send.message({
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
aws_sns: {
target_arn: "your:target:arn",
},
},
providers: {
"aws-sns": {
override: {
body: {
Subject: "Order update",
MessageStructure: "json",
},
},
},
},
},
});
response = client.send.message(
message={
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
"to": {
"aws_sns": {
"target_arn": "your:target:arn",
},
},
"providers": {
"aws-sns": {
"override": {
"body": {
"Subject": "Order update",
"MessageStructure": "json",
},
},
},
},
},
)
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": {
"aws_sns": {
"target_arn": "your:target:arn"
}
},
"providers": {
"aws-sns": {
"override": {
"body": {
"Subject": "Order update",
"MessageStructure": "json"
}
}
}
}
}
}'
response = courier.send_.message(
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
aws_sns: {
target_arn: "your:target:arn"
}
},
providers: {
"aws-sns": {
override: {
body: {
Subject: "Order update",
MessageStructure: "json"
}
}
}
}
}
)
// This provider addresses the recipient with fields outside the typed
// UserRecipient model, so pass the recipient as raw JSON.
to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
"aws_sns": {
"target_arn": "your:target:arn"
}
}`))
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
Providers: shared.MessageProvidersParam{
"aws-sns": shared.MessageProvidersTypeParam{
Override: map[string]any{
"body": map[string]any{
"Subject": "Order update",
"MessageStructure": "json",
},
},
},
},
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
// This provider addresses the recipient with fields outside the
// typed UserRecipient model, so pass them as additional properties.
.to(UserRecipient.builder()
.putAdditionalProperty("aws_sns", JsonValue.from(java.util.Map.of(
"target_arn", "your:target:arn"
)))
.build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.providers(MessageProviders.builder()
.putAdditionalProperty("aws-sns", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
"body", java.util.Map.of(
"Subject", "Order update",
"MessageStructure", "json"
)
))))
.build())
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
'to' => [
'aws_sns' => [
'target_arn' => 'your:target:arn',
],
],
'providers' => [
'aws-sns' => [
'override' => [
'body' => [
'Subject' => 'Order update',
'MessageStructure' => 'json',
],
],
],
],
],
);
SendMessageParams parameters = new()
{
Message = new()
{
// This provider addresses the recipient with fields outside the
// typed UserRecipient model, so build it from raw JSON.
To = UserRecipient.FromRawUnchecked(
JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"aws_sns": {
"target_arn": "your:target:arn"
}
}
"""
)
),
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
Providers = new Dictionary<string, MessageProvidersType>()
{
{
"aws-sns",
new()
{
Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"body": {
"Subject": "Order update",
"MessageStructure": "json"
}
}
"""
),
}
},
},
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"aws_sns": {"target_arn": "your:target:arn"}}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
--message.providers '{"aws-sns": {"override": {"body": {"Subject": "Order update", "MessageStructure": "json"}}}}'
With Courier MCP, send my template to this SNS target ARN with the publish parameters overridden.
Provider details
aws-sns
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.