> ## 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.

# Send iOS push with APNs through Courier

> Connect APNs with a .p8 key, team ID, and bundle ID, and sync device tokens with the iOS SDK.

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

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

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>;
};

## Prerequisites

* [An Apple Developer account](https://developer.apple.com/account)
* An APNs authentication key, downloaded as a `.p8` file
* Your team identifier and the app bundle ID you send to
* A physical iOS device, since simulators do not receive push

## Setup

<Steps>
  <Step title="Create an APNs key">
    In your [Apple Developer Account](https://developer.apple.com/account), open **Certificates → Keys** and add a key. Enable **Apple Push Notifications service (APNs)**, register it, and download the `.p8` file.
  </Step>

  <Step title="Enter your credentials">
    Open the <AppLink href="https://app.courier.com/integrations/catalog/apn">APNs provider configuration</AppLink> and fill in four fields:

    * **Key Id**, the identifier of the APNs key.
    * **Key**, the full contents of the downloaded `.p8` file.
    * **Team Id**, your Apple Developer team identifier.
    * **Topic (App Bundle Id)**, the bundle ID of the app you are sending to.

    Then set the two toggles:

    * **Send to Production**, off by default. Off targets Apple's sandbox, which a local development build needs. On targets production, for your App Store app. Getting this wrong is the usual cause of [`BadEnvironmentKeyInToken`](#badenvironmentkeyintoken).
    * **Attach Mutable Content**, on by default. It adds `"mutable-content": 1`, which is what lets Courier's <Doc href="/docs/sdk-libraries/ios#notification-service-extension">Notification Service Extension</Doc> run before the alert appears. The extension is what reports a delivery while your app is closed. Turn this off and delivery only registers in the foreground. See <Guide href="/docs/guides/set-up-mobile-push#delivery-and-click-tracking">Delivery and click tracking</Guide>.

    <Frame caption="The APNs provider configuration with the Send to Production and Attach Mutable Content toggles.">
      <img src="https://mintcdn.com/courier-4f1f25dc/A-IH_41Pkuff3UAy/assets/apns-auto-override.webp?fit=max&auto=format&n=A-IH_41Pkuff3UAy&q=85&s=19359965ffd78512f7e4a0c3eb59fc38" alt="APNs provider configuration showing the Send to Production and Attach Mutable Content toggles" width="1754" height="694" data-path="assets/apns-auto-override.webp" />
    </Frame>
  </Step>

  <Step title="Install the provider">
    Click "Install Provider" or "Save".
  </Step>
</Steps>

## 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. <Doc href="/docs/send/push/overview">Send push</Doc> covers the model.

APNs addresses the device by token, so the profile you send to needs an `apn` object. Store the token once with <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile" />, which merges into the profile and creates it if it does not exist:

<Tabs>
  <Tab title="One device">
    `apn.token` holds a single device token.

    <CodeGroup>
      ```javascript Node.js highlight={3-5} theme={null}
      const profile = await client.profiles.create('user_123', {
        profile: {
          apn: {
            token: 'YOUR_APNS_TOKEN',
          },
        },
      });
      ```

      ```python Python highlight={4-6} theme={null}
      profile = client.profiles.create(
          user_id="user_123",
          profile={
              "apn": {
                  "token": "YOUR_APNS_TOKEN",
              },
          },
      )
      ```

      ```bash cURL highlight={7-9} theme={null}
      curl --request POST \
        --url https://api.courier.com/profiles/user_123 \
        --header "Authorization: Bearer $COURIER_API_KEY" \
        --header 'Content-Type: application/json' \
        --data '{
          "profile": {
            "apn": {
              "token": "YOUR_APNS_TOKEN"
            }
          }
        }'
      ```

      ```ruby Ruby highlight={4-6} theme={null}
      profile = courier.profiles.create(
        "user_123",
        profile: {
          apn: {
            token: "YOUR_APNS_TOKEN"
          }
        }
      )
      ```

      ```go Go highlight={6-8} theme={null}
      profile, err := client.Profiles.New(
      	context.TODO(),
      	"user_123",
      	courier.ProfileNewParams{
      		Profile: map[string]any{
      			"apn": map[string]any{
      				"token": "YOUR_APNS_TOKEN",
      			},
      		},
      	},
      )
      ```

      ```java Java highlight={4-5} theme={null}
      ProfileCreateParams params = ProfileCreateParams.builder()
          .userId("user_123")
          .profile(ProfileCreateParams.Profile.builder()
              .putAdditionalProperty("apn", JsonValue.from(java.util.Map.of(
                  "token", "YOUR_APNS_TOKEN")))
              .build())
          .build();
      ProfileCreateResponse profile = client.profiles().create(params);
      ```

      ```php PHP highlight={2-4} theme={null}
      $profile = $client->profiles->create('user_123', profile: [
        'apn' => [
          'token' => 'YOUR_APNS_TOKEN',
        ],
      ]);
      ```

      ```csharp C# highlight={7-9} theme={null}
      ProfileCreateParams parameters = new()
      {
          UserID = "user_123",
          Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
              """
              {
                "apn": {
                  "token": "YOUR_APNS_TOKEN"
                }
              }
              """
          ),
      };
      var profile = await client.Profiles.Create(parameters);
      ```

      ```bash CLI highlight={4} wrap theme={null}
      courier profiles create \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123 \
        --profile '{"apn":{"token":"YOUR_APNS_TOKEN"}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, save the APNs token YOUR_APNS_TOKEN on user_123.
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Several devices">
    `apn.tokens` holds every device that user signs in on. Courier delivers to all of them.

    <CodeGroup>
      ```javascript Node.js highlight={3-8} theme={null}
      const profile = await client.profiles.create('user_123', {
        profile: {
          apn: {
            tokens: [
              'APNS_TOKEN_ONE',
              'APNS_TOKEN_TWO',
            ],
          },
        },
      });
      ```

      ```python Python highlight={4-9} theme={null}
      profile = client.profiles.create(
          user_id="user_123",
          profile={
              "apn": {
                  "tokens": [
                      "APNS_TOKEN_ONE",
                      "APNS_TOKEN_TWO",
                  ],
              },
          },
      )
      ```

      ```bash cURL highlight={7-12} theme={null}
      curl --request POST \
        --url https://api.courier.com/profiles/user_123 \
        --header "Authorization: Bearer $COURIER_API_KEY" \
        --header 'Content-Type: application/json' \
        --data '{
          "profile": {
            "apn": {
              "tokens": [
                "APNS_TOKEN_ONE",
                "APNS_TOKEN_TWO"
              ]
            }
          }
        }'
      ```

      ```ruby Ruby highlight={4-6} theme={null}
      profile = courier.profiles.create(
        "user_123",
        profile: {
          apn: {
            tokens: ["APNS_TOKEN_ONE", "APNS_TOKEN_TWO"]
          }
        }
      )
      ```

      ```go Go highlight={6-8} theme={null}
      profile, err := client.Profiles.New(
      	context.TODO(),
      	"user_123",
      	courier.ProfileNewParams{
      		Profile: map[string]any{
      			"apn": map[string]any{
      				"tokens": []any{"APNS_TOKEN_ONE", "APNS_TOKEN_TWO"},
      			},
      		},
      	},
      )
      ```

      ```java Java highlight={4-5} theme={null}
      ProfileCreateParams params = ProfileCreateParams.builder()
          .userId("user_123")
          .profile(ProfileCreateParams.Profile.builder()
              .putAdditionalProperty("apn", JsonValue.from(java.util.Map.of(
                  "tokens", java.util.List.of("APNS_TOKEN_ONE", "APNS_TOKEN_TWO"))))
              .build())
          .build();
      ProfileCreateResponse profile = client.profiles().create(params);
      ```

      ```php PHP highlight={2-4} theme={null}
      $profile = $client->profiles->create('user_123', profile: [
        'apn' => [
          'tokens' => ['APNS_TOKEN_ONE', 'APNS_TOKEN_TWO'],
        ],
      ]);
      ```

      ```csharp C# highlight={7-12} theme={null}
      ProfileCreateParams parameters = new()
      {
          UserID = "user_123",
          Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
              """
              {
                "apn": {
                  "tokens": [
                    "APNS_TOKEN_ONE",
                    "APNS_TOKEN_TWO"
                  ]
                }
              }
              """
          ),
      };
      var profile = await client.Profiles.Create(parameters);
      ```

      ```bash CLI highlight={4} wrap theme={null}
      courier profiles create \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123 \
        --profile '{"apn":{"tokens":["APNS_TOKEN_ONE","APNS_TOKEN_TWO"]}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, save the APNs tokens APNS_TOKEN_ONE and APNS_TOKEN_TWO on user_123.
      ```
    </CodeGroup>
  </Tab>
</Tabs>

A Courier mobile SDK writes that object for you once you extend `CourierDelegate`, so you send to a `user_id` and never handle a token. APNs is Apple-only, so that means the <Doc href="/docs/sdk-libraries/ios">iOS</Doc>, <Doc href="/docs/sdk-libraries/react-native">React Native</Doc>, and <Doc href="/docs/sdk-libraries/flutter">Flutter</Doc> SDKs. The Android SDK uses <Doc href="/docs/integrations/push/firebase-fcm">FCM</Doc> instead.

For a one-off with no stored profile, pass it inline instead: `"to": { "apn": { "token": "YOUR_APNS_TOKEN" } }`.

<CardGroup cols={2}>
  <Card title="Set up push notifications" icon="mobile" href="/docs/guides/set-up-mobile-push">
    Wire up the SDK, sync tokens, and send your first push.
  </Card>

  <Card title="Send by user id" icon="user" href="/docs/recipients/overview#send-by-user-id">
    The call in every language, and the rest of the profile object.
  </Card>
</CardGroup>

## Send to a recipient

<Tabs>
  <Tab title="Send to user id">
    Courier reads `apn` off the saved profile, so preferences apply and the value can change without touching this code.

    <CodeGroup>
      ```javascript Node.js highlight={4} theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            user_id: "user_123",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        },
      });
      ```

      ```python Python highlight={4} theme={null}
      response = client.send.message(
          message={
              "to": {
                  "user_id": "user_123",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      )
      ```

      ```bash cURL highlight={7} 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"
          }
        }'
      ```

      ```ruby Ruby highlight={4} theme={null}
      response = courier.send_.message(
        message: {
          to: {
            user_id: "user_123"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40"
        }
      )
      ```

      ```go Go highlight={5} 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"),
      	},
      })
      ```

      ```java Java highlight={3} theme={null}
      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);
      ```

      ```php PHP highlight={4} theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'user_id' => 'user_123',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
        ],
      );
      ```

      ```csharp C# highlight={5} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      };

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

      ```bash CLI highlight={3} wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"user_id": "user_123"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to user_123 by email.
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Send to a token">
    Pass it inline instead and nothing is stored. Swap this `to` object into the call on the other tab.

    | To address                    | Use                                                                      |
    | ----------------------------- | ------------------------------------------------------------------------ |
    | One device                    | `"to": { "apn": { "token": "YOUR_APNS_TOKEN" } }`                        |
    | Several devices               | `"to": { "apn": { "tokens": ["APNS_TOKEN_ONE", "APNS_TOKEN_TWO"] } }`    |
    | A user, and a specific device | `"to": { "user_id": "user_123", "apn": { "token": "YOUR_APNS_TOKEN" } }` |

    <Warning>
      Without a `user_id`, Courier logs the message under a synthetic `anon_<hash>` recipient and you lose:

      * Stored preferences and profile.
      * Managed token lookup, expiry, sign-out cleanup, and `bundleId` filtering.
      * Delivery and click events on that person's history.

      Put both a `user_id` and a token in `to` to supply your own and keep the user.
    </Warning>

    To pick among several tokens on one user, see <Doc href="/docs/integrations/push/overview#targeting-specific-devices">Targeting specific devices</Doc>.

    <CodeGroup>
      ```javascript Node.js highlight={5-7} theme={null}
      const { requestId } = await courier.send.message({
        message: {
          // A provider recipient is a profile field, so it sits outside the typed union.
          to: {
            apn: {
              token: "YOUR_APNS_TOKEN",
            },
          } as any,
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        },
      });
      ```

      ```python Python highlight={4-6} theme={null}
      response = client.send.message(
          message={
              "to": {
                  "apn": {
                      "token": "YOUR_APNS_TOKEN",
                  },
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      )
      ```

      ```bash cURL highlight={7-9} 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": {
              "apn": {
                "token": "YOUR_APNS_TOKEN"
              }
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40"
          }
        }'
      ```

      ```ruby Ruby highlight={4-6} theme={null}
      response = courier.send_.message(
        message: {
          to: {
            apn: {
              token: "YOUR_APNS_TOKEN"
            }
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40"
        }
      )
      ```

      ```go Go highlight={2-4} theme={null}
      // A provider recipient is a profile field, so it sits outside the typed union.
      to := param.Override[shared.UserRecipientParam](
      	json.RawMessage(`{"apn": {"token": "YOUR_APNS_TOKEN"}}`),
      )

      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      	},
      })
      ```

      ```java Java highlight={3-4} theme={null}
      // A provider recipient is a profile field, so it sits outside the typed union.
      UserRecipient to = UserRecipient.builder()
          .putAdditionalProperty("apn", JsonValue.from(Map.of(
                  "token", "YOUR_APNS_TOKEN")))
          .build();

      client.send().message(SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(to)
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .build())
          .build());
      ```

      ```php PHP highlight={4-6} theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'apn' => [
              'token' => 'YOUR_APNS_TOKEN',
            ],
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
        ],
      );
      ```

      ```csharp C# highlight={3-5} theme={null}
      // A provider recipient is a profile field, so it sits outside the typed union.
      UserRecipient to = UserRecipient.FromRawUnchecked(
          JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
              """{"apn": {"token": "YOUR_APNS_TOKEN"}}"""
          )
      );

      var response = await client.Send.Message(new() { Message = new() { To = to, Template = "nt_01kx4h2jdafq8bk9aftxak4b40" } });
      ```

      ```bash CLI highlight={3} wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"apn": {"token": "YOUR_APNS_TOKEN"}}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to the APNs token YOUR_APNS_TOKEN.
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Overrides

