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

> Attach custom data to a push and read it on iOS, Android, React Native, and Flutter.

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

A push carries more than a title and body. The `data` object rides alongside the alert, and your app reads it on arrival or tap.

Put your fields in a `data` object on the message. One `data` object covers FCM, APNs, and Expo, so you describe the payload once.

Send your push template with a `data` object using <Endpoint method="POST" path="/send" name="Send a message" href="/docs/api-reference/send/send-a-message" />:

<CodeGroup>
  ```javascript Node.js highlight={6-10} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: { user_id: 'user_123' },
      template: 'nt_01kx4h2jdafq8bk9aftxak4b40',
      routing: { method: 'all', channels: ['push'] },
      data: {
        order_id: 'ord_4821',
        screen: 'order-detail',
        carrier: 'UPS',
      },
    },
  });
  ```

  ```python Python highlight={6-10} theme={null}
  response = client.send.message(
      message={
          "to": {"user_id": "user_123"},
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "routing": {"method": "all", "channels": ["push"]},
          "data": {
              "order_id": "ord_4821",
              "screen": "order-detail",
              "carrier": "UPS",
          },
      },
  )
  ```

  ```bash cURL highlight={9-13} 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": "all", "channels": ["push"] },
        "data": {
          "order_id": "ord_4821",
          "screen": "order-detail",
          "carrier": "UPS"
        }
      }
    }'
  ```

  ```ruby Ruby highlight={6-10} theme={null}
  response = courier.send_.message(
    message: {
      to: { user_id: "user_123" },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      routing: { method: "all", channels: ["push"] },
      data: {
        order_id: "ord_4821",
        screen: "order-detail",
        carrier: "UPS"
      }
    }
  )
  ```

  ```go Go highlight={11-15} 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{
  			Method:   string(shared.MessageRoutingMethodAll),
  			Channels: []shared.MessageRoutingChannelUnionParam{{OfString: courier.String("push")}},
  		},
  		Data: map[string]any{
  			"order_id": "ord_4821",
  			"screen":  "order-detail",
  			"carrier": "UPS",
  		},
  	},
  })
  ```

  ```java Java highlight={8-11} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("user_id", "user_123")))
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .routing(JsonValue.from(java.util.Map.of(
              "method", "all",
              "channels", java.util.List.of("push"))))
          .data(JsonValue.from(java.util.Map.of(
              "order_id", "ord_4821",
              "screen", "order-detail",
              "carrier", "UPS")))
          .build())
      .build();
  client.send().message(params);
  ```

  ```php PHP highlight={6-10} theme={null}
  $response = $client->send->message(
    message: [
      'to' => ['userID' => 'user_123'],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'routing' => ['method' => 'all', 'channels' => ['push']],
      'data' => [
        'order_id' => 'ord_4821',
        'screen' => 'order-detail',
        'carrier' => 'UPS',
      ],
    ],
  );
  ```

  ```csharp C# highlight={8-13} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Routing = new Routing { Method = Method.All, Channels = ["push"] },
          Data = new Dictionary<string, JsonElement>()
          {
              { "order_id", JsonSerializer.SerializeToElement("ord_4821") },
              { "screen", JsonSerializer.SerializeToElement("order-detail") },
              { "carrier", JsonSerializer.SerializeToElement("UPS") },
          },
      },
  };

  await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={3} theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message '{"to":{"user_id":"user_123"},"template":"nt_01kx4h2jdafq8bk9aftxak4b40","routing":{"method":"all","channels":["push"]},"data":{"order_id":"ord_4821","screen":"order-detail","carrier":"UPS"}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send the order-shipped push to user_123 with the order id and deep link in the data.
  ```
</CodeGroup>

<Note>
  **Older templates need a route for `data`.**<br />
  New templates map `data` into the FCM and APNs payload by default, and a send with inline `content` needs no mapping at all. Having trouble getting your fields through on an older template? <Doc href="/docs/design/templates/data-mapping">Data mapping</Doc> covers it. Expo forwards `data` either way.
</Note>

Courier adds one key of its own to the same object, `trackingUrl`, for delivery and click tracking. Treat that name as reserved.

## How the data arrives per provider

Each provider has its own payload rules, so the same `data` object lands in a different shape.

