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

# Import your users

> Create or update profiles one at a time or from a CSV, and delete them.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

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

<Tags items="Users" />

Get your users into Courier so a send only needs their `user_id`.

A profile stores one person's contact details. Load profiles from a CSV or your own database, then update them as your system of record changes.

<Doc href="/docs/recipients/overview">Users</Doc> covers one profile and the field each channel reads. This guide is the same call at the scale of your whole user table.

## Prerequisites

* <AppLink href="https://app.courier.com/">A Courier account</AppLink>
* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>
* <Doc href="/docs/recipients/overview#what-each-channel-needs">Which field addresses which channel</Doc>

The examples assume a client initialized once with your key from the environment:

<CodeGroup>
  ```javascript Node.js theme={null}
  import Courier from '@trycourier/courier';

  const client = new Courier({
    apiKey: process.env['COURIER_API_KEY'],
  });
  ```

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

  from courier import Courier

  client = Courier(
      api_key=os.environ.get("COURIER_API_KEY"),
  )
  ```

  ```bash cURL theme={null}
  export COURIER_API_KEY="YOUR_COURIER_API_KEY"
  ```

  ```ruby Ruby theme={null}
  require "courier"

  courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])
  ```

  ```go Go theme={null}
  client := courier.NewClient(
  	option.WithAPIKey(os.Getenv("COURIER_API_KEY")),
  )
  ```

  ```java Java theme={null}
  CourierClient client = CourierOkHttpClient.fromEnv();
  ```

  ```php PHP theme={null}
  $client = new Client(apiKey: getenv('COURIER_API_KEY'));
  ```

  ```csharp C# theme={null}
  CourierClient client = new();
  ```

  ```bash CLI theme={null}
  export COURIER_API_KEY="YOUR_COURIER_API_KEY"
  ```
</CodeGroup>

## Create or update a profile

<Steps>
  <Step title="Create or update a profile">
    Creating and updating are the same call. <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile" /> merges the values you send into the profile, creating it if it does not exist and keeping any fields you leave out. The `user_id` in the path is the identifier you send to. You choose it.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const profile = await client.profiles.create('user_123', {
        profile: {
          email: 'sarah@acme-corp.com',
          phone_number: '+15551234567',
          name: 'Sarah Bennett',
          locale: 'en-US',
          custom: { company: 'Acme Corp', plan: 'business' },
        },
      });
      ```

      ```python Python theme={null}
      profile = client.profiles.create(
          user_id="user_123",
          profile={
              "email": "sarah@acme-corp.com",
              "phone_number": "+15551234567",
              "name": "Sarah Bennett",
              "locale": "en-US",
              "custom": {"company": "Acme Corp", "plan": "business"},
          },
      )
      ```

      ```bash cURL 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": {
            "email": "sarah@acme-corp.com",
            "phone_number": "+15551234567",
            "name": "Sarah Bennett",
            "locale": "en-US",
            "custom": { "company": "Acme Corp", "plan": "business" }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      profile = courier.profiles.create(
        "user_123",
        profile: {
          email: "sarah@acme-corp.com",
          phone_number: "+15551234567",
          name: "Sarah Bennett",
          locale: "en-US",
          custom: {company: "Acme Corp", plan: "business"}
        }
      )
      ```

      ```go Go theme={null}
      profile, err := client.Profiles.New(
      	context.TODO(),
      	"user_123",
      	courier.ProfileNewParams{
      		Profile: map[string]any{
      			"email":        "sarah@acme-corp.com",
      			"phone_number": "+15551234567",
      			"name":         "Sarah Bennett",
      			"locale":       "en-US",
      			"custom": map[string]any{
      				"company": "Acme Corp",
      				"plan":    "business",
      			},
      		},
      	},
      )
      ```

      ```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"))
              .putAdditionalProperty("name", JsonValue.from("Sarah Bennett"))
              .putAdditionalProperty("locale", JsonValue.from("en-US"))
              .putAdditionalProperty("custom", JsonValue.from(Map.of("company", "Acme Corp", "plan", "business")))
              .build())
          .build();
      ProfileCreateResponse profile = client.profiles().create(params);
      ```

      ```php PHP theme={null}
      $profile = $client->profiles->create('user_123', profile: [
        'email' => 'sarah@acme-corp.com',
        'phone_number' => '+15551234567',
        'name' => 'Sarah Bennett',
        'locale' => 'en-US',
        'custom' => ['company' => 'Acme Corp', 'plan' => 'business'],
      ]);
      ```

      ```csharp C# theme={null}
      ProfileCreateParams parameters = new()
      {
          UserID = "user_123",
          Profile = new Dictionary<string, JsonElement>()
          {
              { "email", JsonSerializer.SerializeToElement("sarah@acme-corp.com") },
              { "phone_number", JsonSerializer.SerializeToElement("+15551234567") },
              { "name", JsonSerializer.SerializeToElement("Sarah Bennett") },
              { "locale", JsonSerializer.SerializeToElement("en-US") },
              { "custom", JsonSerializer.SerializeToElement(new { company = "Acme Corp", plan = "business" }) },
          },
      };
      var profile = await client.Profiles.Create(parameters);
      ```

      ```bash CLI theme={null}
      courier profiles create \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123 \
        --profile '{"email":"sarah@acme-corp.com","phone_number":"+15551234567","name":"Sarah Bennett","locale":"en-US","custom":{"company":"Acme Corp","plan":"business"}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, create a profile for user_123 with their email, phone number, and name.
      ```
    </CodeGroup>

    <Note>
      **Device tokens are managed separately.**<br />
      Push device tokens live on the user, but go through their own API, not the profile body. See <Doc href="/docs/integrations/push/overview">Push providers</Doc> to register them.
    </Note>
  </Step>

  <Step title="Read the profile back">
    Fetch the profile and confirm the stored fields, or open the user in <AppLink href="https://app.courier.com/directory/users">Users</AppLink>:

    <CodeGroup>
      ```javascript Node.js theme={null}
      const profile = await client.profiles.retrieve('user_123');
      ```

      ```python Python theme={null}
      profile = client.profiles.retrieve("user_123")
      ```

      ```bash cURL theme={null}
      curl --request GET \
        --url https://api.courier.com/profiles/user_123 \
        --header "Authorization: Bearer $COURIER_API_KEY"
      ```

      ```ruby Ruby theme={null}
      profile = courier.profiles.retrieve("user_123")
      ```

      ```go Go theme={null}
      profile, err := client.Profiles.Get(context.TODO(), "user_123")
      ```

      ```java Java theme={null}
      ProfileRetrieveResponse profile = client.profiles().retrieve("user_123");
      ```

      ```php PHP theme={null}
      $profile = $client->profiles->retrieve('user_123');
      ```

      ```csharp C# theme={null}
      ProfileRetrieveParams parameters = new() { UserID = "user_123" };
      var profile = await client.Profiles.Retrieve(parameters);
      ```

      ```bash CLI theme={null}
      courier profiles retrieve \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123
      ```

      ```text MCP theme={null}
      With Courier MCP, show me the stored profile for user_123.
      ```
    </CodeGroup>
  </Step>

  <Step title="Replace instead of merge">
    Use `POST` for everyday updates so you never drop data. Use `PUT` when the profile should become exactly what you send, dropping any field you omit.

    <CodeGroup>
      ```javascript Node.js theme={null}
      const response = await client.profiles.replace('user_123', {
        profile: { email: 'sarah@acme-corp.com', name: 'Sarah Bennett' },
      });
      ```

      ```python Python theme={null}
      response = client.profiles.replace(
          user_id="user_123",
          profile={
              "email": "sarah@acme-corp.com",
              "name": "Sarah Bennett",
          },
      )
      ```

      ```bash cURL theme={null}
      curl --request PUT \
        --url https://api.courier.com/profiles/user_123 \
        --header "Authorization: Bearer $COURIER_API_KEY" \
        --header 'Content-Type: application/json' \
        --data '{
          "profile": {
            "email": "sarah@acme-corp.com",
            "name": "Sarah Bennett"
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.profiles.replace(
        "user_123",
        profile: {email: "sarah@acme-corp.com", name: "Sarah Bennett"}
      )
      ```

      ```go Go theme={null}
      response, err := client.Profiles.Replace(
      	context.TODO(),
      	"user_123",
      	courier.ProfileReplaceParams{
      		Profile: map[string]any{
      			"email": "sarah@acme-corp.com",
      			"name":  "Sarah Bennett",
      		},
      	},
      )
      ```

      ```java Java theme={null}
      ProfileReplaceParams params = ProfileReplaceParams.builder()
          .userId("user_123")
          .profile(ProfileReplaceParams.Profile.builder()
              .putAdditionalProperty("email", JsonValue.from("sarah@acme-corp.com"))
              .putAdditionalProperty("name", JsonValue.from("Sarah Bennett"))
              .build())
          .build();
      ProfileReplaceResponse response = client.profiles().replace(params);
      ```

      ```php PHP theme={null}
      $response = $client->profiles->replace('user_123', profile: [
        'email' => 'sarah@acme-corp.com',
        'name' => 'Sarah Bennett',
      ]);
      ```

      ```csharp C# theme={null}
      ProfileReplaceParams parameters = new()
      {
          UserID = "user_123",
          Profile = new Dictionary<string, JsonElement>()
          {
              { "email", JsonSerializer.SerializeToElement("sarah@acme-corp.com") },
              { "name", JsonSerializer.SerializeToElement("Sarah Bennett") },
          },
      };
      var response = await client.Profiles.Replace(parameters);
      ```

      ```bash CLI theme={null}
      courier profiles replace \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123 \
        --profile '{"email":"sarah@acme-corp.com","name":"Sarah Bennett"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, replace user_123's profile with just their email and name.
      ```
    </CodeGroup>

    <Warning>
      **Replacing removes omitted fields.**<br />
      After a `PUT`, any field not in the request (a `phone_number`, a `custom` value) is gone. Use `POST` unless you intend a full overwrite. To change one field, use `PATCH` with a JSON-patch operation.
    </Warning>
  </Step>