<Doc href="/docs/send/overrides#how-overrides-work">How overrides work</Doc> covers the two levels and which one wins. <Doc href="/docs/integrations/push/overview#channel-overrides">Push channel overrides</Doc> lists the fields every push provider takes.

An APNs override sits at `providers.apn.override`. `body` changes the notification, and `config` swaps the credentials the send authenticates with.

### Body overrides

Courier compiles the reserved `aps` dictionary from the message content and these fields.

| Field               | Sets                                                                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `payload`           | Custom data at the payload root, beside `aps`. Your app reads these keys on tap. Unlike FCM, APNs preserves nested objects and non-string types. |
| `alert`             | The title, subtitle, and body Courier rendered from the message.                                                                                 |
| `sound`             | The tone played. Defaults to `ping.aiff`, so every push plays the default tone.                                                                  |
| `badge`             | The app icon badge, as a number. Courier ignores `0`, so a send cannot clear the badge.                                                          |
| `expiry`            | A Unix timestamp for how long APNs keeps retrying. Defaults to one hour after the send.                                                          |
| `topic`             | The bundle id the push targets.                                                                                                                  |
| `category`          | The notification category, for actionable notifications.                                                                                         |
| `threadId`          | The thread the notification groups into.                                                                                                         |
| `content-available` | Background delivery. A value above `0` marks the push content-available.                                                                         |
| `apns-priority`     | The delivery priority APNs sends at.                                                                                                             |
| `apns-collapse-id`  | The id APNs coalesces notifications on.                                                                                                          |