| Provider                                                          | Where it lands                                   | Nested values                                                   | Override key                         |
| :---------------------------------------------------------------- | :----------------------------------------------- | :-------------------------------------------------------------- | :----------------------------------- |
| <Doc href="/docs/integrations/push/firebase-fcm">FCM</Doc>             | The `data` block of the FCM message              | Flattened to strings. An object or array becomes a JSON string. | `override.body.data`                 |
| <Doc href="/docs/integrations/push/apple-push-notification">APNs</Doc> | The top level of the APNs payload, next to `aps` | Preserved as real JSON.                                         | `override.body.payload`              |
| <Doc href="/docs/integrations/push/expo">Expo</Doc>                    | The `data` field of the Expo push message        | Preserved as real JSON.                                         | `override` (merged at the top level) |

**FCM flattens everything to strings.** FCM only accepts string values in `data`, so Courier converts anything else with `JSON.stringify`. Send `{ "nested": { "carrier": "UPS" } }` and the device receives `nested` as the string `{"carrier":"UPS"}`, which your app has to parse. Numbers arrive as strings too: `badgeCount: 3` arrives as `"3"`.

**Expo forwards `data` on every send.** The Expo handler copies the request's `data` straight into the payload, so it arrives with no data mapping.

### The same send, side by side

Sending this `data` object, with the template's push channel mapping it through:

```json theme={null}
{
  "order_id": "ord_4821",
  "screen": "order-detail",
  "badgeCount": 3,
  "isPriority": true,
  "nested": {
    "carrier": "UPS",
    "eta": "2026-07-30"
  },
  "tags": ["shipped", "priority"]
}
```

produces two different payloads on the device:

<CodeGroup>
  ```json APNs theme={null}
  {
    "aps": {
      "alert": { "title": "Order #4821 shipped", "body": "Arriving Thursday." },
      "sound": "ping.aiff",
      "mutable-content": 1
    },
    "order_id": "ord_4821",
    "screen": "order-detail",
    "badgeCount": 3,
    "isPriority": true,
    "nested": { "carrier": "UPS", "eta": "2026-07-30" },
    "tags": ["shipped", "priority"],
    "trackingUrl": "https://<tenant>.ct0.app/t/<token>"
  }
  ```

  ```json FCM theme={null}
  {
    "title": "Order #4821 shipped",
    "body": "Arriving Thursday.",
    "order_id": "ord_4821",
    "screen": "order-detail",
    "badgeCount": "3",
    "isPriority": "true",
    "nested": "{\"carrier\":\"UPS\",\"eta\":\"2026-07-30\"}",
    "tags": "[\"shipped\",\"priority\"]",
    "trackingUrl": "https://<tenant>.ct0.app/t/<token>",
    "gcm.message_id": "1785281856917263",
    "google.c.sender.id": "313914309026"
  }
  ```
</CodeGroup>

On APNs your keys are siblings of `aps` and keep their JSON types. On FCM they live in the `data` block and are all strings.

Both payloads reflect Courier's **default** provider settings, tuned for the mobile SDKs:

* **FCM: Apply Recommended Courier Mobile SDK Formatting** (`remapData`), on by default. It ships the message as `data` instead of `notification`, which improves delivery and lets you style the notification yourself. It also attaches an APNs override so iOS delivery tracking stays accurate. That is why `title` and `body` appear in the `data` block above. Turn it off and they arrive in a separate `notification` block.
* **APNs: Attach Mutable Content** (`mutableContent`), on by default. It adds `"mutable-content": 1`, which lets a <Doc href="/docs/sdk-libraries/ios#notification-service-extension">Notification Service Extension</Doc> see the message before it reaches the user and record the delivery. Turn it off and messages still arrive, but delivery metrics get less accurate.

An iOS device receiving through FCM also gets the `aps` block and the `google.c.*` keys shown above. An Android device does not.

<Tip>
  Flat, already-stringified `data` values avoid parsing on Android. One level of string keys and string values behaves identically on FCM, APNs, and Expo.
</Tip>

## Read the data on the device

The Courier mobile SDKs hand you the full payload on delivery and on tap. Read your keys from it and route.