</Steps>

## Import users from a CSV

For a one-time load or an occasional top-up, import a CSV in the console. There is no CSV API. To load users programmatically, call the profile endpoints above for each user.

<Steps>
  <Step title="Open the importer">
    Go to <AppLink href="https://app.courier.com/directory/users">Users</AppLink> and click **Import**. Download the CSV template from the dialog so your columns match exactly.
  </Step>

  <Step title="Fill in the template">
    Only `id` is required. Keep the header row exactly as the template provides it: do not rename, reorder, or add columns. Columns include `id`, `name`, `first_name`, `last_name`, `email`, `phone_number`, `locale`, `zoneinfo`, and the OIDC standard claims.
  </Step>

  <Step title="Save as UTF-8 CSV and upload">
    Save as **CSV (comma delimited)**, UTF-8 without a BOM, and with no blank line after the last row. Upload the file and click **Add Users** to create the profiles.
  </Step>
</Steps>

<Accordion title="Fixing a 'Too few fields' import error">
  This error means the parser read a row as a single field, almost always a delimiter, encoding, or trailing-line problem:

  * **Wrong delimiter.** Open the file in a plain text editor. If fields are separated by semicolons or tabs (some spreadsheet locales use semicolons), re-save as CSV (comma delimited).
  * **Wrong encoding.** Garbled characters or `ÿþ` at the start mean the file is UTF-16. Recreate it as UTF-8. A UTF-8 BOM (`ï»¿` before the first field) can also interfere. Remove it.
  * **Trailing blank line.** A "Too few fields" error on the last row only usually means an empty final line. Delete any blank lines after the last data row.
  * **Extra columns.** If the expected field count is higher than the template's, your header has extra trailing commas. Remove them.

  The most reliable fix is to build the CSV in a plain text editor rather than a spreadsheet app.
