> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Push providers

> Every push provider Courier supports, how device tokens are stored, and payload overrides.

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

## Available providers

<CardGroup cols={3}>
  <Card title="Apple APNs" href="/docs/integrations/push/apple-push-notification" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-apple-push-notification.webp?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=b8fa5199aa5c51740cad4cc88223459f" horizontal width="216" height="216" data-path="assets/provider-apple-push-notification.webp" />

  <Card title="Firebase FCM" href="/docs/integrations/push/firebase-fcm" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-firebase-fcm.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=eb05e81963787b1862f486a89e43b3f2" horizontal width="24" height="24" data-path="assets/provider-firebase-fcm.svg" />

  <Card title="Expo" href="/docs/integrations/push/expo" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-expo.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=0431e5b8ce68ab5edb48a22ec78771b9" horizontal width="24" height="24" data-path="assets/provider-expo.svg" />

  <Card title="OneSignal" href="/docs/integrations/push/onesignal-push" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-onesignal-push.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=75d2612dd02cbc937df4683a4622709a" horizontal width="24" height="24" data-path="assets/provider-onesignal-push.svg" />

  <Card title="Airship" href="/docs/integrations/push/airship" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-airship.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=936a485045cb571fd2a64f79faaa53ef" horizontal width="24" height="24" data-path="assets/provider-airship.svg" />

  <Card title="Amazon SNS" href="/docs/integrations/push/aws-sns" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-aws-sns.webp?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=64b6a140d65c06245d57828f233b3789" horizontal width="216" height="216" data-path="assets/provider-aws-sns.webp" />

  <Card title="Pusher Beams" href="/docs/integrations/push/pusher-beams" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-pusher-beams.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=72c516da6af26ac5d9e21790bc96b7a3" horizontal width="24" height="24" data-path="assets/provider-pusher-beams.svg" />

  <Card title="Pusher" href="/docs/integrations/push/pusher" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-pusher.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=9aea54d60b5e6f62e567de03ac51f915" horizontal width="24" height="24" data-path="assets/provider-pusher.svg" />

  <Card title="MagicBell" href="/docs/integrations/push/magicbell" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-magicbell.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=4753005cfd5fbea72f03687216f35ab0" horizontal width="24" height="24" data-path="assets/provider-magicbell.svg" />

  <Card title="Pushbullet" href="/docs/integrations/push/pushbullet" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-pushbullet.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=46630dc4c687073fbbe4151d82501096" horizontal width="24" height="24" data-path="assets/provider-pushbullet.svg" />

  <Card title="Beamer" href="/docs/integrations/push/beamer" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-beamer.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=78bc63c8943a68dc52ca4c9b6534502b" horizontal width="24" height="24" data-path="assets/provider-beamer.svg" />

  <Card title="NowPush" href="/docs/integrations/push/nowpush" icon="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/provider-nowpush.svg?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=6d83dd41bebe7c86e340a9b1ee96da98" horizontal width="24" height="24" data-path="assets/provider-nowpush.svg" />
</CardGroup>

Amazon SNS also sends <Doc href="/docs/integrations/sms/aws-sns">SMS</Doc>. Apple APNs and Firebase FCM are the two the <Doc href="/docs/sdk-libraries/sdks-overview">Courier Mobile SDKs</Doc> register device tokens for.

<Tip>
  Can't find a provider? Start a chat on the Courier site, or email [support@courier.com](mailto:support@courier.com)
</Tip>

## Channel overrides

<Doc href="/docs/send/overrides#how-overrides-work">How overrides work</Doc> covers the two levels and which one wins. These are the fields a push channel override accepts, whichever provider sends.

| Field   | Sets                                                       |
| :------ | :--------------------------------------------------------- |
| `title` | The notification title                                     |
| `body`  | The notification body                                      |
| `data`  | Custom key-value data delivered alongside the notification |
| `icon`  | The notification icon                                      |