<CodeGroup>
  ```swift iOS theme={null}
  class AppDelegate: CourierDelegate {

      override func pushNotificationDeliveredInForeground(
          message: [AnyHashable: Any]
      ) -> UNNotificationPresentationOptions {
          let order_id = message["order_id"] as? String
          print("Delivered for order: \(order_id ?? "none")")
          return [.sound, .list, .banner, .badge]
      }

      override func pushNotificationClicked(message: [AnyHashable: Any]) {
          if let screen = message["screen"] as? String {
              router.navigate(to: screen)
          }
      }
  }
  ```

  ```kotlin Android theme={null}
  class MainActivity : CourierActivity() {

      // The payload is a Map<String, String>: every FCM data value is a string
      override fun onPushNotificationClicked(pushNotification: Map<String, String>) {
          val screen = pushNotification["screen"]
          val order_id = pushNotification["order_id"]

          // A nested object arrives as a JSON string, so parse it
          val carrier = pushNotification["nested"]?.let { JSONObject(it).getString("carrier") }

          screen?.let { router.navigate(it) }
      }

      override fun onPushNotificationDelivered(pushNotification: Map<String, String>) {
          Log.d("Courier", "Delivered: $pushNotification")
      }
  }
  ```

  ```dart Flutter theme={null}
  Courier.shared.addPushListener(
    onPushClicked: (push) {
      final screen = push['screen'];
      if (screen != null) {
        router.navigate(screen);
      }
    },
    onPushDelivered: (push) {
      debugPrint('Delivered: $push');
    },
  );
  ```

  ```typescript React Native theme={null}
  Courier.shared.addPushNotificationListener({
    onPushNotificationClicked: (push) => {
      if (push.screen) {
        router.navigate(push.screen);
      }
    },
    onPushNotificationDelivered: (push) => {
      console.log("Delivered:", push);
    },
  });
  ```
</CodeGroup>

On iOS and Android the handler is a method on a class you already own, so it lives as long as
your app does. Flutter and React Native return a listener instead, and it holds a reference
until you drop it. Call `remove()` on teardown, such as inside Flutter's `dispose()`.

## Deep-link on tap

Send the destination under your own key in `data`, then read it in your tap handler and route. The example at the top of this page uses `screen`. Any URL works there: a custom scheme like `acme://orders/ord_4821`, or a universal link like `https://acme.com/orders/ord_4821`.

Universal links are set up on the platform, not in Courier. Courier carries the string, and the operating system decides whether it opens your app:

* [Apple's Universal Links guide](https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content)
* [Android's App Links guide](https://developer.android.com/training/app-links)
* [Expo's deep linking guide](https://docs.expo.dev/linking/into-your-app/)

The tap handler is the one in [Read the data on the device](#read-the-data-on-the-device) above.

## Send data with no visible alert

A silent push shows the user nothing. Use it to move the app icon badge, or to wake your app in the background and sync.

`providers.apn.override.silent` keeps the alert off the payload, so nothing appears on the lock screen. Pair it with `badge` and APNs moves the number itself, with no code running in your app:

<CodeGroup>
  ```javascript Node.js highlight={10,12} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: {
        user_id: "user_123",
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      providers: {
        apn: {
          override: {
            silent: true,
            body: {
              badge: 3,
            },
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={10,12} theme={null}
  response = client.send.message(
      message={
          "to": {
              "user_id": "user_123",
          },
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "providers": {
              "apn": {
                  "override": {
                      "silent": True,
                      "body": {
                          "badge": 3,
                      },
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={13,15} 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",
        "providers": {
          "apn": {
            "override": {
              "silent": true,
              "body": {
                "badge": 3
              }
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={10,12} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        user_id: "user_123"
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      providers: {
        apn: {
          override: {
            silent: true,
            body: {
              badge: 3
            }
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={12,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"),
  		Providers: shared.MessageProvidersParam{
  			"apn": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"silent": true,
  					"body": map[string]any{
  						"badge": 3,
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={7-8} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().userId("user_123").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .providers(MessageProviders.builder()
              .putAdditionalProperty("apn", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "silent", true,
                      "body", java.util.Map.of("badge", 3)
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={10,12} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'user_id' => 'user_123',
      ],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'providers' => [
        'apn' => [
          'override' => [
            'silent' => true,
            'body' => [
              'badge' => 3,
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={16,18} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "apn",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "silent": true,
                            "body": {
                              "badge": 3
                            }
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

  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.providers '{"apn": {"override": {"silent": true, "body": {"badge": 3}}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 as a silent iOS push that sets the badge to 3.
  ```
</CodeGroup>

To wake your app in the background as well, add `"content-available": 1` to the same `body`. iOS then launches your app to handle the payload, which is what you want for syncing content before the user opens it. A badge on its own does not need it.

On FCM, the message already ships as data with no `notification` block, because **Apply Recommended Courier Mobile SDK Formatting** (`remapData`) is on by default. Courier moves `title`, `body`, and `image` into the `data` block alongside your own keys. Your app reads all of it as data and decides what to display.

If that setting is off on your integration, switch it back on for a single send:

<CodeGroup>
  ```javascript Node.js highlight={11} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: {
        user_id: "user_123",
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      providers: {
        "firebase-fcm": {
          override: {
            config: {
              remapData: true,
            },
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={11} theme={null}
  response = client.send.message(
      message={
          "to": {
              "user_id": "user_123",
          },
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "providers": {
              "firebase-fcm": {
                  "override": {
                      "config": {
                          "remapData": True,
                      },
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={14} 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",
        "providers": {
          "firebase-fcm": {
            "override": {
              "config": {
                "remapData": true
              }
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={11} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        user_id: "user_123"
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      providers: {
        "firebase-fcm": {
          override: {
            config: {
              remapData: true
            }
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={13} 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"),
  		Providers: shared.MessageProvidersParam{
  			"firebase-fcm": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"config": map[string]any{
  						"remapData": true,
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={7} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().userId("user_123").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .providers(MessageProviders.builder()
              .putAdditionalProperty("firebase-fcm", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "config", java.util.Map.of("remapData", true)
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={11} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'user_id' => 'user_123',
      ],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'providers' => [
        'firebase-fcm' => [
          'override' => [
            'config' => [
              'remapData' => true,
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={17} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "firebase-fcm",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "config": {
                              "remapData": true
                            }
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

  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.providers '{"firebase-fcm": {"override": {"config": {"remapData": true}}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 with FCM data remapping turned on for this send.
  ```
</CodeGroup>

Your app needs background delivery configured either way: **Background Modes → Remote notifications** on iOS, and a `FirebaseMessagingService` that handles `onMessageReceived` on Android.

## Verify the payload

<Steps>
  <Step title="Send to a real device">
    Send the request above to a `user_id` with a registered token. Simulators do not receive push.
  </Step>

  <Step title="Confirm the payload on the device">
    Log the full message in your delivery or tap handler and check your keys are present. On Android, every value is a string.
  </Step>

  <Step title="Check the log if a key is missing">
    Open <AppLink href="https://app.courier.com/logs">Logs</AppLink> and open the message. The provider request shows the exact payload Courier sent, including the `data` block. Fields missing there were dropped by the send, not the device. The usual cause is a template send with no data mapping.
  </Step>
</Steps>

## FAQ

<AccordionGroup>
  <Accordion title="Why is my data missing from the push payload?">
    Check the template's age. New templates map `data` through by default, but one created before that default uses `data` for variable substitution only. <Doc href="/docs/design/templates/data-mapping">Data mapping</Doc> shows how to turn it on, or set the fields in a <Doc href="/docs/send/overrides#how-overrides-work">provider override</Doc>. Expo forwards `data` either way.
  </Accordion>

  <Accordion title="Why did my number arrive as a string on Android?">
    FCM only permits string values in its `data` block, so Courier stringifies everything else. `3` becomes `"3"` and `{ "a": "b" }` becomes `"{\"a\":\"b\"}"`. Parse on the device, or send flat strings to begin with.
  </Accordion>

  <Accordion title="Can I send different data to iOS and Android?">
    Send the shared fields in the top-level `data` object, then add per-platform fields in each provider's override (`firebase-fcm` under `override.body.data`, `apn` under `override.body.payload`).
  </Accordion>

  <Accordion title="Is there a size limit on the data payload?">
    The provider sets it, not Courier. APNs allows 4KB for a normal push, and FCM allows 4KB for the data block. The provider rejects anything over the limit and the message goes `UNDELIVERABLE`. Send IDs and let the app fetch the rest.
  </Accordion>

  <Accordion title="Which provider keys does Courier use for push?">
    `firebase-fcm`, `apn`, `expo`, `onesignal`, and `pusher-beams`. Use these keys to register a token and to override a provider on a send.
  </Accordion>
</AccordionGroup>
