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

# Migrate from OneSignal

> Map OneSignal messages, segments, and players to Courier, with an API mapping and a plan.

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

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

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

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

This guide maps OneSignal concepts to Courier and gives you a step-by-step migration plan.

## Map OneSignal concepts to Courier

| OneSignal                                                                  | Courier                                                                                          |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [Apps](https://documentation.onesignal.com/docs/en/apps-organizations)     | <Doc href="/docs/workspaces/overview">Workspaces</Doc>                                                |
| [Segments](https://documentation.onesignal.com/docs/en/segmentation)       | <Doc href="/docs/recipients/lists-and-audiences/audiences">Audiences</Doc>                            |
| [Templates](https://documentation.onesignal.com/docs/en/templates)         | <Doc href="/docs/design/templates/overview">Templates</Doc>                                           |
| [Journeys](https://documentation.onesignal.com/docs/journeys-overview)     | <Doc href="/docs/journeys/overview">Journeys</Doc>                                                    |
| [Subscriptions](https://documentation.onesignal.com/docs/en/subscriptions) | Channel tokens on <Doc href="/docs/recipients/overview">User Profiles</Doc>                           |
| [Users / Aliases](https://documentation.onesignal.com/docs/en/aliases)     | <Doc href="/docs/recipients/overview">Users / Profiles</Doc>                                          |
| [Tags](https://documentation.onesignal.com/docs/en/add-user-data-tags)     | Custom properties on profiles + <Doc href="/docs/recipients/preferences/overview">Preferences</Doc>   |
| [Outcomes](https://documentation.onesignal.com/docs/custom-outcomes)       | <Doc href="/docs/monitor/overview">Analytics</Doc> + <Doc href="/docs/monitor/overview">Message Logs</Doc> |
| In-App Messages                                                            | <Doc href="/docs/in-app/overview">Inbox</Doc>                                                         |

### Apps and workspaces

OneSignal [apps](https://documentation.onesignal.com/docs/en/apps-organizations) map to Courier <Doc href="/docs/workspaces/overview">workspaces</Doc>. Each workspace has its own integrations, templates, and user profiles, plus separate Test and Production <Doc href="/docs/workspaces/overview#environments-and-api-keys">environments</Doc>.

### Segments and audiences

OneSignal [segments](https://documentation.onesignal.com/docs/en/segmentation) filter on user attributes, activity, and tags to build dynamic groups. Courier <Doc href="/docs/recipients/lists-and-audiences/audiences">Audiences</Doc> work the same way. Define filter rules on profile attributes and Courier keeps membership current as profiles change.

For static groups like mailing lists or beta testers, use <Endpoint method="GET" path="/lists" name="List Lists" href="/docs/api-reference/lists/list-lists">Lists</Endpoint>.

### Templates

OneSignal [templates](https://documentation.onesignal.com/docs/en/templates) are reusable message blueprints for push, email, and SMS. One Courier <Doc href="/docs/design/templates/overview">template</Doc> holds content for every channel: email, SMS, push, chat, and inbox.

<Doc href="/docs/design/templates/design-studio">Design Studio</Doc> uses drag-and-drop content blocks. <Doc href="/docs/design/elemental/overview">Elemental</Doc> JSON is the code-first path. Both support <Doc href="/docs/design/templates/variables">`{{variable}}`</Doc> Handlebars personalization, channel-specific overrides, and <Doc href="/docs/design/elemental/locales">localization</Doc>.

OneSignal uses Liquid syntax. Courier uses Handlebars. Both have conditionals, loops, and variable interpolation, with different syntax.

### Journeys

OneSignal [Journeys](https://documentation.onesignal.com/docs/journeys-overview) are automated multi-step message flows. Courier <Doc href="/docs/journeys/overview">Journeys</Doc> do the same, with <Doc href="/docs/journeys/nodes/delay">delays</Doc>, <Doc href="/docs/journeys/nodes/branch">branching</Doc>, <Doc href="/docs/journeys/nodes/batch">batching</Doc>, <Doc href="/docs/journeys/nodes/digest">digests</Doc>, and <Doc href="/docs/journeys/nodes/cancel">cancellation</Doc>.

| Journey Step       | Courier Equivalent                                                       |
| ------------------ | ------------------------------------------------------------------------ |
| Send message       | <Doc href="/docs/journeys/nodes/send">Send node</Doc> (references a template) |
| Wait               | <Doc href="/docs/journeys/nodes/delay">Delay node</Doc>                       |
| Yes/No branch      | <Doc href="/docs/journeys/nodes/branch">Branch node</Doc>                     |
| Multi-split branch | <Doc href="/docs/journeys/nodes/branch">Branch node</Doc> with multiple paths |

A journey starts from a <Doc href="/docs/journeys/invoke">trigger</Doc>: an API invoke, an inbound webhook, a Segment event, or a user joining an audience. Build journeys visually <Doc href="/docs/journeys/build">in the UI</Doc> or define them through the <Doc href="/docs/journeys/build">Journeys API</Doc>.

### Users, subscriptions, and profiles

OneSignal separates [users](https://documentation.onesignal.com/docs/en/aliases) (identified by aliases) from [subscriptions](https://documentation.onesignal.com/docs/en/subscriptions) (individual devices, email addresses, phone numbers). One Courier <Doc href="/docs/recipients/overview">profile</Doc> per user holds all of it: email, phone, push tokens, and custom properties.

Profiles accept nested JSON, so account tiers, team roles, or feature flags fit as they are. Create profiles ahead of time through the API, or identify users inline at send time.

### Tags and preferences

OneSignal [tags](https://documentation.onesignal.com/docs/en/add-user-data-tags) are key-value pairs used for both targeting and preference management. Courier splits these in two:

* **Custom profile properties**: any attribute on a user profile, for targeting and personalization
* **<Doc href="/docs/recipients/preferences/overview">Preferences</Doc>**: opt-in and opt-out by channel, category, or topic, enforced at send time

### In-app messages and Inbox

OneSignal's in-app messages are modal overlays triggered by user activity. Courier <Doc href="/docs/in-app/overview">Inbox</Doc> is a persistent in-app notification center with drop-in components for <Doc href="/docs/sdk-libraries/courier-react-web">React</Doc>, <Doc href="/docs/sdk-libraries/ios">iOS</Doc>, <Doc href="/docs/sdk-libraries/android">Android</Doc>, and <Doc href="/docs/sdk-libraries/courier-js-web">vanilla JS</Doc>.

Inbox runs on the same delivery pipeline as email, push, and SMS, with no separate provider to configure. You get read/unread state, archiving, per-user history, <Doc href="/docs/in-app/add-toasts">toast notifications</Doc>, and <Doc href="/docs/in-app/customize-the-inbox">tab-based organization</Doc>.

### Outcomes and analytics

OneSignal [Outcomes](https://documentation.onesignal.com/docs/custom-outcomes) track conversion events tied to notifications. Courier gives you <Doc href="/docs/monitor/overview">Message Logs</Doc> (per-message timeline from API request to provider delivery) and <Doc href="/docs/monitor/overview">Analytics</Doc> (aggregate delivery and engagement metrics). To pipe events into your own analytics, configure <Doc href="/docs/monitor/webhooks/outbound">Outbound Webhooks</Doc>.

## Why Courier

* **Multi-channel.** Courier treats email, SMS, push, chat, webhooks, and in-app as equal channels with one orchestration layer. You're not bolting non-push channels onto a push-first platform.
* **Content and logic stay separate.** Templates and journeys are independent resources. Your product team updates copy in Design Studio while engineers tune journey timing. Neither blocks the other.
* **Design Studio.** Build email, SMS, push, and chat content with drag-and-drop blocks. Preview across channels, personalize with variables, and publish without deploying code.
* **Automatic failover.** Configure multiple providers per channel and Courier fails over automatically. If your primary email provider goes down, traffic shifts to the backup without code changes.
* **Built-in in-app channel.** Courier Inbox works without a third-party provider. Drop in a React, iOS, or Android component and deliver in-app notifications on the same pipeline as email and push.
* **Hosted preferences.** Ship a user-facing preference center with a single config, or embed React components in your app. No custom UI required.
* **The providers you already use.** Email, SMS, push, chat, webhooks, CDPs, and observability tools. Switch providers without changing your send code.
* **Full delivery observability.** <Doc href="/docs/monitor/overview">Message Logs</Doc> track every message from API request to provider delivery with a detailed timeline, error details, and rendered content inspection.

## Migrate step by step

<Steps>
  <Step title="Create your Courier workspace">
    <AppLink href="https://app.courier.com/signup">Sign up</AppLink> and create a workspace. The Test and Production <Doc href="/docs/workspaces/overview#environments-and-api-keys">environments</Doc> have independent API keys, so you can migrate without touching live traffic.
  </Step>

  <Step title="Configure integrations">
    Go to **Integrations** in your Courier dashboard and connect a provider for each channel you use in OneSignal. If you send push via FCM and APNs, configure both. Connect your email and SMS providers (SendGrid, Twilio, etc.). Multiple providers on one channel gives you <Doc href="/docs/send/routing">failover</Doc>.

    For in-app notifications, enable <Doc href="/docs/in-app/overview">Courier Inbox</Doc>. No external provider needed.
  </Step>

  <Step title="Recreate templates">
    OneSignal templates are per-channel. In Courier, a single <Doc href="/docs/design/templates/overview">template</Doc> holds content for all channels:

    1. Create a new template for each notification type
    2. Add content blocks for each channel (email body, SMS text, push title/body, etc.)
    3. Convert Liquid variables to Handlebars (`{{variable}}`). The data comes from the send request's `data` field
    4. Publish the template so you can send it
  </Step>

  <Step title="Migrate user data">
    Create Courier profiles that consolidate your OneSignal user and subscription data. Use the <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile">Profiles API</Endpoint>, or identify users inline at send time.

    ```json theme={null}
    {
      "user_id": "user_123",
      "profile": {
        "email": "sarah@acme-corp.com",
        "phone_number": "+15551234567",
        "firebaseToken": "fcm_device_token_here",
        "custom": {
          "name": "Sarah Bennett",
          "plan": "enterprise"
        }
      }
    }
    ```
  </Step>

  <Step title="Set up preferences">
    If you use OneSignal tags for opt-in and opt-out, rebuild that structure with Courier <Doc href="/docs/recipients/preferences/overview">Preferences</Doc>:

    1. Define <Doc href="/docs/recipients/preferences/overview">subscription topics</Doc> for each notification category
    2. Configure default channel routing per topic
    3. Migrate user preference selections via the <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences">Preferences API</Endpoint>

    Courier ships a <Guide href="/docs/guides/build-a-preference-center#hosted-page">hosted preference page</Guide> you can deploy immediately, or <Guide href="/docs/guides/build-a-preference-center#embedded-component">React components</Guide> to embed preferences in your app.
  </Step>

  <Step title="Update your send calls">
    Replace OneSignal's Create Notification API calls with Courier's <Doc href="/docs/send/overview">Send API</Doc>:

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { requestId } = await client.send.message({
        message: {
          to: { user_id: 'user_123' },
          template: 'nt_01kx4h2jdafq8bk9aftxak4b40',
          data: {
            order_id: 'ORD-456',
            total: 79.99,
          },
        },
      });
      ```

      ```python Python theme={null}
      response = client.send.message(
          message={
              "to": {"user_id": "user_123"},
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {
                  "order_id": "ORD-456",
                  "total": 79.99,
              },
          },
      )
      ```

      ```bash cURL 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",
            "data": {
              "order_id": "ORD-456",
              "total": 79.99
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.send_.message(
        message: {
          to: { user_id: "user_123" },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: { order_id: "ORD-456", total: 79.99 }
        }
      )
      ```

      ```go Go 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"),
      		Data: map[string]any{
      			"order_id": "ORD-456",
      			"total": 79.99,
      		},
      	},
      })
      ```

      ```java Java theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(JsonValue.from(java.util.Map.of("user_id", "user_123")))
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .data(JsonValue.from(java.util.Map.of(
                  "order_id", "ORD-456",
                  "total", 79.99)))
              .build())
          .build();
      client.send().message(params);
      ```

      ```php PHP theme={null}
      $response = $client->send->message(
        message: [
          'to' => ['userID' => 'user_123'],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => ['order_id' => 'ORD-456', 'total' => 79.99],
        ],
      );
      ```

      ```csharp C# theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new Dictionary<string, JsonElement>()
              {
                  { "order_id", JsonSerializer.SerializeToElement("ORD-456") },
                  { "total", JsonSerializer.SerializeToElement(79.99) },
              },
          },
      };

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

      ```bash CLI theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message '{"to":{"user_id":"user_123"},"template":"nt_01kx4h2jdafq8bk9aftxak4b40","data":{"order_id":"ORD-456","total":79.99}}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 with the order details.
      ```
    </CodeGroup>

    Courier applies routing, preferences, and failover from your template and workspace configuration. For multi-channel sends, set the `routing` field:

    <CodeGroup>
      ```javascript Node.js highlight={7} theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            user_id: "user_123",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          routing: {
            method: "single",
            channels: [
              "push",
              "email",
              "sms",
            ],
          },
        },
      });
      ```

      ```python Python highlight={7} theme={null}
      response = client.send.message(
          message={
              "to": {
                  "user_id": "user_123",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "routing": {
                  "method": "single",
                  "channels": [
                      "push",
                      "email",
                      "sms",
                  ],
              },
          },
      )
      ```

      ```bash cURL highlight={10} wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "user_id": "user_123"
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "routing": {
              "method": "single",
              "channels": [
                "push",
                "email",
                "sms"
              ]
            }
          }
        }'
      ```

      ```ruby Ruby highlight={7} theme={null}
      response = courier.send_.message(
        message: {
          to: {
            user_id: "user_123"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          routing: {
            method: "single",
            channels: [
              "push",
              "email",
              "sms"
            ]
          }
        }
      )
      ```

      ```go Go highlight={9} theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				UserID: courier.String("user_123"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Routing: courier.SendMessageParamsMessageRouting{
      			Channels: []shared.MessageRoutingChannelUnionParam{
      				{OfString: courier.String("push")},
      				{OfString: courier.String("email")},
      				{OfString: courier.String("sms")},
      			},
      			Method: "single",
      		},
      	},
      })
      ```

      ```java Java highlight={5} theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().userId("user_123").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .routing(SendMessageParams.Message.Routing.builder()
                  .addChannel("push")
                  .addChannel("email")
                  .addChannel("sms")
                  .method(SendMessageParams.Message.Routing.Method.SINGLE)
                  .build())
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP highlight={7} theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'user_id' => 'user_123',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'routing' => [
            'method' => 'single',
            'channels' => [
              'push',
              'email',
              'sms',
            ],
          ],
        ],
      );
      ```

      ```csharp C# highlight={7} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Routing = new() { Channels = ["push", "email", "sms"], Method = Send::Method.Single },
          },
      };

      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.routing '{"method": "single", "channels": ["push", "email", "sms"]}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 over email and push.
      ```
    </CodeGroup>

    This tries push first, then email, then SMS. The first successful delivery wins.
  </Step>

  <Step title="Recreate journeys">
    If you use OneSignal Journeys, recreate them in Courier <Doc href="/docs/journeys/overview">Journeys</Doc>, visually or through the <Doc href="/docs/journeys/build">Journeys API</Doc>.

    For notifications that need no orchestration, skip journeys and send directly via the Send API.
  </Step>

  <Step title="Test and cut over">
    1. Send test messages in your Test environment and verify delivery in <AppLink href="https://app.courier.com/logs">Message Logs</AppLink>
    2. Check that preferences, routing, and template rendering match your OneSignal setup
    3. Switch your production code to Courier's Production API key
    4. Monitor <Doc href="/docs/monitor/overview">Message Logs</Doc> and <Doc href="/docs/monitor/overview">Analytics</Doc> for delivery confirmation
  </Step>
</Steps>

## Migrate with an AI agent

Most of this migration is mechanical: read a template definition, create the equivalent Courier template, rewrite the call site. That suits a coding agent well.

Three things to set up first:

* **<Doc href="/docs/resources/skills">Courier Skills</Doc>** teaches the agent Courier's real APIs and patterns so it stops guessing at endpoints:

  ```bash theme={null}
  npx skills add trycourier/courier-skills
  ```

  In Claude Code you can install it as a plugin instead, which self-updates and ships the Courier docs MCP server.

* **<Doc href="/docs/resources/mcp">Courier's MCP server</Doc>** gives the agent direct access to your workspace, so it can create templates, build journeys, upsert users, and read send logs. See <Doc href="/docs/get-started/build-with-ai">Build with AI</Doc> for setup in Claude Code, Cursor, Codex, and VS Code.

* **Docs for agents.** Point it at [llms-full.txt](https://www.courier.com/docs/llms-full.txt) when it needs API detail beyond this page.

Then start it with a prompt like this one. Fill in the bracketed parts.

```text theme={null}
Help me migrate our notifications from OneSignal to Courier.

Read first:
- https://www.courier.com/docs/guides/migrate/from-onesignal
  (concept mapping and the migration steps you should follow)
- https://www.courier.com/docs/llms-full.txt
  (full Courier docs, for API detail as you need it)

Our setup:
- Codebase: [path]
- OneSignal API credentials: [env var]
- Courier: connected through the Courier MCP server

Work in this order. Stop after step 3 and wait for my review
before you create anything in Courier.

0. Setup. Install Courier Skills so you build against Courier's real
   APIs and patterns rather than guessing:
   npx skills add trycourier/courier-skills
   Then confirm the Courier MCP server is connected. If either one is
   missing, stop and tell me before going further.

1. Inventory. Use the OneSignal API to list every template, journey,
   and segment in each app. For each template record its channels; for
   each journey record its trigger, steps, and target segment.

2. Classify each as:
   - transactional (an action in our product must produce it)
   - marketing (campaign or lifecycle)
   - in-app messages (these are a separate surface, see the guide)

3. Find the send sites. Grep the codebase for OneSignal
   POST /notifications calls. Note which ones send inline content and
   which reference a template_id, and which target segments or filters
   rather than an external_id.

   Report the inventory, the classification, and the send-site list.
   Then wait.

4. Transactional first. For each one: create the Courier template with
   the channels it needs, then rewrite the call site to a Courier /send
   that names the template and passes data. Recipients addressed by
   external_id become a Courier user_id. Leave the OneSignal call in
   place behind a flag so we can run both and compare.

5. Journeys. Recreate each OneSignal journey through the Courier
   journeys API. Waits become delay nodes, branches become branch
   nodes, message steps become send nodes.

6. Segments and tags. Convert segments to Courier audiences built on
   profile attributes, and map the tags you use for opt-out into
   Courier subscription topics rather than leaving them as attributes.

Rules:
- Never send to a real user. Use a test profile in the Test
  environment for every verification send.
- Never pause, disable, or delete anything in OneSignal. That is my call.
- If something in OneSignal has no Courier equivalent, say so in your
  report rather than approximating it.
```

Step 6 deserves a careful pass. OneSignal data tags cover both segmentation and opt-out, and only the opt-out tags should become subscription topics. Anything that gates whether a user may be contacted belongs in Preferences, where Courier enforces it at send time.

## Map the API

| Operation            | OneSignal                                               | Courier                                                                                                                                            |
| -------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Send a notification  | `POST /notifications`                                   | <Endpoint method="POST" path="/send" name="Send a message" href="/docs/api-reference/send/send-a-message" />                                            |
| Create/update a user | `POST /apps/:id/users`                                  | <Endpoint method="PUT" path="/profiles/{user_id}" name="Replace a Profile" href="/docs/api-reference/user-profiles/replace-a-profile" />                |
| Get a user           | `GET /apps/:id/users/by/external_id/:id`                | <Endpoint method="GET" path="/profiles/{user_id}" name="Get a Profile" href="/docs/api-reference/user-profiles/get-a-profile" />                        |
| Add a subscription   | `POST /apps/:id/users/by/external_id/:id/subscriptions` | Push tokens on <Endpoint method="PUT" path="/profiles/{user_id}" name="Replace a Profile" href="/docs/api-reference/user-profiles/replace-a-profile" /> |
| Manage segments      | `POST /apps/:id/segments`                               | <Endpoint method="PUT" path="/audiences/{audience_id}" name="Update an Audience" href="/docs/api-reference/audiences/update-an-audience" />             |
| Get message status   | `GET /notifications/:id`                                | <Endpoint method="GET" path="/messages/{message_id}" name="Get message" href="/docs/api-reference/messages/get-message" />                              |
| Bulk operations      | `POST /notifications` (with `include_aliases`)          | <Endpoint method="POST" path="/bulk" name="Create a bulk job" href="/docs/api-reference/bulk/create-a-bulk-job" />                                      |
| Create a template    | `POST /templates`                                       | <Doc href="/docs/design/templates/design-studio">Design Studio</Doc> or <Doc href="/docs/design/elemental/overview">Elemental API</Doc>                      |