<CodeGroup>
  ```javascript Node.js highlight={7-10} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      to: { user_id: 'user_123' },
      template: 'nt_01kx4h2jdafq8bk9aftxak4b40',
      channels: {
        push: {
          override: {
            title: 'Order shipped',
            body: 'Your order is on its way.',
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={7-10} theme={null}
  response = client.send.message(
      message={
          "to": {"user_id": "user_123"},
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "channels": {
              "push": {
                  "override": {
                      "title": "Order shipped",
                      "body": "Your order is on its way.",
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={10-13} wrap theme={null}
  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",
        "channels": {
          "push": {
            "override": {
              "title": "Order shipped",
              "body": "Your order is on its way."
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={7-10} theme={null}
  response = courier.send_.message(
    message: {
      to: { user_id: "user_123" },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      channels: {
        push: {
          override: {
            title: "Order shipped",
            body: "Your order is on its way."
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={11-14} theme={null}
  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"),
  		Channels: shared.MessageChannelsParam{
  			"push": shared.ChannelParam{
  				Override: map[string]any{
  					"title":       "Order shipped",
  					"body":        "Your order is on its way.",
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={7-9} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().userId("user_123").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .channels(MessageChannels.builder()
              .putAdditionalProperty("push", JsonValue.from(java.util.Map.of(
                  "override", java.util.Map.of(
                      "title", "Order shipped",
                      "body", "Your order is on its way."))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={7-10} theme={null}
  $response = $client->send->message(
    message: [
      'to' => ['userID' => 'user_123'],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'channels' => [
        'push' => [
          'override' => [
            'title' => 'Order shipped',
            'body' => 'Your order is on its way.',
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={13-17} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Channels = new Dictionary<string, Channel>()
          {
              {
                  "push",
                  new()
                  {
                      Override = new Dictionary<string, JsonElement>()
                      {
                          { "title", JsonSerializer.SerializeToElement("Order shipped") },
                          { "body", JsonSerializer.SerializeToElement("Your order is on its way.") },
                      },
                  }
              },
          },
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={5} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"user_id": "user_123"}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
    --message.channels '{"push":{"override":{"title":"Order shipped","body":"Your order is on its way."}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 and override the push title to "Order shipped".
  ```
</CodeGroup>

## Tracking

Courier attaches a `trackingUrl` to every push request. Posting to it updates the notification's state. The <Doc href="/docs/sdk-libraries/sdks-overview">Courier Mobile SDKs</Doc> do this for you. To do it manually:

### Example message

```json theme={null}
{
  "message": {
    "data": {
      "trackingUrl": "https://api.courier.com/e/123_channelTrackingId"
      // other data attributes
    }
    // other messages attributes
  }
}
```

### Example request

<CodeGroup>
  ```javascript Node.js theme={null}
  await fetch("https://api.courier.com/e/123_channelTrackingId", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ event: "DELIVERED" }),
  });
  ```

  ```python Python theme={null}
  import requests

  requests.post(
      "https://api.courier.com/e/123_channelTrackingId",
      json={"event": "DELIVERED"},
  )
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/e/123_channelTrackingId \
    -H "Content-Type: application/json" \
    -d '{"event":"DELIVERED"}'
  ```
</CodeGroup>

`event` is `DELIVERED` or `CLICKED`.

### Provider-specific tracking

Each provider puts the `trackingUrl` somewhere different in the payload your app receives:

| Provider                                                                | Where to read it                                                                                                                                                               |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <Doc href="/docs/integrations/push/apple-push-notification">Apple APNs</Doc> | `data` on the incoming [`payload`](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns/) |
| <Doc href="/docs/integrations/push/firebase-fcm">Firebase FCM</Doc>          | `data` on the incoming `message` payload                                                                                                                                       |
| <Doc href="/docs/integrations/push/expo">Expo</Doc>                          | `data` on the incoming payload                                                                                                                                                 |
| <Doc href="/docs/integrations/push/pusher">Pusher</Doc>                      | `data` on the incoming payload                                                                                                                                                 |
| <Doc href="/docs/integrations/push/airship">Airship</Doc>                    | `global_attributes` [data bag](https://docs.airship.com/whats-new/2021-08-02-push-api-personalization/)                                                                        |

## Targeting specific devices

If a user has tokens from more than one app, target a subset at send time. Use it for per-platform notifications, or when several apps share one provider project.

The <Doc href="/docs/sdk-libraries/sdks-overview">Courier mobile SDKs</Doc> already tag every token they register with the app's own identifier, the bundle id on iOS and the package name on Android. Nothing to set up, so the only thing you write is the filter.

### Filter tokens at send time

Pass `filterByBundleId` and `bundleId` in the provider override to send only to tokens whose `device.app_id` matches:

<CodeGroup>
  ```javascript Node.js highlight={17-18} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        user_id: "user_123",
      },
      routing: {
        method: "single",
        channels: [
          "push",
        ],
      },
      providers: {
        "firebase-fcm": {
          override: {
            config: {
              filterByBundleId: true,
              bundleId: "com.acme-corp.app",
            },
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={17-18} theme={null}
  response = client.send.message(
      message={
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "to": {
              "user_id": "user_123",
          },
          "routing": {
              "method": "single",
              "channels": [
                  "push",
              ],
          },
          "providers": {
              "firebase-fcm": {
                  "override": {
                      "config": {
                          "filterByBundleId": True,
                          "bundleId": "com.acme-corp.app",
                      },
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={20-21} wrap theme={null}
  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": {
          "user_id": "user_123"
        },
        "routing": {
          "method": "single",
          "channels": [
            "push"
          ]
        },
        "providers": {
          "firebase-fcm": {
            "override": {
              "config": {
                "filterByBundleId": true,
                "bundleId": "com.acme-corp.app"
              }
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={17-18} theme={null}
  response = courier.send_.message(
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        user_id: "user_123"
      },
      routing: {
        method: "single",
        channels: [
          "push"
        ]
      },
      providers: {
        "firebase-fcm": {
          override: {
            config: {
              filterByBundleId: true,
              bundleId: "com.acme-corp.app"
            }
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={19-20} theme={null}
  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"),
  		Routing: courier.SendMessageParamsMessageRouting{
  			Channels: []shared.MessageRoutingChannelUnionParam{
  				{OfString: courier.String("push")},
  			},
  			Method: "single",
  		},
  		Providers: shared.MessageProvidersParam{
  			"firebase-fcm": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"config": map[string]any{
  						"filterByBundleId": true,
  						"bundleId": "com.acme-corp.app",
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={12-13} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().userId("user_123").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .routing(SendMessageParams.Message.Routing.builder()
              .addChannel("push")
              .method(SendMessageParams.Message.Routing.Method.SINGLE)
              .build())
          .providers(MessageProviders.builder()
              .putAdditionalProperty("firebase-fcm", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "config", java.util.Map.of(
                          "filterByBundleId", true,
                          "bundleId", "com.acme-corp.app"
                      )
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={17-18} theme={null}
  $response = $client->send->message(
    message: [
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'to' => [
        'user_id' => 'user_123',
      ],
      'routing' => [
        'method' => 'single',
        'channels' => [
          'push',
        ],
      ],
      'providers' => [
        'firebase-fcm' => [
          'override' => [
            'config' => [
              'filterByBundleId' => true,
              'bundleId' => 'com.acme-corp.app',
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={18-19} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Routing = new() { Channels = ["push"], Method = Send::Method.Single },
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "firebase-fcm",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "config": {
                              "filterByBundleId": true,
                              "bundleId": "com.acme-corp.app"
                            }
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={6} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"user_id": "user_123"}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
    --message.routing '{"method": "single", "channels": ["push"]}' \
    --message.providers '{"firebase-fcm": {"override": {"config": {"filterByBundleId": true, "bundleId": "com.acme-corp.app"}}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my template to user_123, only to tokens from my desktop app bundle.
  ```
</CodeGroup>

Use the bundle id of the app whose devices you want, `com.acme-corp.app` above. If `bundleId` matches none of the user's tokens, the message is not delivered to that provider.

The same override structure works for any push provider:

| Provider                        | Override path                            |
| ------------------------------- | ---------------------------------------- |
| Firebase Cloud Messaging        | `providers.firebase-fcm.override.config` |
| Apple Push Notification service | `providers.apn.override.config`          |
| Expo                            | `providers.expo.override.config`         |
| OneSignal                       | `providers.onesignal.override.config`    |

<Info>
  Filtering reads the tokens Courier manages, whether an SDK registered them or your backend did. A token passed inline in `to`, such as `firebaseToken` or `apn.token`, carries no device record and bypasses the filter.
</Info>

Registering tokens from your backend rather than through an SDK? Set `device.app_id` yourself on the write. <Doc href="/docs/send/push/overview#manage-tokens-yourself">Manage tokens yourself</Doc> has that call.

<Note>
  **Custom data not reaching the device?** `data` maps into the push payload by default. <Doc href="/docs/design/templates/data-mapping">Data mapping</Doc> covers the v1 template setting that can stop it.
</Note>