For a push that carries data but shows no alert, see <Doc href="/docs/send/push/custom-data#send-data-with-no-visible-alert">silent push</Doc>.

### Config overrides

Each of these replaces the integration's own credentials and settings, for one send.

| Field                  | Replaces                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `isProduction`         | Which APNs environment the send targets. Defaults to `true`.                                                 |
| `mutableContent`       | Whether the push is marked mutable, for a notification service extension.                                    |
| `teamId`               | The Apple team the send authenticates as.                                                                    |
| `key` and `keyId`      | The `.p8` key and its id, on p8 auth.                                                                        |
| `pfx` and `passphrase` | The `.p12` certificate and its password, on p12 auth. `passphrase` replaces the integration's `pfxPassword`. |

<Warning>
  **`override.body.aps` replaces the `aps` dictionary, it does not merge.** The rendered `alert`, `sound`, `badge`, `category`, `thread-id`, and `content-available` are all discarded unless you restate them. Use the individual fields above to change one and keep the rest.
</Warning>

An `aps` override leaves the rest of the request alone. `expiry`, `topic`, `apns-priority`, `apns-collapse-id`, and your own `payload` keys are headers or sit outside `aps`, and mutable content is applied afterward.

### Override the payload

The `apn` override applies whenever APNs is the provider that runs.

