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

# Users and profiles

> Store a user's email, phone, tokens, and chat IDs once, then send by user_id.

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

A user is the stored record Courier uses to reach one person. Send by `user_id` and Courier resolves the addresses.

That record is the profile. It holds email, phone number, device tokens, locale, timezone, and custom attributes, so you never repeat contact details on a request. Preferences, lists, and audiences attach to it.

```json theme={null}
{
  "profile": {
    "name": "Sarah Bennett",
    "email": "sarah@acme-corp.com",
    "phone_number": "+15551234567",
    "locale": "en-US",
    "custom": { "company": "Acme Corp", "plan": "business" }
  }
}
```

## Save a user

Creating and updating are the same call, so this is safe to run whenever your own record changes:

<CodeGroup>
  ```javascript Node.js theme={null}
  await client.profiles.create("user_123", {
    profile: {
      email: "sarah@acme-corp.com",
      phone_number: "+15551234567",
    },
  });
  ```

  ```python Python theme={null}
  client.profiles.create(
      "user_123",
      profile={
          "email": "sarah@acme-corp.com",
          "phone_number": "+15551234567",
      },
  )
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/profiles/user_123 \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "profile": {
        "email": "sarah@acme-corp.com",
        "phone_number": "+15551234567"
      }
    }'
  ```

  ```ruby Ruby theme={null}
  courier.profiles.create(
    "user_123",
    profile: {
      email: "sarah@acme-corp.com",
      phone_number: "+15551234567"
    }
  )
  ```

  ```go Go theme={null}
  _, err := client.Profiles.New(context.TODO(), "user_123", courier.ProfileNewParams{
  	Profile: map[string]any{
  		"email":        "sarah@acme-corp.com",
  		"phone_number": "+15551234567",
  	},
  })
  ```

  ```java Java theme={null}
  ProfileCreateParams params = ProfileCreateParams.builder()
      .userId("user_123")
      .profile(ProfileCreateParams.Profile.builder()
          .putAdditionalProperty("email", JsonValue.from("sarah@acme-corp.com"))
          .putAdditionalProperty("phone_number", JsonValue.from("+15551234567"))
          .build())
      .build();

  client.profiles().create(params);
  ```

  ```php PHP theme={null}
  $client->profiles->create(
    'user_123',
    profile: [
      'email' => 'sarah@acme-corp.com',
      'phone_number' => '+15551234567',
    ],
  );
  ```

  ```csharp C# theme={null}
  await client.Profiles.Create(
      "user_123",
      new()
      {
          Profile = new Dictionary<string, JsonElement>
          {
              { "email", JsonSerializer.SerializeToElement("sarah@acme-corp.com") },
              { "phone_number", JsonSerializer.SerializeToElement("+15551234567") },
          },
      }
  );
  ```

  ```bash CLI theme={null}
  courier profiles create user_123 \
    --api-key "$COURIER_API_KEY" \
    --profile '{"email": "sarah@acme-corp.com", "phone_number": "+15551234567"}'
  ```

  ```text MCP theme={null}
  With Courier MCP, save user_123 with the email sarah@acme-corp.com and the phone number +15551234567.
  ```
</CodeGroup>

The `user_id` is yours to choose. Use whatever your own system already calls that person, so you never keep a mapping table.

## What each channel needs

One field per channel, and Courier picks the right one per send:

| Channel  | Field on the profile                                     | Example                                                                                      |
| :------- | :------------------------------------------------------- | :------------------------------------------------------------------------------------------- |
| Email    | `email`                                                  | `sarah@acme-corp.com`                                                                        |
| SMS      | `phone_number`, in E.164                                 | `+15551234567`                                                                               |
| WhatsApp | `phone_number`, in E.164                                 | `+15551234567`                                                                               |
| Push     | none. Device tokens are stored outside the profile       | Registered by the <Doc href="/docs/sdk-libraries/sdks-overview">mobile SDK</Doc> or the Token API |
| Inbox    | none. The `user_id` is the address                       |                                                                                              |
| Chat     | the provider's own object, such as `slack` or `ms_teams` | `{ "access_token": "xoxb-…", "email": "…" }`                                                 |

A profile holds every field at once. Save `email` and `phone_number` together and one template can reach either channel without a second write.

Each category introduction carries its own providers' fields: <Doc href="/docs/integrations/email/overview">email</Doc>, <Doc href="/docs/integrations/sms/overview">SMS</Doc>, <Doc href="/docs/integrations/push/overview">push</Doc>, and <Doc href="/docs/integrations/direct-message/overview">chat</Doc>.

## Send by user id

Once the profile exists, a send names the person and nothing else:

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

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

  ```bash cURL highlight={6} 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={3} theme={null}
  response = courier.send_.message(
    message: {
      to: {user_id: "user_123"},
      template: "nt_01kx4h2jdafq8bk9aftxak4b40"
    }
  )
  ```

  ```go Go highlight={4} 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.Message message = SendMessageParams.Message.builder()
      .to(SendMessageParams.Message.To.ofUserRecipient(
          UserRecipient.builder().userId("user_123").build()))
      .template("nt_01kx4h2jdafq8bk9aftxak4b40")
      .build();

  client.send().message(SendMessageParams.builder().message(message).build());
  ```

  ```php PHP highlight={3} 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} 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 welcome template to user_123.
  ```