</Accordion>

## Delete a profile

Deleting a profile removes all of its stored data. Sends to that `user_id` fail until a new profile exists.

<CodeGroup>
  ```javascript Node.js theme={null}
  await client.profiles.delete('user_123');
  ```

  ```python Python theme={null}
  client.profiles.delete("user_123")
  ```

  ```bash cURL theme={null}
  curl --request DELETE \
    --url https://api.courier.com/profiles/user_123 \
    --header "Authorization: Bearer $COURIER_API_KEY"
  ```

  ```ruby Ruby theme={null}
  courier.profiles.delete("user_123")
  ```

  ```go Go theme={null}
  err := client.Profiles.Delete(context.TODO(), "user_123")
  ```

  ```java Java theme={null}
  client.profiles().delete("user_123");
  ```

  ```php PHP theme={null}
  $client->profiles->delete('user_123');
  ```

  ```csharp C# theme={null}
  ProfileDeleteParams parameters = new() { UserID = "user_123" };
  await client.Profiles.Delete(parameters);
  ```

  ```bash CLI theme={null}
  courier profiles delete \
    --api-key "$COURIER_API_KEY" \
    --user-id user_123
  ```

  ```text MCP theme={null}
  With Courier MCP, delete the profile for user_123.
  ```
</CodeGroup>

## Verify

<Steps>
  <Step title="Read one profile back">
    Fetch a profile you just wrote and confirm every field you sent is there, spelled the way your templates expect.
  </Step>

  <Step title="Send to the user_id alone">
    Send a message addressed only by `user_id`, with no inline profile. Courier resolves the contact details from storage, which proves the import landed.
  </Step>

  <Step title="Check the count after a CSV import">
    Open <AppLink href="https://app.courier.com/directory/users">Users</AppLink> and confirm the number of profiles matches your file's row count. A short count usually means rows the parser rejected.
  </Step>
</Steps>