<CodeGroup>
  ```javascript Node.js highlight={15-23} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      to: {
        user_id: "user_123",
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      routing: {
        method: "single",
        channels: [
          "push",
        ],
      },
      providers: {
        apn: {
          override: {
            body: {
              sound: "ping.aiff",
              badge: 99,
              payload: {
                YOUR_CUSTOM_KEY: "YOUR_CUSTOM_VALUE",
              },
            },
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={15-23} theme={null}
  response = client.send.message(
      message={
          "to": {
              "user_id": "user_123",
          },
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "routing": {
              "method": "single",
              "channels": [
                  "push",
              ],
          },
          "providers": {
              "apn": {
                  "override": {
                      "body": {
                          "sound": "ping.aiff",
                          "badge": 99,
                          "payload": {
                              "YOUR_CUSTOM_KEY": "YOUR_CUSTOM_VALUE",
                          },
                      },
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={18-26} 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",
        "routing": {
          "method": "single",
          "channels": [
            "push"
          ]
        },
        "providers": {
          "apn": {
            "override": {
              "body": {
                "sound": "ping.aiff",
                "badge": 99,
                "payload": {
                  "YOUR_CUSTOM_KEY": "YOUR_CUSTOM_VALUE"
                }
              }
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={15-23} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        user_id: "user_123"
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      routing: {
        method: "single",
        channels: [
          "push"
        ]
      },
      providers: {
        apn: {
          override: {
            body: {
              sound: "ping.aiff",
              badge: 99,
              payload: {
                YOUR_CUSTOM_KEY: "YOUR_CUSTOM_VALUE"
              }
            }
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={17-25} 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{
  			"apn": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"body": map[string]any{
  						"sound": "ping.aiff",
  						"badge": 99,
  						"payload": map[string]any{
  							"YOUR_CUSTOM_KEY": "YOUR_CUSTOM_VALUE",
  						},
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={10-18} 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("apn", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "body", java.util.Map.of(
                          "sound", "ping.aiff",
                          "badge", 99,
                          "payload", java.util.Map.of(
                              "YOUR_CUSTOM_KEY", "YOUR_CUSTOM_VALUE"
                          )
                      )
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={15-23} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'user_id' => 'user_123',
      ],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'routing' => [
        'method' => 'single',
        'channels' => [
          'push',
        ],
      ],
      'providers' => [
        'apn' => [
          'override' => [
            'body' => [
              'sound' => 'ping.aiff',
              'badge' => 99,
              'payload' => [
                'YOUR_CUSTOM_KEY' => 'YOUR_CUSTOM_VALUE',
              ],
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={14-26} 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>()
          {
              {
                  "apn",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "body": {
                              "sound": "ping.aiff",
                              "badge": 99,
                              "payload": {
                                "YOUR_CUSTOM_KEY": "YOUR_CUSTOM_VALUE"
                              }
                            }
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

  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 '{"apn": {"override": {"body": {"sound": "ping.aiff", "badge": 99, "payload": {"YOUR_CUSTOM_KEY": "YOUR_CUSTOM_VALUE"}}}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my template to user_123 on push, with an APNs sound and badge.
  ```