</CodeGroup>

Which channel it goes out on is the template's and <Doc href="/docs/send/routing">routing</Doc>'s decision, not the recipient's.

## Address someone you have not saved

An address in the `to` object sends with no stored profile. It reaches the recipient and leaves nothing behind, so there is no profile to update later and no preferences to respect. A [saved user](#send-by-user-id) stays the norm, because preferences, lists, tenants, and the inbox all key off one.

Each channel takes its own field or object:

<Tabs>
  <Tab title="Email">
    ```json theme={null}
    { "to": { "email": "sarah@acme-corp.com" } }
    ```

    Every <Doc href="/docs/integrations/email/overview">email provider</Doc> reads this field.
  </Tab>

  <Tab title="SMS">
    ```json theme={null}
    { "to": { "phone_number": "+15551234567" } }
    ```

    E.164 only. Every <Doc href="/docs/integrations/sms/overview">SMS provider</Doc> reads it, and <Doc href="/docs/integrations/direct-message/whatsapp">WhatsApp</Doc> reads the same field.
  </Tab>

  <Tab title="Push">
    ```json theme={null}
    { "to": { "apn": { "token": "YOUR_APNS_TOKEN" } } }
    ```

    One key per provider: `apn`, `firebase-fcm`, or `expo`. Each takes a `token` or a `tokens` array. See <Doc href="/docs/integrations/push/overview">push providers</Doc>.
  </Tab>

  <Tab title="Slack">
    ```json theme={null}
    { "to": { "slack": { "access_token": "xoxb-xxxxx", "email": "sarah@acme-corp.com" } } }
    ```

    Address by `email`, `channel`, or `user_id`, always with the `access_token`. See <Doc href="/docs/integrations/direct-message/slack">Slack</Doc>.
  </Tab>

  <Tab title="Microsoft Teams">
    ```json theme={null}
    { "to": { "ms_teams": { "tenant_id": "YOUR_TEAMS_TENANT_ID", "service_url": "https://smba.trafficmanager.net/amer/", "user_id": "TEAMS_USER_ID" } } }
    ```

    `tenant_id` and `service_url` are always required. Then one of `user_id`, `email`, `channel_id`, `channel_name`, or `conversation_id`. See <Doc href="/docs/integrations/direct-message/microsoft-teams">Microsoft Teams</Doc>.
  </Tab>

  <Tab title="Discord">
    ```json theme={null}
    { "to": { "discord": { "channel_id": "DISCORD_CHANNEL_ID" } } }
    ```

    A `channel_id` for a channel, or a `user_id` for a direct message. See <Doc href="/docs/integrations/direct-message/discord">Discord</Doc>.
  </Tab>

  <Tab title="Webhook">
    ```json theme={null}
    { "to": { "webhook": { "url": "https://api.acme-corp.com/courier-events" } } }
    ```

    Takes `method`, `headers`, and an `authentication` object as well. See <Doc href="/docs/integrations/other/webhook-integration">Webhook</Doc>.
  </Tab>

  <Tab title="Inbox">
    ```json theme={null}
    { "to": { "user_id": "user_123" } }
    ```

    The inbox has no direct form. The `user_id` is the address, because that is who the SDK signs in as. See <Doc href="/docs/in-app/overview">Inbox</Doc>.
  </Tab>
</Tabs>

<Note>
  **The two combine.** Courier merges an inline value over the saved profile **for that message only**, so this sends to the override address and never changes the profile:

  ```json theme={null}
  { "to": { "user_id": "user_123", "email": "billing@acme-corp.com" } }
  ```
</Note>

## When to write a profile

**On signup**, in the same handler that creates the user in your own database. <Guide href="/docs/guides/notify-from-clerk">Clerk</Guide>, <Guide href="/docs/guides/notify-from-supabase">Supabase</Guide>, <Guide href="/docs/guides/notify-from-firebase-auth">Firebase Auth</Guide>, and <Guide href="/docs/guides/notify-from-auth0">Auth0</Guide> each show that call in place.

**Whenever an address changes.** `POST` merges, so writing one field leaves the rest alone.

**Not on every send.** A profile write per send costs a round trip and buys nothing, because the profile is already there.

## The profile object

A profile accepts three kinds of field:

* **OpenID Connect standard claims.** `email`, `phone_number`, `name`, `given_name`, `family_name`, `locale`, `zoneinfo`, `birthdate`, and more.
* **`custom`.** A free-form object for anything specific to your app.
* **Provider-specific keys.** `apn`, `airship`, and others, for inline channel tokens.

```json theme={null}
{
  "profile": {
    "name": "Sarah Bennett",
    "email": "sarah@acme-corp.com",
    "phone_number": "+15551234567",
    "locale": "en-US",
    "custom": {
      "company": "Acme Corp",
      "plan": "business",
      "role": "admin"
    }
  }
}
```

Every profile field is available in templates. Reference top-level fields as `{profile.name}` and custom data as `{profile.custom.company}`.

### Merge, replace, or patch

The Profiles API has four operations on `/profiles/{user_id}`. They differ in what happens to fields you leave out:

| Operation      | Method   | Effect on omitted fields                                                       |
| -------------- | -------- | ------------------------------------------------------------------------------ |
| Merge / create | `POST`   | Preserved. Supplied values are merged into the existing profile, or create it. |
| Replace        | `PUT`    | Removed. The profile becomes exactly what you send.                            |
| Patch          | `PATCH`  | Untouched. Applies a list of JSON-patch operations (`op`, `path`, `value`).    |
| Delete         | `DELETE` | The whole profile is removed.                                                  |

Use `POST` for everyday updates so you never drop data. Use `PUT` only for a deliberate full overwrite. The Node SDK names these `profiles.create`, `profiles.replace`, `profiles.update` for patch, and `profiles.delete`.

### Device tokens

Push reaches a device, not an address, so a user carries a device token per device they sign in on. The Courier mobile SDKs register and refresh those tokens for you, on iOS, Android, Flutter, and React Native.

<Guide href="/docs/guides/set-up-mobile-push">Set up push notifications</Guide> walks the whole thing: a provider, the SDK, and a test push to your own device.

Tokens attach to the `user_id` rather than to the profile body, so Courier expires a dead one without touching the profile. A backend that manages tokens itself writes them with 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>.

## Limits & behavior

* **A send resolves an existing profile, it does not create one.** To reach a user by `user_id`, save the profile first or include inline contact details on that send. In-app recipients are the exception. Signing a user in to the Inbox registers them, so you can send to a user who exists only through <Doc href="/docs/in-app/authenticate-users">Inbox authentication</Doc>.
* **No endpoint lists every user.** You retrieve profiles one at a time by known `user_id`. Keep your own system of record as the source of truth.
* **`custom` is free-form, standard claims are typed.** Store app-specific data under `custom`. Courier accepts top-level keys outside the OIDC claim set, but they are clearest in `custom`.
* **No documented rate limit on the Profiles API.** The <Endpoint method="POST" path="/send" name="Send a message" href="/docs/api-reference/send/send-a-message">Send API enforces a rate limit</Endpoint>, but profile reads and writes have no published per-second cap. For a large sync, throttle your loop or import a CSV in the console.
* **Device tokens are not part of the profile body.** Writing a profile with `PUT` does not remove stored device tokens.

## FAQ

<AccordionGroup>
  <Accordion title="What should I use for the user_id?">
    The id your own system already uses for that person. Courier accepts any string, so matching your database means no mapping table and no second lookup.
  </Accordion>

  <Accordion title="Does sending to a new user_id create the profile?">
    A send looks up an existing profile, it does not persist one from the `to` object. Create the profile with <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile" /> first, or pass inline contact details for a one-off. The Inbox is the exception. Authenticating a user registers them.
  </Accordion>

  <Accordion title="Does saving a profile twice create two users?">
    `user_id` is the key, so a second write to the same id updates that user. <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile" /> is an upsert.
  </Accordion>

  <Accordion title="Can one user hold email, phone, and a Slack token at once?">
    One profile holds every address at once, which is the point. Save every address you have and the same send reaches whichever channel the template and <Doc href="/docs/send/routing">routing</Doc> pick.
  </Accordion>

  <Accordion title="Where do push device tokens get saved?">
    The mobile SDKs register them on `signIn`, or your backend writes them with <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" />. Either way they attach to the `user_id` rather than to the profile body. See <Guide href="/docs/guides/set-up-mobile-push">Set up mobile push</Guide>.
  </Accordion>

  <Accordion title="Can I export or dump all of my users?">
    Courier has no endpoint to list every `user_id`, export all profiles, or return a total user count. Retrieve profiles one at a time with <Endpoint method="GET" path="/profiles/{user_id}" name="Get a Profile" href="/docs/api-reference/user-profiles/get-a-profile" />. List the users in one tenant with <Endpoint method="GET" path="/tenants/{tenant_id}/users" name="Get users in Tenant" href="/docs/api-reference/tenants/get-users-in-tenant" />.
  </Accordion>

  <Accordion title="Is 'recipient' the same as 'user'?">
    Courier uses them interchangeably. A recipient identified by `user_id` is a stored user profile.
  </Accordion>

  <Accordion title="What is the difference between POST and PUT on a profile?">
    `POST` merges, so fields you omit are preserved. `PUT` replaces, so the profile becomes exactly the object you send and omitted fields are removed. Use `POST` for partial updates, `PUT` only for a deliberate full overwrite.
  </Accordion>

  <Accordion title="How do I store data that is not an OIDC claim?">
    Put it under `custom`. It is free-form, and every key is available in templates as `{profile.custom.plan}`.
  </Accordion>
</AccordionGroup>
