> ## 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 push with Expo through Courier

> Connect Expo and store each recipient's Expo push tokens on the profile.

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

## Prerequisites

* [An Expo account](https://expo.dev/)
* [An Expo project with push notifications configured](https://docs.expo.dev/push-notifications/overview/)

## Setup

Open the <AppLink href="https://app.courier.com/integrations/catalog/expo">Expo Integration</AppLink> in Courier and enable it. The integration needs no credentials, because Courier delivers using the Expo push tokens on each recipient's profile.

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

Expo addresses the device by Exponent push token, so the profile you send to needs an `expo` 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">
    Pass one device's token as a string to `expo.token`.

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

      ```python Python highlight={4-6} theme={null}
      profile = client.profiles.create(
          user_id="user_123",
          profile={
              "expo": {
                  "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
              },
          },
      )
      ```

      ```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": {
            "expo": {
              "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
            }
          }
        }'
      ```

      ```ruby Ruby highlight={4-6} theme={null}
      profile = courier.profiles.create(
        "user_123",
        profile: {
          expo: {
            token: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
          }
        }
      )
      ```

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

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

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

      ```csharp C# highlight={7-9} theme={null}
      ProfileCreateParams parameters = new()
      {
          UserID = "user_123",
          Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
              """
              {
                "expo": {
                  "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
                }
              }
              """
          ),
      };
      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 '{"expo":{"token":"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, save the Expo push token ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] on user_123.
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Several devices">
    To reach every device on a profile, pass the tokens as an array to `expo.tokens`.

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

      ```python Python highlight={4-9} theme={null}
      profile = client.profiles.create(
          user_id="user_123",
          profile={
              "expo": {
                  "tokens": [
                      "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
                      "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]",
                  ],
              },
          },
      )
      ```

      ```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": {
            "expo": {
              "tokens": [
                "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
                "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]"
              ]
            }
          }
        }'
      ```

      ```ruby Ruby highlight={4-6} theme={null}
      profile = courier.profiles.create(
        "user_123",
        profile: {
          expo: {
            tokens: ["ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]"]
          }
        }
      )
      ```

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

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

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

      ```csharp C# highlight={7-12} theme={null}
      ProfileCreateParams parameters = new()
      {
          UserID = "user_123",
          Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
              """
              {
                "expo": {
                  "tokens": [
                    "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
                    "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]"
                  ]
                }
              }
              """
          ),
      };
      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 '{"expo":{"tokens":["ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]","ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]"]}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, save both of my Expo push tokens on user_123.
      ```
    </CodeGroup>
  </Tab>
</Tabs>

<Note>
  If you pass both `token` and `tokens`, Courier merges `token` into the `tokens` array and dedupes it.
</Note>

Then send to `user_id` and Courier resolves the address.

For a one-off with no stored profile, pass it inline instead: `"to": { "expo": { "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]" } }`.

<CardGroup cols={1}>
  <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 `expo` 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.

    <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: {
            expo: {
              token: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
            },
          } as any,
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        },
      });
      ```

      ```python Python highlight={4-6} theme={null}
      response = client.send.message(
          message={
              "to": {
                  "expo": {
                      "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
                  },
              },
              "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": {
              "expo": {
                "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
              }
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40"
          }
        }'
      ```

      ```ruby Ruby highlight={4-6} theme={null}
      response = courier.send_.message(
        message: {
          to: {
            expo: {
              token: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
            }
          },
          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(`{"expo": {"token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"}}`),
      )

      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("expo", JsonValue.from(Map.of(
                  "token", "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]")))
          .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' => [
            'expo' => [
              'token' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]',
            ],
          ],
          '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>>(
              """{"expo": {"token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"}}"""
          )
      );

      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 '{"expo": {"token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"}}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to that Expo push 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.

A provider override changes what Courier sends to Expo's API. Override keys apply at the top level of the Expo message, **not** nested under `body`. This request adds a ttl and turns off the sound:

<CodeGroup>
  ```javascript Node.js highlight={5-7,13-18} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        expo: {
          token: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
        },
      },
      data: {
        dataForPushMessage: true,
      },
      providers: {
        expo: {
          override: {
            ttl: 10,
            sound: null,
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={5-7,13-18} theme={null}
  response = client.send.message(
      message={
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "to": {
              "expo": {
                  "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
              },
          },
          "data": {
              "dataForPushMessage": True,
          },
          "providers": {
              "expo": {
                  "override": {
                      "ttl": 10,
                      "sound": None,
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={8-10,16-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": {
          "expo": {
            "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
          }
        },
        "data": {
          "dataForPushMessage": true
        },
        "providers": {
          "expo": {
            "override": {
              "ttl": 10,
              "sound": null
            }
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={5-7,13-18} theme={null}
  response = courier.send_.message(
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        expo: {
          token: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
        }
      },
      data: {
        dataForPushMessage: true
      },
      providers: {
        expo: {
          override: {
            ttl: 10,
            sound: nil
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={4-6,17-22} theme={null}
  // 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(`{
    "expo": {
      "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
    }
  }`))

  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  		Data: map[string]any{
  			"dataForPushMessage": true,
  		},
  		Providers: shared.MessageProvidersParam{
  			"expo": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"ttl": 10,
  					"sound": nil,
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={6-8,15-18} theme={null}
  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("expo", JsonValue.from(java.util.Map.of(
                  "token", "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
              )))
              .build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .data(JsonValue.from(java.util.Map.of(
              "dataForPushMessage", true
          )))
          .providers(MessageProviders.builder()
              .putAdditionalProperty("expo", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
                      "ttl", 10,
                      "sound", null
                  ))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={5-7,13-18} theme={null}
  $response = $client->send->message(
    message: [
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'to' => [
        'expo' => [
          'token' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]',
        ],
      ],
      'data' => [
        'dataForPushMessage' => true,
      ],
      'providers' => [
        'expo' => [
          'override' => [
            'ttl' => 10,
            'sound' => null,
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={11-13,25-38} theme={null}
  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>>(
                  """
                  {
                    "expo": {
                      "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
                    }
                  }
                  """
              )
          ),
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Data = new Dictionary<string, JsonElement>()
          {
              { "dataForPushMessage", JsonSerializer.SerializeToElement(true) },
          },
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "expo",
                  new()
                  {
                      Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "ttl": 10,
                            "sound": null
                          }
                          """
                      ),
                  }
              },
          },
      },
  };

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

  ```bash CLI highlight={3,6} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"expo": {"token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"}}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
    --message.data '{"dataForPushMessage": true}' \
    --message.providers '{"expo": {"override": {"ttl": 10, "sound": null}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my template to this Expo push token with a 10 second TTL and no sound.
  ```
</CodeGroup>

Each key you set replaces that key outright. Keys you leave out are still generated and sent (title, subtitle, body, and data above). For every option, see [Expo's push notifications docs](https://docs.expo.dev/push-notifications/overview/).

To send to specific devices when a user has multiple Expo tokens, see <Doc href="/docs/integrations/push/overview#targeting-specific-devices">Targeting Specific Devices</Doc>.

## Tracking events

Courier includes tracking URL information in the `data` attribute of the incoming message payload.

See <Doc href="/docs/integrations/push/overview#tracking">Courier push notification tracking</Doc>

## Troubleshooting

### Could not find APNs credentials

Expo's [help forum](https://forums.expo.dev/t/push-notification-error-could-not-find-apns-credentials-for-standalone-app/6955) explains how Expo handles APN tokens:

> Expo push tokens work quite similarly to how Apple's tokens work with APN's, since Expo's server is a relatively thin service that relays notifications to Apple. Expo needs valid APN credentials to send push notifications. And as with APNs in general (not specific to Expo), it doesn't matter what APNs credentials you had at the time an iOS device provided a device token - the APN credentials just need to be valid at the time of sending the notification.

> So, if you revoke your APNs credentials or clear them from Expo, or if your APNs credentials expire (APN p12 certificates expire after one year), Apple will reject all of your push notifications until you provide Expo with valid APN credentials. To be clear, **your Expo push tokens and the underlying iOS device tokens are still valid** and will still work once you provide new, valid APN credentials.

> In this case, the error `Could not find APNs credentials for…` means that Expo does not have any copy of your APNs credentials (currently a p12 certificate). You will need to let Expo recreate the credentials for you (**recommended in most cases:** run `exp build:ios -c` to clear all of them and then run `exp build:ios` to let Expo re-provision them) or upload your own credentials from Apple's developer center (be sure to follow the instructions carefully.

* Your push key should be correctly configured at `https://expo.dev/accounts/[account]/projects/[project]/credentials?platform=ios`
* You've correctly set your bundle identifier in your configuration
* If you are explicitly passing in the `experienceId` in `Notifications.getExpoPushTokenAsync({ experienceId: 'xxxx' })`, make sure it is in the format of `@expo_account/expo_projectname`

## Provider details

```text theme={null}
expo
```

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>