</CodeGroup>

Custom data, deep links on tap, and silent pushes are the same call with a different payload. <Guide href="/docs/guides/set-up-mobile-push">Set up push notifications</Guide> covers each, including where APNs and FCM differ.

Custom data not reaching the device? <Doc href="/docs/design/templates/data-mapping">Data mapping</Doc> covers the one template setting that stops it.

## Troubleshooting

### `BadDeviceToken`

APNs returns `BadDeviceToken` when the token does not match any registered device for the environment you target (production vs. sandbox). Common causes:

* **Wrong environment**: a token registered against the sandbox environment fails in production, and vice versa. Check the **Send to Production** toggle in the <AppLink href="https://app.courier.com/integrations/catalog/apn">APNs provider configuration</AppLink>.
* **Stale token**: the user uninstalled and reinstalled the app, generating a new token. Update the stored token via the <Endpoint method="PUT" path="/users/{user_id}/tokens/{token}" name="Add a token to a user" href="/docs/api-reference/device-tokens/add-a-token-to-a-user">Device Tokens API</Endpoint> when your app receives a new registration token.
* **Token never registered**: the token was never added to a Courier user profile. Your app must call the registration step on every launch.

### `BadEnvironmentKeyInToken`

The key that signed the APNs JWT belongs to a different team or bundle ID than the token. Check that the **Team ID** and **Key ID** in your Courier APNs configuration match the app whose tokens you send.

### Invalid device tokens

Courier records rejected tokens so later sends skip them. This applies only to **managed tokens**, the ones a Courier Mobile SDK syncs. Tokens you pass in `apn.token` or `apn.tokens` are used as sent.

* `BadDeviceToken`, `Unregistered`, `DeviceTokenNotForTopic`, and `MissingDeviceToken` mark that token failed until the device registers a fresh one.
* The status is scoped **to one token and user**. Other users, tokens, and channels are unaffected.
* Managed tokens not refreshed in **60 days** go stale and are skipped. The SDKs refresh on app launch.

<Tip>
  If a user stops receiving push notifications, confirm their device has registered a current token. Reinstalling the app generates a new one. Confirm your app calls the Courier SDK's token registration on every launch.
</Tip>

## Provider details

```text theme={null}
apn
```

Courier recommends routing to the channel. Naming this key in `routing.channels` instead is supported, and sends through just this provider.

<Card title="Send to a specific provider" icon="bullseye-arrow" href="/docs/send/send-to-a-provider" horizontal arrow="true">
  When that is worth doing, and what you give up: failover, channel priority, and providers you add later.
</Card>
