# Courier > Courier is one API for email, SMS, push, chat, and in-app inbox, sending through the notification providers you already use. It adds templates, routing, preferences, and multi-step journeys, so your application makes one request instead of integrating each channel. > ## 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 `. 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` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). 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. > To translate a template, write one locale at a time with `PUT /notifications/{id}/locales/{localeId}`, which merges into the existing translations. `PUT /notifications/{id}/content` and `PUT /notifications/{id}/elements/{elementId}` overwrite translations along with the content, so send each element's `id` and `locales` back, and first remove every `checksum` and any `locales` key that starts with an underscore. > 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. # Delete an Audience Source: https://www.courier.com/docs/api-reference/audiences/delete-an-audience /openapi-specs/openapi.documented.yml delete /audiences/{audience_id} Deletes an audience permanently, so update any caller sending to it by audience id first. Those sends fail once the audience is gone. # Get an Audience Source: https://www.courier.com/docs/api-reference/audiences/get-an-audience /openapi-specs/openapi.documented.yml get /audiences/{audience_id} Returns one audience with its name, description, and the filter and AND or OR operator that decide which users belong to it. # List all Audiences Source: https://www.courier.com/docs/api-reference/audiences/list-all-audiences /openapi-specs/openapi.documented.yml get /audiences Returns the audiences in the workspace with paging. Audiences are filter-based groups that recalculate as user profiles change. # List Audience members Source: https://www.courier.com/docs/api-reference/audiences/list-audience-members /openapi-specs/openapi.documented.yml get /audiences/{audience_id}/members Returns the users currently matching an audience filter, with paging. Membership is recalculated, so results shift as profiles change. # Update an Audience Source: https://www.courier.com/docs/api-reference/audiences/update-an-audience /openapi-specs/openapi.documented.yml put /audiences/{audience_id} Creates or replaces an audience from a filter and an AND or OR operator. Membership recalculates automatically as profiles change. # Get an Audit Event Source: https://www.courier.com/docs/api-reference/audit-events/get-an-audit-event /openapi-specs/openapi.documented.yml get /audit-events/{audit-event-id} Returns one audit event by id, including the actor who performed it, the target they changed, the source, the event type, and a timestamp. # List Audit Events Source: https://www.courier.com/docs/api-reference/audit-events/list-audit-events /openapi-specs/openapi.documented.yml get /audit-events Returns the workspace's audit event log with cursor paging. Each event records the actor, target, source, type, and timestamp of a change. # Create a JWT Source: https://www.courier.com/docs/api-reference/authentication/create-a-jwt /openapi-specs/openapi.documented.yml post /auth/issue-token Returns a JWT for authenticating client-side SDKs such as the Inbox. You supply the scope and an expires_in duration, both required. # Invoke an ad hoc Automation Source: https://www.courier.com/docs/api-reference/automations/invoke-an-ad-hoc-automation /openapi-specs/openapi.documented.yml post /automations/invoke Runs a series of automation steps supplied inline, without a saved template, and returns a runId. # Invoke an Automation Source: https://www.courier.com/docs/api-reference/automations/invoke-an-automation /openapi-specs/openapi.documented.yml post /automations/{templateId}/invoke Starts an automation run from a saved template for one recipient, with optional data and profile, and returns a runId. # List Automation runs Source: https://www.courier.com/docs/api-reference/automations/list-automation-runs /openapi-specs/openapi.documented.yml get /automations/runs List runs of the workspace's v2 Automations, newest first, filtered by status, Template, or date range and paged by cursor. Journey (v3) runs are listed by `GET /journeys/runs` instead — the two surfaces never return each other's runs. Runs are retained for 95 days. # List Automations Source: https://www.courier.com/docs/api-reference/automations/list-automations /openapi-specs/openapi.documented.yml get /automations Lists the workspace's saved automation templates, each with its id and a cursor for paging to the next page of results. # List steps for an Automation run Source: https://www.courier.com/docs/api-reference/automations/list-steps-for-an-automation-run /openapi-specs/openapi.documented.yml get /automations/runs/{id}/steps List the per-step state of one Automation run, in full — this endpoint is not paginated. `message_id` is present on send steps that produced a message; follow it to `GET /messages/{message_id}` for delivery status. A send to a List or an Audience yields one `message_id` for the request, not one per recipient. # Create a new Brand Source: https://www.courier.com/docs/api-reference/brands/create-a-new-brand /openapi-specs/openapi.documented.yml post /brands Creates a brand from a name and settings, including primary and secondary colors. Brands supply the logo, colors, and styling that templates render with. # Delete a Brand Source: https://www.courier.com/docs/api-reference/brands/delete-a-brand /openapi-specs/openapi.documented.yml delete /brands/{brand_id} Deletes a brand by id. Reassign any template or tenant that references it before deleting to keep their styling intact. # Get a Brand Source: https://www.courier.com/docs/api-reference/brands/get-a-brand /openapi-specs/openapi.documented.yml get /brands/{brand_id} Returns one brand by id, including its colors, logo and styling settings, Handlebars snippets, and published version. # List Brands Source: https://www.courier.com/docs/api-reference/brands/list-brands /openapi-specs/openapi.documented.yml get /brands Lists the workspace's brands. Every entry carries its name, styling settings, snippets, and published version. # Replace a Brand Source: https://www.courier.com/docs/api-reference/brands/replace-a-brand /openapi-specs/openapi.documented.yml put /brands/{brand_id} Replaces a brand with the values you supply, so send the complete settings and snippets rather than only the fields you want changed. # Archive Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/archive-broadcast /openapi-specs/openapi.documented.yml delete /broadcasts/{broadcastId} Archive a broadcast. This is a soft delete — the archived broadcast is returned and no longer appears in list results. # Cancel Broadcast schedule Source: https://www.courier.com/docs/api-reference/broadcasts/cancel-broadcast-schedule /openapi-specs/openapi.documented.yml post /broadcasts/{broadcastId}/cancel Cancel a broadcast's pending schedule, returning it to the draft state. Only valid for a scheduled broadcast. # Create Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/create-broadcast /openapi-specs/openapi.documented.yml post /broadcasts Create a broadcast. Provisions a private notification template for the broadcast and returns the new broadcast in the draft state. Exactly one channel is required. # Duplicate Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/duplicate-broadcast /openapi-specs/openapi.documented.yml post /broadcasts/{broadcastId}/duplicate Duplicate a broadcast (and its template) into a new draft named "{source name} (copy)". # Get Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/get-broadcast /openapi-specs/openapi.documented.yml get /broadcasts/{broadcastId} Retrieve a broadcast by ID. Archived broadcasts return 404. # Get Broadcast content Source: https://www.courier.com/docs/api-reference/broadcasts/get-broadcast-content /openapi-specs/openapi.documented.yml get /broadcasts/{broadcastId}/content Retrieve the broadcast's content — the elemental content of its private notification template. Defaults to the working draft, since broadcast content is authored as a draft until the broadcast is sent. # List Broadcasts Source: https://www.courier.com/docs/api-reference/broadcasts/list-broadcasts /openapi-specs/openapi.documented.yml get /broadcasts List broadcasts in your workspace. Cursor-paginated; returns broadcasts newest-first. # Schedule Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/schedule-broadcast /openapi-specs/openapi.documented.yml post /broadcasts/{broadcastId}/schedule Schedule a broadcast for a future send to a list or audience. Publishes the broadcast template first. Not allowed once the broadcast is sending or sent. For an immediate send use POST /broadcasts/{broadcastId}/send. # Send Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/send-broadcast /openapi-specs/openapi.documented.yml post /broadcasts/{broadcastId}/send Send a broadcast immediately to a list or audience. Publishes the broadcast template first. Not allowed once the broadcast is sending or sent. # Update Broadcast Source: https://www.courier.com/docs/api-reference/broadcasts/update-broadcast /openapi-specs/openapi.documented.yml put /broadcasts/{broadcastId} Update a broadcast's name. Content is edited via the broadcast's notification template, not this endpoint. # Update Broadcast content Source: https://www.courier.com/docs/api-reference/broadcasts/update-broadcast-content /openapi-specs/openapi.documented.yml put /broadcasts/{broadcastId}/content Author the broadcast's content by replacing the draft elemental content of its private notification template. The draft is published automatically when the broadcast is sent or scheduled. # Add users Source: https://www.courier.com/docs/api-reference/bulk/add-users /openapi-specs/openapi.documented.yml post /bulk/{job_id} Ingest user data into a Bulk Job. **Important**: For email-based bulk jobs, each user must include `profile.email` for provider routing to work correctly. The `to.email` field is not sufficient for email provider routing. # Create a bulk job Source: https://www.courier.com/docs/api-reference/bulk/create-a-bulk-job /openapi-specs/openapi.documented.yml post /bulk Creates a new bulk job for sending messages to multiple recipients. **Required**: `message.event` (event ID or notification ID) **Optional (V2 format)**: `message.template` (notification ID) or `message.content` (Elemental content) can be provided to override the notification associated with the event. # Get a job Source: https://www.courier.com/docs/api-reference/bulk/get-a-job /openapi-specs/openapi.documented.yml get /bulk/{job_id} Returns a bulk job's message definition, its status — CREATED, PROCESSING, COMPLETED, or ERROR — and running counts of users received, messages enqueued, and failures. Poll it to follow a job through to completion. # Get users Source: https://www.courier.com/docs/api-reference/bulk/get-users /openapi-specs/openapi.documented.yml get /bulk/{job_id}/users Returns the users ingested into a bulk job with paging, each carrying the status Courier recorded for it and the id of the message it produced. # Run a job Source: https://www.courier.com/docs/api-reference/bulk/run-a-job /openapi-specs/openapi.documented.yml post /bulk/{job_id}/run Starts processing a bulk job, sending to every user ingested into it. Returns 204 immediately; the job runs asynchronously, so poll the job to watch its status and counts. # Add a token to a user Source: https://www.courier.com/docs/api-reference/device-tokens/add-a-token-to-a-user /openapi-specs/openapi.documented.yml put /users/{user_id}/tokens/{token} Registers one device token for a user against a provider key, overwriting the token if it already exists. Push sends resolve tokens per user. # Add multiple tokens to user Source: https://www.courier.com/docs/api-reference/device-tokens/add-multiple-tokens-to-user /openapi-specs/openapi.documented.yml put /users/{user_id}/tokens Registers several device tokens for a user in one call, overwriting any stored token with a matching value. # Delete user token Source: https://www.courier.com/docs/api-reference/device-tokens/delete-user-token /openapi-specs/openapi.documented.yml delete /users/{user_id}/tokens/{token} Deletes one device token for a user, addressed by the token value, so push sends no longer target that device. # Get a token Source: https://www.courier.com/docs/api-reference/device-tokens/get-a-token /openapi-specs/openapi.documented.yml get /users/{user_id}/tokens/{token} Returns one device token with its provider key, status and status reason, expiry date, and any properties stored alongside it. # List tokens Source: https://www.courier.com/docs/api-reference/device-tokens/list-tokens /openapi-specs/openapi.documented.yml get /users/{user_id}/tokens Returns every device token registered for a user, each with its provider key, status, and expiry date. # Update a token Source: https://www.courier.com/docs/api-reference/device-tokens/update-a-token /openapi-specs/openapi.documented.yml patch /users/{user_id}/tokens/{token} Applies a JSON Patch to a device token, changing its status, expiry, or properties without re-registering it. # Delete a topic's digest Source: https://www.courier.com/docs/api-reference/digests/delete-a-topics-digest /openapi-specs/openapi.documented.yml delete /preferences/sections/{section_id}/topics/{topic_id}/digest Turn off a topic's digest, leaving the topic itself in place. The template is unlinked and the digest's schedules are removed along with their delivery rules. Equivalent to sending `digest: null` on a topic replace. # List digest instances Source: https://www.courier.com/docs/api-reference/digests/list-digest-instances /openapi-specs/openapi.documented.yml get /digests/schedules/{schedule_id}/instances Returns the digest instances for a schedule, one per user, with cursor paging. Use it to see what has accumulated before a digest releases. # Release a digest early Source: https://www.courier.com/docs/api-reference/digests/release-a-digest-early /openapi-specs/openapi.documented.yml post /digests/schedules/{schedule_id}/trigger Send a digest now instead of waiting for its scheduled time, so your users get what they have collected so far right away. # Release a recipient's digest Source: https://www.courier.com/docs/api-reference/digests/release-a-recipients-digest /openapi-specs/openapi.documented.yml post /preferences/sections/{section_id}/topics/{topic_id}/digest/release Send one recipient's held digest now, instead of waiting for its schedule. Use it to preview what a digest will look like, or to let someone flush their own. Keyed on the topic because that is how a held digest is stored: one per recipient per topic, with the schedule recorded on it rather than part of its identity. To flush every recipient on a schedule instead, use `POST /digests/schedules/{schedule_id}/trigger`. # Archive a Journey Source: https://www.courier.com/docs/api-reference/journeys/archive-a-journey /openapi-specs/openapi.documented.yml delete /journeys/{templateId} Archives a journey so it can no longer be invoked. Runs already in flight continue to completion, so archiving never strands a user mid-sequence. # Archive a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/archive-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml delete /journeys/{templateId}/templates/{notificationId} Archives one journey's notification template, preventing further sends. Detach any send node referencing it beforehand. # Cancel Journey runs Source: https://www.courier.com/docs/api-reference/journeys/cancel-journey-runs /openapi-specs/openapi.documented.yml post /journeys/cancel Cancels in-flight journey runs, either every run sharing a cancelation token or one run by id. Use it to stop a sequence when the event resolves. # Create a Journey Source: https://www.courier.com/docs/api-reference/journeys/create-a-journey /openapi-specs/openapi.documented.yml post /journeys Creates a journey from a set of nodes, in draft state unless you pass a published state. Send nodes cannot be included until their templates exist. # Create a Notification Template scoped to a Journey Source: https://www.courier.com/docs/api-reference/journeys/create-a-notification-template-scoped-to-a-journey /openapi-specs/openapi.documented.yml post /journeys/{templateId}/templates Create a notification template scoped to this journey. Defaults to `DRAFT` state; pass `state: "PUBLISHED"` to publish on create. The content tree must contain exactly one channel block whose `channel` matches the `channel` on the request — a journey-scoped template carries a single channel. Top-level elements, or a block for a different channel, return `400`. The template designer renders only the channel block matching the tab it draws, so content stored without one cannot be opened. An empty `elements` array is accepted. # Fetch a Journey by id Source: https://www.courier.com/docs/api-reference/journeys/fetch-a-journey-by-id /openapi-specs/openapi.documented.yml get /journeys/{templateId} Fetch a journey by id. Pass `?version=draft` (default `published`) to retrieve the working draft, or `?version=vN` to retrieve a historical version. # Fetch a Journey run Source: https://www.courier.com/docs/api-reference/journeys/fetch-a-journey-run /openapi-specs/openapi.documented.yml get /journeys/runs/{run_id} Fetch one Journey run by id. Returns `404` for an unknown run, a run belonging to another workspace, a run past the 95-day retention window, or an Automation run id — the same body in every case, so the response never reveals whether a run exists elsewhere. # Fetch a Journey-scoped Notification Template by id Source: https://www.courier.com/docs/api-reference/journeys/fetch-a-journey-scoped-notification-template-by-id /openapi-specs/openapi.documented.yml get /journeys/{templateId}/templates/{notificationId} Returns a journey's own notification template with its name, brand, subscription topic, and content. Defaults to the published version. # Fetch the content of a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/fetch-the-content-of-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml get /journeys/{templateId}/templates/{notificationId}/content Returns the Elemental elements and version of a journey-scoped template's content. Compare versions to see what changed between publishes. # Invoke a Journey Source: https://www.courier.com/docs/api-reference/journeys/invoke-a-journey /openapi-specs/openapi.documented.yml post /journeys/{templateId}/invoke Starts a journey run for one user and returns a runId. Runs execute asynchronously, so the response arrives before any message is sent. # List Journey runs Source: https://www.courier.com/docs/api-reference/journeys/list-journey-runs /openapi-specs/openapi.documented.yml get /journeys/runs List runs of the workspace's Journeys, newest first, filtered by status, Journey, or date range and paged by cursor. Runs of v2 Automations are listed by `GET /automations/runs` instead — the two surfaces never return each other's runs. Runs are retained for 95 days. # List Journeys Source: https://www.courier.com/docs/api-reference/journeys/list-journeys /openapi-specs/openapi.documented.yml get /journeys Lists the workspace's journeys, each carrying a name, state, and enabled flag. Paged by cursor. # List Notification Templates scoped to a Journey Source: https://www.courier.com/docs/api-reference/journeys/list-notification-templates-scoped-to-a-journey /openapi-specs/openapi.documented.yml get /journeys/{templateId}/templates List notification templates scoped to this journey. Journey-scoped notification templates can only be referenced from `send` nodes within the same journey. # List steps for a Journey run Source: https://www.courier.com/docs/api-reference/journeys/list-steps-for-a-journey-run /openapi-specs/openapi.documented.yml get /journeys/runs/{run_id}/steps List the per-node state of one Journey run, in full — this endpoint is not paginated. Each step's `node_id` is the id of the node in the published Journey, so a step maps directly onto the Journey graph. `message_id` is present on send steps that produced a message; follow it to `GET /messages/{message_id}` for delivery status. # List versions of a Journey Source: https://www.courier.com/docs/api-reference/journeys/list-versions-of-a-journey /openapi-specs/openapi.documented.yml get /journeys/{templateId}/versions Lists a journey's published versions, most recent first, so you have a version id to roll back to. Paged by cursor. # List versions of a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/list-versions-of-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml get /journeys/{templateId}/templates/{notificationId}/versions Lists the published versions of a template that belongs to a journey, most recent first. Paged by cursor. # Publish a Journey Source: https://www.courier.com/docs/api-reference/journeys/publish-a-journey /openapi-specs/openapi.documented.yml post /journeys/{templateId}/publish Publishes a journey's current draft as a new version, making it live for new runs. Pass a version instead to roll back to an earlier one. # Publish a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/publish-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml post /journeys/{templateId}/templates/{notificationId}/publish Publishes a journey-scoped template's draft as a new version. Pass a version instead to roll back the template to an earlier publish. # Replace a Journey Source: https://www.courier.com/docs/api-reference/journeys/replace-a-journey /openapi-specs/openapi.documented.yml put /journeys/{templateId} Replaces a journey's working draft, leaving the published version live until you publish. Reach for this when editing a journey already running. # Replace a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/replace-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml put /journeys/{templateId}/templates/{notificationId} Replaces the draft content of one journey's notification template. Publish it before send nodes referencing it render the change. # Replace a locale of a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/replace-a-locale-of-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml put /journeys/{templateId}/templates/{notificationId}/locales/{localeId} Set locale-specific content overrides for a journey-scoped notification template. Each element override must reference an existing element by ID. # Replace the content of a Journey-scoped Notification Template Source: https://www.courier.com/docs/api-reference/journeys/replace-the-content-of-a-journey-scoped-notification-template /openapi-specs/openapi.documented.yml put /journeys/{templateId}/templates/{notificationId}/content Replace the elemental content of a journey-scoped notification template. Overwrites all elements in the template draft with the provided content. # Add subscribers to a List Source: https://www.courier.com/docs/api-reference/lists/add-subscribers-to-a-list /openapi-specs/openapi.documented.yml post /lists/{list_id}/subscriptions Subscribes additional users to the list, without modifying existing subscriptions. If the list does not exist, it will be automatically created. # Delete a List Source: https://www.courier.com/docs/api-reference/lists/delete-a-list /openapi-specs/openapi.documented.yml delete /lists/{list_id} Deletes a list, halting sends that target it. A previously deleted list can be brought back with the companion restore endpoint. # Get a List Source: https://www.courier.com/docs/api-reference/lists/get-a-list /openapi-specs/openapi.documented.yml get /lists/{list_id} Returns one list by id with its name and created and updated timestamps. Fetch its subscribers separately with the subscriptions endpoint. # List Lists Source: https://www.courier.com/docs/api-reference/lists/list-lists /openapi-specs/openapi.documented.yml get /lists Returns the workspace's lists, filterable by a pattern to fetch a subset such as every regional list. Paged by cursor. # List subscriptions for a List Source: https://www.courier.com/docs/api-reference/lists/list-subscriptions-for-a-list /openapi-specs/openapi.documented.yml get /lists/{list_id}/subscriptions Returns the users subscribed to a list with paging, each with the preferences recorded for that subscription. # Restore a List Source: https://www.courier.com/docs/api-reference/lists/restore-a-list /openapi-specs/openapi.documented.yml put /lists/{list_id}/restore Restores a previously deleted list along with its subscribers, so a list removed by mistake can be brought back rather than rebuilt. # Subscribe a user Profile to a List Source: https://www.courier.com/docs/api-reference/lists/subscribe-a-user-profile-to-a-list /openapi-specs/openapi.documented.yml put /lists/{list_id}/subscriptions/{user_id} Subscribes one user to a list, creating the list if it does not yet exist. Optional preferences apply to this subscription only. # Subscribe users to a List Source: https://www.courier.com/docs/api-reference/lists/subscribe-users-to-a-list /openapi-specs/openapi.documented.yml put /lists/{list_id}/subscriptions Subscribes the users to the list, overwriting existing subscriptions. If the list does not exist, it will be automatically created. # Unsubscribe a user Profile from a List Source: https://www.courier.com/docs/api-reference/lists/unsubscribe-a-user-profile-from-a-list /openapi-specs/openapi.documented.yml delete /lists/{list_id}/subscriptions/{user_id} Removes one user's subscription to a list, addressed by list id and user id. The user's profile and other subscriptions are separate resources. # Update a List Source: https://www.courier.com/docs/api-reference/lists/update-a-list /openapi-specs/openapi.documented.yml put /lists/{list_id} Creates or replaces a list from a name and preferences. Subscribers are managed through the separate subscriptions endpoints. # Archive message Source: https://www.courier.com/docs/api-reference/messages/archive-message /openapi-specs/openapi.documented.yml put /requests/{request_id}/archive Archives a send request by its request id. Use it to remove test sends or superseded requests from the message list without deleting them. # Cancel message Source: https://www.courier.com/docs/api-reference/messages/cancel-message /openapi-specs/openapi.documented.yml post /messages/{message_id}/cancel Cancels a message that is still in the delivery pipeline and returns the message record with its resulting canceled or failed status. # Get message Source: https://www.courier.com/docs/api-reference/messages/get-message /openapi-specs/openapi.documented.yml get /messages/{message_id} Returns a sent message's status, recipient, event, and per-provider delivery detail, with timestamps for enqueued, sent, delivered, opened, and clicked. # Get message content Source: https://www.courier.com/docs/api-reference/messages/get-message-content /openapi-specs/openapi.documented.yml get /messages/{message_id}/output Returns the rendered content Courier delivered for a message, broken out per channel, to confirm what the recipient received. # Get message history Source: https://www.courier.com/docs/api-reference/messages/get-message-history /openapi-specs/openapi.documented.yml get /messages/{message_id}/history Returns the ordered event history for a sent message, one entry per status transition with its timestamp. # List messages Source: https://www.courier.com/docs/api-reference/messages/list-messages /openapi-specs/openapi.documented.yml get /messages Returns previously sent messages, most recent first, each carrying its status, recipient, channel, and provider. Paged by cursor. # Resend message Source: https://www.courier.com/docs/api-reference/messages/resend-message /openapi-specs/openapi.documented.yml post /messages/{message_id}/resend Resends a previously sent message to the same recipient and content, returning a new messageId. The original send request is not modified. # Archive a Preference Section Source: https://www.courier.com/docs/api-reference/preference-topics/archive-a-preference-section /openapi-specs/openapi.documented.yml delete /preferences/sections/{section_id} Archive a workspace preference. The workspace preference must be empty: delete its topics first, otherwise the request fails with 409. # Archive Topic in Section Source: https://www.courier.com/docs/api-reference/preference-topics/archive-topic-in-section /openapi-specs/openapi.documented.yml delete /preferences/sections/{section_id}/topics/{topic_id} Archives a subscription topic and removes it from its workspace preference, addressed by section id and topic id. # Create a Preference Section Source: https://www.courier.com/docs/api-reference/preference-topics/create-a-preference-section /openapi-specs/openapi.documented.yml post /preferences/sections Creates a workspace preference and returns its generated id. Add subscription topics to it afterwards with the topics endpoint. # Create Topic in Section Source: https://www.courier.com/docs/api-reference/preference-topics/create-topic-in-section /openapi-specs/openapi.documented.yml post /preferences/sections/{section_id}/topics Creates a subscription topic inside a workspace preference. The default status sets whether users start opted in, opted out, or required. # Get a Preference Section Source: https://www.courier.com/docs/api-reference/preference-topics/get-a-preference-section /openapi-specs/openapi.documented.yml get /preferences/sections/{section_id} Returns one workspace preference by id, including its subscription topics, routing options, and custom routing flag. # Get Topic in Section Source: https://www.courier.com/docs/api-reference/preference-topics/get-topic-in-section /openapi-specs/openapi.documented.yml get /preferences/sections/{section_id}/topics/{topic_id} Returns one subscription topic with its default status, routing options, allowed preferences, and unsubscribe header setting. # List preference changes Source: https://www.courier.com/docs/api-reference/preference-topics/list-preference-changes /openapi-specs/openapi.documented.yml get /preferences/logs Returns the history of preference changes in this environment, newest first. Each entry records one change a user made to one subscription topic, and carries the value before it where there was one. Supply user_id to read a single user's history instead of the whole environment. # List Preference Sections Source: https://www.courier.com/docs/api-reference/preference-topics/list-preference-sections /openapi-specs/openapi.documented.yml get /preferences/sections Returns the workspace's preferences, each embedding its subscription topics, routing options, and whether custom routing is allowed. # List Topics in Section Source: https://www.courier.com/docs/api-reference/preference-topics/list-topics-in-section /openapi-specs/openapi.documented.yml get /preferences/sections/{section_id}/topics Returns the subscription topics inside a workspace preference, each with its default status and routing options. # Publish Preferences page Source: https://www.courier.com/docs/api-reference/preference-topics/publish-preferences-page /openapi-specs/openapi.documented.yml post /preferences/publish Publishes the workspace preference page, snapshotting every preference and topic, and returns the page id and a preview URL. # Replace a Preference Section Source: https://www.courier.com/docs/api-reference/preference-topics/replace-a-preference-section /openapi-specs/openapi.documented.yml put /preferences/sections/{section_id} Replace a workspace preference. Full document replacement; missing optional fields are cleared. Topics attached to the workspace preference are unaffected. # Replace Topic in Section Source: https://www.courier.com/docs/api-reference/preference-topics/replace-topic-in-section /openapi-specs/openapi.documented.yml put /preferences/sections/{section_id}/topics/{topic_id} Replace a topic within a workspace preference. Full document replacement; missing optional fields are cleared. Same 404 rules as GET. # Archive Device Set Source: https://www.courier.com/docs/api-reference/previews/archive-device-set /openapi-specs/openapi.documented.yml delete /previews/device-sets/{deviceSetId} Archive a device set. This is a soft delete — the archived set is returned and no longer appears in list results. Runs already created against it keep their own copy of the device list and are unaffected. The Courier-provided default set cannot be archived and returns 409. # Create Device Set Source: https://www.courier.com/docs/api-reference/previews/create-device-set /openapi-specs/openapi.documented.yml post /previews/device-sets Create a named, reusable set of preview devices. Every id must be one listed by `GET /previews/devices`; any other is a 422. # Create Preview Run Source: https://www.courier.com/docs/api-reference/previews/create-preview-run /openapi-specs/openapi.documented.yml post /notifications/{id}/previews/runs Render this template's email content on each of the requested devices. Returns as soon as the run exists and its render is queued — the screenshots are produced asynchronously. Poll `GET /notifications/{id}/previews/runs/{previewRunId}` until every result reaches a terminal status. Name the devices either with `device_set_id`, for a saved set, or with `device_ids`, for a one-off list. Exactly one of the two is required. Inline `device_ids` must be ids listed by `GET /previews/devices`; any other id is a 422, refused before the run exists or is billed. A template that does not exist is a 404. One that exists but cannot be previewed — not a Design Studio template, no email channel, or no such `template_version` — is a 422, also refused before the run exists or is billed. Preview runs are a metered add-on. A workspace without it, or with its billing suspended, receives a 402. # Get Device Set Source: https://www.courier.com/docs/api-reference/previews/get-device-set /openapi-specs/openapi.documented.yml get /previews/device-sets/{deviceSetId} Retrieve a preview set by ID. Archived sets return 404. # Get Preview Run Source: https://www.courier.com/docs/api-reference/previews/get-preview-run /openapi-specs/openapi.documented.yml get /notifications/{id}/previews/runs/{previewRunId} Retrieve one of this template's preview runs together with its per-device results. A run is only readable under the template it previewed: under any other template it is a 404, the same as a run that does not exist. `thumbnail_url` and `screenshot_url` are short-lived signed URLs, re-signed on every read. Fetch them now rather than storing them. Both are null until Courier's own copy of the image exists, which is what `status: COMPLETED` on a result means. # List Device Sets Source: https://www.courier.com/docs/api-reference/previews/list-device-sets /openapi-specs/openapi.documented.yml get /previews/device-sets List the workspace's preview sets. Archived sets are not returned. # List Preview Devices Source: https://www.courier.com/docs/api-reference/previews/list-preview-devices /openapi-specs/openapi.documented.yml get /previews/devices List the devices a preview can be rendered on. Reference data, identical for every workspace — these ids are what a device set is built from and what a run reports results for. # List Preview Runs Source: https://www.courier.com/docs/api-reference/previews/list-preview-runs /openapi-specs/openapi.documented.yml get /notifications/{id}/previews/runs List this template's preview runs, newest first. Cursor-paginated. A template that does not exist is a 404, the same as every other `/notifications/{id}` route. # Update Device Set Source: https://www.courier.com/docs/api-reference/previews/update-device-set /openapi-specs/openapi.documented.yml put /previews/device-sets/{deviceSetId} Replace a device set. This is a full replace, not a patch — both the name and the device list are always written. The Courier-provided default set cannot be changed and returns 409. # Create a Provider Source: https://www.courier.com/docs/api-reference/providers/create-a-provider /openapi-specs/openapi.documented.yml post /providers Configures a provider integration from a Courier provider key and its settings. Check the catalog endpoint for the schema each provider expects. # Delete a Provider Source: https://www.courier.com/docs/api-reference/providers/delete-a-provider /openapi-specs/openapi.documented.yml delete /providers/{id} Deletes a provider configuration, which fails while routing strategies or templates still reference it. Update those references first. # Get a Provider Source: https://www.courier.com/docs/api-reference/providers/get-a-provider /openapi-specs/openapi.documented.yml get /providers/{id} Returns one configured provider by id, including its channel, provider key, alias, title, and current settings. # List available Provider types Source: https://www.courier.com/docs/api-reference/providers/list-available-provider-types /openapi-specs/openapi.documented.yml get /providers/catalog Returns the provider types Courier supports, each with a display name, description, and the configuration fields it requires. # List Providers Source: https://www.courier.com/docs/api-reference/providers/list-providers /openapi-specs/openapi.documented.yml get /providers Lists the provider integrations configured in the workspace, one entry per channel and provider key with its alias and settings. # Patch a Provider Source: https://www.courier.com/docs/api-reference/providers/patch-a-provider /openapi-specs/openapi.documented.yml patch /providers/{id} Applies JSON Patch (RFC 6902) operations to a provider configuration and keeps every field the operations don't touch. Paths address the provider as it is returned: /title, /alias and /settings/. The operations apply in order, and if one fails, nothing is saved. # Update a Provider Source: https://www.courier.com/docs/api-reference/providers/update-a-provider /openapi-specs/openapi.documented.yml put /providers/{id} Replaces a provider's configuration in full, clearing any field you omit rather than merging it. Send the complete settings object. # Archive Routing Strategy Source: https://www.courier.com/docs/api-reference/routing-strategies/archive-routing-strategy /openapi-specs/openapi.documented.yml delete /routing-strategies/{id} Archive a routing strategy. The strategy must not have associated notification templates. Unlink all templates before archiving. # Create Routing Strategy Source: https://www.courier.com/docs/api-reference/routing-strategies/create-routing-strategy /openapi-specs/openapi.documented.yml post /routing-strategies Create a routing strategy. Requires a name and routing configuration at minimum. Channels and providers default to empty if omitted. # Get Routing Strategy Source: https://www.courier.com/docs/api-reference/routing-strategies/get-routing-strategy /openapi-specs/openapi.documented.yml get /routing-strategies/{id} Returns one routing strategy by id with its name, tags, channels, and the routing rules that decide provider order and fallback. # List notifications for a Routing Strategy Source: https://www.courier.com/docs/api-reference/routing-strategies/list-notifications-for-a-routing-strategy /openapi-specs/openapi.documented.yml get /routing-strategies/{id}/notifications Returns the notification templates using a routing strategy, with paging. Check this before changing a strategy that templates depend on. # List Routing Strategies Source: https://www.courier.com/docs/api-reference/routing-strategies/list-routing-strategies /openapi-specs/openapi.documented.yml get /routing-strategies List routing strategies in your workspace. Returns metadata only (no routing/channels/providers content). Use GET /routing-strategies/{id} for full details. # Replace Routing Strategy Source: https://www.courier.com/docs/api-reference/routing-strategies/replace-routing-strategy /openapi-specs/openapi.documented.yml put /routing-strategies/{id} Replace a routing strategy. Full document replacement; the caller must send the complete desired state. Missing optional fields are cleared. # Send a message Source: https://www.courier.com/docs/api-reference/send/send-a-message /openapi-specs/openapi.documented.yml post /send Sends a message to one or more recipients and returns a requestId. Courier routes it to email, SMS, push, chat, or in-app based on your rules. Use the returned requestId to look up delivery status via the Messages API. # Archive Notification Template Source: https://www.courier.com/docs/api-reference/templates/archive-notification-template /openapi-specs/openapi.documented.yml delete /notifications/{id} Archives a notification template, preventing new sends from referencing it. The template stays retrievable for its version history. # Cancel submission Source: https://www.courier.com/docs/api-reference/templates/cancel-submission /openapi-specs/openapi.documented.yml delete /notifications/{id}/{submissionId}/checks Cancels a pending template submission, withdrawing it from the approval workflow. The template stays in draft and can be resubmitted later. # Create Notification Template Source: https://www.courier.com/docs/api-reference/templates/create-notification-template /openapi-specs/openapi.documented.yml post /notifications Create a notification template. Requires all fields in the notification object. Templates are created in draft state by default. Content must place its elements inside a channel block — `{ "type": "channel", "channel": "email", "elements": [...] }` — or the request returns `400`. The template designer renders only the channel block matching the tab it draws, so content stored without one cannot be opened. An empty `elements` array is accepted, and the requirement applies to creation only: `PUT /notifications/{id}` still accepts unwrapped content. Note this endpoint takes versioned content only — the `{ title, body }` shorthand accepted by `/send` is rejected here with an `invalid_request_error` on `notification.content.version`. # Get Notification Content Source: https://www.courier.com/docs/api-reference/templates/get-notification-content /openapi-specs/openapi.documented.yml get /notifications/{id}/content Returns a template's content and checksum. V2 templates return Elemental elements, while V1 templates return blocks and channels instead. # Get Notification Template Source: https://www.courier.com/docs/api-reference/templates/get-notification-template /openapi-specs/openapi.documented.yml get /notifications/{id} Retrieve a notification template by ID. Returns the published version by default. Pass version=draft to retrieve an unpublished template. # Get Notification Template Metrics Source: https://www.courier.com/docs/api-reference/templates/get-notification-template-metrics /openapi-specs/openapi.documented.yml get /notifications/{id}/metrics Fetch the delivery funnel for one Notification Template as a time series — sent, delivered, opened, clicked, errors, and undeliverable — broken out per provider and channel inside each bucket. Sum the entries in a bucket for its totals; there is no bucket-level total. Choose the window absolutely with `start` and `end`, or relatively with `lookback` (an ISO 8601 duration). `start` and `end` take precedence when both are supplied, and a request carrying neither defaults to `lookback=P30D`. The window is snapped outwards onto the `granularity` grid so every bucket it overlaps is returned whole, and the snapped boundaries come back as `start` and `end` — align a chart on those rather than on what was requested. Every boundary is UTC; there is no timezone support. Every bucket in the window is returned, including the quiet ones, whose `data` array is empty, so a series is directly plottable with no gap filling client-side. An unknown template id returns `200` with an all-empty series rather than `404`, and messages sent without a Notification Template never appear here. Available in the US region only. # Get submission checks Source: https://www.courier.com/docs/api-reference/templates/get-submission-checks /openapi-specs/openapi.documented.yml get /notifications/{id}/{submissionId}/checks Returns the approval checks recorded for a template submission, each with its pass or fail result. # List Notification Template Versions Source: https://www.courier.com/docs/api-reference/templates/list-notification-template-versions /openapi-specs/openapi.documented.yml get /notifications/{id}/versions Returns a notification template's published versions, most recent first, for comparison or rollback. Paged. # List Notification Templates Source: https://www.courier.com/docs/api-reference/templates/list-notification-templates /openapi-specs/openapi.documented.yml get /notifications Lists the workspace's notification templates. Each carries a name, tags, brand, routing, and its draft or published state. # Publish Notification Template Source: https://www.courier.com/docs/api-reference/templates/publish-notification-template /openapi-specs/openapi.documented.yml post /notifications/{id}/publish Publish a notification template. Publishes the current draft by default. Pass a version in the request body to publish a specific historical version. # Replace Notification Content Source: https://www.courier.com/docs/api-reference/templates/replace-notification-content /openapi-specs/openapi.documented.yml put /notifications/{id}/content Replaces all Elemental content in a template, overwriting every existing element. Supported for V2 templates only, not V1 blocks and channels. # Replace Notification Element Source: https://www.courier.com/docs/api-reference/templates/replace-notification-element /openapi-specs/openapi.documented.yml put /notifications/{id}/elements/{elementId} Replaces one Elemental element in a template, addressed by its element id. Supported for V2 templates only, not V1 blocks and channels. # Replace Notification Locale Source: https://www.courier.com/docs/api-reference/templates/replace-notification-locale /openapi-specs/openapi.documented.yml put /notifications/{id}/locales/{localeId} Sets locale-specific content overrides for a template. Each override must reference an element that already exists in the default content. # Replace Notification Template Source: https://www.courier.com/docs/api-reference/templates/replace-notification-template /openapi-specs/openapi.documented.yml put /notifications/{id} Replaces a notification template in full, so send every field rather than only the ones you want changed. Publish separately to make it live. # Replace submission checks Source: https://www.courier.com/docs/api-reference/templates/replace-submission-checks /openapi-specs/openapi.documented.yml put /notifications/{id}/{submissionId}/checks Replaces the approval checks on a template submission with the complete set supplied in the request body. # Add a user to a Tenant Source: https://www.courier.com/docs/api-reference/tenant-memberships/add-a-user-to-a-tenant /openapi-specs/openapi.documented.yml put /users/{user_id}/tenants/{tenant_id} Adds a user to one tenant, optionally with a tenant-specific profile that overrides their workspace profile for sends in that tenant. # Add a user to multiple Tenants Source: https://www.courier.com/docs/api-reference/tenant-memberships/add-a-user-to-multiple-tenants /openapi-specs/openapi.documented.yml put /users/{user_id}/tenants Adds a user to several tenants in one call, each optionally with a per-tenant profile that overrides their workspace profile. # List a user's Tenants Source: https://www.courier.com/docs/api-reference/tenant-memberships/list-a-users-tenants /openapi-specs/openapi.documented.yml get /users/{user_id}/tenants Returns the tenants a user belongs to, with cursor paging. A user can belong to many tenants, each with its own profile and preferences. # Remove a user from all Tenants Source: https://www.courier.com/docs/api-reference/tenant-memberships/remove-a-user-from-all-tenants /openapi-specs/openapi.documented.yml delete /users/{user_id}/tenants Removes a user from every tenant they belong to in one call. Their workspace-level profile is a separate resource. # Remove user from a Tenant Source: https://www.courier.com/docs/api-reference/tenant-memberships/remove-user-from-a-tenant /openapi-specs/openapi.documented.yml delete /users/{user_id}/tenants/{tenant_id} Removes a user from one tenant. Their other tenant memberships and workspace profile are managed through separate endpoints. # Create or update a Tenant Template Source: https://www.courier.com/docs/api-reference/tenant-templates/create-or-update-a-tenant-template /openapi-specs/openapi.documented.yml put /tenants/{tenant_id}/templates/{template_id} Creates or updates a notification template scoped to one tenant, letting a tenant override the content the workspace template would send. This is an upsert: it creates when the tenant has no template under `template_id`, and updates when it does. On the create half, content must place its elements inside a channel block — `{ "type": "channel", "channel": "email", "elements": [...] }` — or the request returns `400`. The template designer renders only the channel block matching the tab it draws, so content stored without one cannot be opened. An empty `elements` array is accepted, as is the `{ title, body }` shorthand, which has no elements to wrap. Updates are not checked, so tenant templates already stored without a wrapper stay editable. # Delete a Tenant Template Source: https://www.courier.com/docs/api-reference/tenant-templates/delete-a-tenant-template /openapi-specs/openapi.documented.yml delete /tenants/{tenant_id}/templates/{template_id} Deletes a tenant's notification template by id. Sends for that tenant then use the workspace template registered under the same id. # Get a Template in Tenant Source: https://www.courier.com/docs/api-reference/tenant-templates/get-a-template-in-tenant /openapi-specs/openapi.documented.yml get /tenants/{tenant_id}/templates/{template_id} Returns a tenant's notification template with its content, version, and created, updated, and published timestamps. # Get a Template version Source: https://www.courier.com/docs/api-reference/tenant-templates/get-a-template-version /openapi-specs/openapi.documented.yml get /tenants/{tenant_id}/templates/{template_id}/versions/{version} Returns one version of a tenant template, addressed by version number or by latest, with its content and publish timestamp. # List Templates in Tenant Source: https://www.courier.com/docs/api-reference/tenant-templates/list-templates-in-tenant /openapi-specs/openapi.documented.yml get /tenants/{tenant_id}/templates Lists a tenant's notification templates, each carrying its version and published timestamp. Paged. # Publish a Tenant Template Source: https://www.courier.com/docs/api-reference/tenant-templates/publish-a-tenant-template /openapi-specs/openapi.documented.yml post /tenants/{tenant_id}/templates/{template_id}/publish Publishes a version of a tenant's notification template, making it the content that tenant's sends render from until you publish another. # Create or replace a Tenant Source: https://www.courier.com/docs/api-reference/tenants/create-or-replace-a-tenant /openapi-specs/openapi.documented.yml put /tenants/{tenant_id} Creates or replaces a tenant from a name, parent, brand, properties, and default preferences supplied in the request body. # Create or replace default Preferences for Topic Source: https://www.courier.com/docs/api-reference/tenants/create-or-replace-default-preferences-for-topic /openapi-specs/openapi.documented.yml put /tenants/{tenant_id}/default_preferences/items/{topic_id} Sets a tenant's default opt-in status for one subscription topic, which applies to every member unless a user sets their own override. # Delete a Tenant Source: https://www.courier.com/docs/api-reference/tenants/delete-a-tenant /openapi-specs/openapi.documented.yml delete /tenants/{tenant_id} Deletes a tenant. Its members' workspace-level profiles and preferences live outside the tenant and are managed separately. # Get a Tenant Source: https://www.courier.com/docs/api-reference/tenants/get-a-tenant /openapi-specs/openapi.documented.yml get /tenants/{tenant_id} Returns one tenant with its name, parent tenant id, default preferences, properties, and the user profile applied to its members. # Get users in Tenant Source: https://www.courier.com/docs/api-reference/tenants/get-users-in-tenant /openapi-specs/openapi.documented.yml get /tenants/{tenant_id}/users Returns the users belonging to a tenant with cursor paging. Use it to see who a tenant-scoped send will reach. # List Tenants Source: https://www.courier.com/docs/api-reference/tenants/list-tenants /openapi-specs/openapi.documented.yml get /tenants Lists the workspace's tenants, each carrying a name, parent tenant, properties, and default preferences. Paged. # Remove default Preferences for Topic Source: https://www.courier.com/docs/api-reference/tenants/remove-default-preferences-for-topic /openapi-specs/openapi.documented.yml delete /tenants/{tenant_id}/default_preferences/items/{topic_id} Removes a tenant's default preference for one subscription topic, addressed by tenant id and topic id. # Track an event Source: https://www.courier.com/docs/api-reference/track-events/track-an-event /openapi-specs/openapi.documented.yml post /inbound/courier Records an inbound event that can trigger a journey. Requires an event name, a messageId you generate, a type, and a properties object. # Get a Translation Source: https://www.courier.com/docs/api-reference/translations/get-a-translation /openapi-specs/openapi.documented.yml get /translations/{domain}/{locale} Returns the translation strings stored for one domain and locale, for use in localized notification content. # Update Translations by locale Source: https://www.courier.com/docs/api-reference/translations/update-translations-by-locale /openapi-specs/openapi.documented.yml put /translations/{domain}/{locale} Uploads the translation strings for one domain and locale. Courier uses them to render localized content for recipients in that locale. # Delete user Subscription Topic Source: https://www.courier.com/docs/api-reference/user-preferences/delete-user-subscription-topic /openapi-specs/openapi.documented.yml delete /users/{user_id}/preferences/{topic_id} Removes a user's override for one subscription topic, resetting it to the effective default from the tenant or workspace. # Get user Subscription Topic Source: https://www.courier.com/docs/api-reference/user-preferences/get-user-subscription-topic /openapi-specs/openapi.documented.yml get /users/{user_id}/preferences/{topic_id} Returns a user's opt-in status and channel choices for one subscription topic, or the effective default if they have set no override. # Get user's Preferences Source: https://www.courier.com/docs/api-reference/user-preferences/get-users-preferences /openapi-specs/openapi.documented.yml get /users/{user_id}/preferences Returns a user's preference overrides with paging, one entry per subscription topic they have set a choice for. # Replace user Preferences in bulk Source: https://www.courier.com/docs/api-reference/user-preferences/replace-user-preferences-in-bulk /openapi-specs/openapi.documented.yml put /users/{user_id}/preferences Replaces a user's entire set of preference overrides. Any topic you leave out is reset to its default, so send the full set rather than a subset. # Update or create user Preferences for a Subscription Topic Source: https://www.courier.com/docs/api-reference/user-preferences/update-or-create-user-preferences-for-a-subscription-topic /openapi-specs/openapi.documented.yml put /users/{user_id}/preferences/{topic_id} Sets a user's opt-in status and channel choices for one subscription topic, overriding the tenant default for that topic only. # Update user Preferences in bulk Source: https://www.courier.com/docs/api-reference/user-preferences/update-user-preferences-in-bulk /openapi-specs/openapi.documented.yml post /users/{user_id}/preferences Adds or updates a user's preferences for several subscription topics at once. Topics you leave out keep whatever they were set to before. # Create a Profile Source: https://www.courier.com/docs/api-reference/user-profiles/create-a-profile /openapi-specs/openapi.documented.yml post /profiles/{user_id} Merges the supplied values into a user's profile, creating it if absent and leaving any key you omit untouched. Prefer this for everyday writes. # Delete a Profile Source: https://www.courier.com/docs/api-reference/user-profiles/delete-a-profile /openapi-specs/openapi.documented.yml delete /profiles/{user_id} Deletes a user's profile and stored contact details. List subscriptions and preferences are separate resources, so remove those too if required. # Delete List subscriptions Source: https://www.courier.com/docs/api-reference/user-profiles/delete-list-subscriptions /openapi-specs/openapi.documented.yml delete /profiles/{user_id}/lists Removes every list subscription for a user at once. Their profile and preferences are untouched, so this only affects list-targeted sends. # Get a Profile Source: https://www.courier.com/docs/api-reference/user-profiles/get-a-profile /openapi-specs/openapi.documented.yml get /profiles/{user_id} Returns a user's stored profile and preferences, including the email address, phone number, and push tokens Courier can reach them on. # Get List subscriptions Source: https://www.courier.com/docs/api-reference/user-profiles/get-list-subscriptions /openapi-specs/openapi.documented.yml get /profiles/{user_id}/lists Returns the lists a user is subscribed to, with paging. Use it to check what a recipient will receive before sending to a list. # Replace a Profile Source: https://www.courier.com/docs/api-reference/user-profiles/replace-a-profile /openapi-specs/openapi.documented.yml put /profiles/{user_id} Overwrites a user profile in full, removing any key absent from the request body. Use the patch endpoint when changing a single field. # Subscribe to one or more Lists Source: https://www.courier.com/docs/api-reference/user-profiles/subscribe-to-one-or-more-lists /openapi-specs/openapi.documented.yml post /profiles/{user_id}/lists Subscribes a user to one or more lists, creating any list that does not yet exist. Optional preferences apply to each subscription. # Update a Profile Source: https://www.courier.com/docs/api-reference/user-profiles/update-a-profile /openapi-specs/openapi.documented.yml patch /profiles/{user_id} Applies a JSON Patch to a user profile, adding, removing, or replacing individual fields without sending the whole object. # Broadcasts Source: https://www.courier.com/docs/broadcasts/overview Send one message to a whole list or audience, now or on a schedule. A broadcast is one message to a whole list or audience, sent now or on a schedule. You design the content once, pick who receives it, and send. Use a broadcast for one-off sends: newsletters, product launches, and policy updates. A broadcast is those two things together: content written the way a [Template DOC](/docs/design/templates/overview) is written, addressed to a [list or audience DOC](/docs/recipients/lists-and-audiences/overview). ## Which one to use Broadcasts and [journeys DOC](/docs/journeys/overview) both send to many people, and the difference is what starts the send. | | Broadcasts | Journeys | | - | - | - | | Shape | One message to a group | Per-user flow, single or multi-step | | Trigger | You send it, now or scheduled | A user event or API call | | Recipients | A list or an audience | Users entering the flow | | Best for | Newsletters, promotions, announcements | Onboarding, receipts, digests, re-engagement | Sending the same message to a group at a chosen moment is a broadcast. A user's action that should send a message or start a sequence is a journey. ## How it works ### From draft to sent A broadcast moves through three states: * **Draft**: created but not yet sent or scheduled. * **Scheduled**: queued for a set time. You can reschedule or cancel, but the content is locked. To change the message, cancel, edit, and schedule again. * **Sent**: delivered, with [performance metrics DOC](/docs/monitor/analytics#broadcasts). Each broadcast sends on a **single channel**. To reach the same group elsewhere, create a separate broadcast. Recipients are a [list DOC](/docs/recipients/lists-and-audiences/lists) you curate by hand, or an [audience DOC](/docs/recipients/lists-and-audiences/audiences) Courier keeps current from rules. ### Its content You write a broadcast's content in the same editor a [Template DOC](/docs/design/templates/overview) uses, and it is stored as the same [Elemental DOC](/docs/design/elemental/overview) document. Variables resolve from each recipient's profile, so one broadcast reads differently for every person. Build a broadcast in the console, or drive the whole flow through the [Broadcasts API](/docs/api-reference/broadcasts/create-broadcast): create, write content, then send or schedule. ## FAQ > **Is there an API to send a Broadcast?** > > [Create Broadcast](/docs/api-reference/broadcasts/create-broadcast) creates one, [Update Broadcast content](/docs/api-reference/broadcasts/update-broadcast-content) writes the content, and [Send Broadcast](/docs/api-reference/broadcasts/send-broadcast) or `/schedule` delivers it. You can also skip Broadcasts and send to a `list_id` or `audience_id` through the [Send API > DOC](/docs/send/overview). > **Should I send to a List or an Audience?** > > Use a list for a group you curate by hand, and an audience for a rule-based segment Courier keeps current as profiles change. Either works as recipients. # Send a broadcast Source: https://www.courier.com/docs/broadcasts/send Create, schedule, and send a broadcast from the console or the API. A Broadcast sends one message to a whole List or Audience, now or on a schedule. ## Send from the console **1. Create the Broadcast** In the console, open **Broadcasts** under **Orchestration** and select **Add broadcast**. Name it and pick a channel. Each Broadcast sends on one channel. ![The Add a new broadcast modal: a Name field and channel options for Email, SMS, and In-App](https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/broadcast-add-new-broadcast.webp?fit=max\&auto=format\&n=LdpdyPjJHKHJqFY9\&q=85\&s=1f127c70480698f1f941834ffe55d65a) **2. Design the content** Drag blocks onto the canvas, the same editor as a Template. Variables resolve from each recipient's Profile. ![The broadcast design editor: the blocks panel on the left, the email canvas in the center, and Email styles on the right](https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/broadcast-create-design-editor.webp?fit=max\&auto=format\&n=LdpdyPjJHKHJqFY9\&q=85\&s=418e672fdb6bcc1625e7828c97280c7f) **3. Choose recipients** Open **Recipients** and pick a [List DOC](/docs/recipients/lists-and-audiences/lists) you curate by hand, or an [Audience DOC](/docs/recipients/lists-and-audiences/audiences) Courier keeps current from rules. ![The broadcast Recipients dropdown open, listing lists and audiences alongside the send-time fields](https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/broadcast-recipients-dropdown.webp?fit=max\&auto=format\&n=LdpdyPjJHKHJqFY9\&q=85\&s=c249ec837329c452a82313f34d5cb1fb) **4. Send or schedule** Send now, or schedule a date, time, and timezone. A scheduled Broadcast locks its content. To change the message, cancel, edit, and schedule again. ![The broadcast send panel with Scheduled selected, showing date, time, and timezone](https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/broadcast-schedule-details.webp?fit=max\&auto=format\&n=LdpdyPjJHKHJqFY9\&q=85\&s=f19098c8935951e6578f1989ca7f6cc7) **5. Check performance** After it sends, the [**Performance** tab DOC](/docs/monitor/analytics#broadcasts) reports delivery, open, click, and error rates, plus a per-recipient log. ![The Performance tab for a sent broadcast](https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/broadcast-performance.webp?fit=max\&auto=format\&n=LdpdyPjJHKHJqFY9\&q=85\&s=de242699501487e48013e642be593f69) ## Send with the API **1. Create the Broadcast** [Create Broadcast](/docs/api-reference/broadcasts/create-broadcast) takes a name and one channel, and returns the Broadcast ID. `channel` is one of `email`, `sms`, `push`, `inbox`, `slack`, or `msteams`. ```javascript Node.js highlight={3} theme={null} const broadcast = await client.broadcasts.create({ name: 'March product update', channel: 'email', }); ``` ```python Python highlight={3} theme={null} broadcast = client.broadcasts.create( name="March product update", channel="email", ) ``` ```bash cURL highlight={5} theme={null} curl --request POST \ --url https://api.courier.com/broadcasts \ --header "Authorization: Bearer $COURIER_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "name": "March product update", "channel": "email" }' ``` ```ruby Ruby theme={null} broadcast = courier.broadcasts.create(name: "March product update", channel: "email") ``` ```go Go highlight={4} theme={null} broadcast, err := client.Broadcasts.New(context.TODO(), courier.BroadcastNewParams{ CreateBroadcastRequest: courier.CreateBroadcastRequestParam{ Name: "March product update", Channel: "email", }, }) ``` ```java Java highlight={3} theme={null} CreateBroadcastRequest params = CreateBroadcastRequest.builder() .name("March product update") .channel(CreateBroadcastRequest.Channel.EMAIL) .build(); Broadcast broadcast = client.broadcasts().create(params); ``` ```php PHP highlight={3} theme={null} $broadcast = $client->broadcasts->create( name: 'March product update', channel: 'email', ); ``` ```csharp C# highlight={4} theme={null} BroadcastCreateParams parameters = new() { Name = "March product update", Channel = "email", }; var broadcast = await client.Broadcasts.Create(parameters); ``` ```bash CLI highlight={4} theme={null} courier broadcasts create \ --api-key "$COURIER_API_KEY" \ --name "March product update" \ --channel email ``` **2. Write the content** [Update Broadcast content](/docs/api-reference/broadcasts/update-broadcast-content) takes the same Elemental document a Template uses. ```javascript Node.js theme={null} await client.broadcasts.putContent('YOUR_BROADCAST_ID', { content: { version: '2022-01-01', elements: [{ type: 'text', content: 'Here is what shipped in March.' }], }, }); ``` ```python Python theme={null} client.broadcasts.put_content( broadcast_id="YOUR_BROADCAST_ID", content={ "version": "2022-01-01", "elements": [{"type": "text", "content": "Here is what shipped in March."}], }, ) ``` ```bash cURL theme={null} curl --request PUT \ --url https://api.courier.com/broadcasts/YOUR_BROADCAST_ID/content \ --header "Authorization: Bearer $COURIER_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "content": { "version": "2022-01-01", "elements": [{ "type": "text", "content": "Here is what shipped in March." }] } }' ``` ```ruby Ruby theme={null} courier.broadcasts.put_content( "YOUR_BROADCAST_ID", content: {elements: [{type: "text", content: "Here is what shipped in March."}], version: "2022-01-01"} ) ``` ```go Go theme={null} // Elemental nodes carry no typed content field, so set it as an extra field. textNode := shared.ElementalTextNodeWithTypeParam{Type: "text"} textNode.SetExtraFields(map[string]any{"content": "Here is what shipped in March."}) _, err := client.Broadcasts.PutContent( context.TODO(), "YOUR_BROADCAST_ID", courier.BroadcastPutContentParams{ NotificationContentPutRequest: courier.NotificationContentPutRequestParam{ Content: courier.NotificationContentPutRequestContentParam{ Elements: []shared.ElementalNodeUnionParam{{ OfElementalTextNodeWithType: &textNode, }}, Version: courier.String("2022-01-01"), }, }, }, ) ``` ```java Java theme={null} BroadcastPutContentParams params = BroadcastPutContentParams.builder() .broadcastId("YOUR_BROADCAST_ID") .notificationContentPutRequest(NotificationContentPutRequest.builder() .content(NotificationContentPutRequest.Content.builder() .addElement(ElementalTextNodeWithType.builder() .type(ElementalTextNodeWithType.Type.TEXT) .putAdditionalProperty("content", JsonValue.from("Here is what shipped in March.")) .build()) .version("2022-01-01") .build()) .build()) .build(); client.broadcasts().putContent(params); ``` ```php PHP theme={null} $client->broadcasts->putContent( 'YOUR_BROADCAST_ID', content: ['elements' => [['type' => 'text', 'content' => 'Here is what shipped in March.']], 'version' => '2022-01-01'], ); ``` ```csharp C# theme={null} BroadcastPutContentParams parameters = new() { BroadcastID = "YOUR_BROADCAST_ID", Content = new() { // Elemental nodes expose no typed content property, so build the node from raw JSON. Elements = [ ElementalTextNodeWithType.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "type": "text", "content": "Here is what shipped in March." } """)), ], Version = "2022-01-01", }, }; await client.Broadcasts.PutContent(parameters); ``` ```bash CLI theme={null} courier broadcasts put-content \ --api-key "$COURIER_API_KEY" \ --broadcast-id YOUR_BROADCAST_ID \ --content '{"version":"2022-01-01","elements":[{"type":"text","content":"Here is what shipped in March."}]}' ``` **3. Send it** [Send Broadcast](/docs/api-reference/broadcasts/send-broadcast) targets a List or an Audience and sends immediately. ```javascript Node.js theme={null} await client.broadcasts.send('YOUR_BROADCAST_ID', { recipient_type: 'list', recipient_id: 'acme-corp.beta-testers', }); ``` ```python Python theme={null} client.broadcasts.send( broadcast_id="YOUR_BROADCAST_ID", recipient_type="list", recipient_id="acme-corp.beta-testers", ) ``` ```bash cURL theme={null} curl --request POST \ --url https://api.courier.com/broadcasts/YOUR_BROADCAST_ID/send \ --header "Authorization: Bearer $COURIER_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "recipient_type": "list", "recipient_id": "acme-corp.beta-testers" }' ``` ```ruby Ruby theme={null} courier.broadcasts.send_( "YOUR_BROADCAST_ID", recipient_type: "list", recipient_id: "acme-corp.beta-testers" ) ``` ```go Go theme={null} _, err := client.Broadcasts.Send( context.TODO(), "YOUR_BROADCAST_ID", courier.BroadcastSendParams{ SendBroadcastRequest: courier.SendBroadcastRequestParam{ RecipientType: "list", RecipientID: "acme-corp.beta-testers", }, }, ) ``` ```java Java theme={null} BroadcastSendParams params = BroadcastSendParams.builder() .broadcastId("YOUR_BROADCAST_ID") .sendBroadcastRequest(SendBroadcastRequest.builder() .recipientType(SendBroadcastRequest.RecipientType.LIST) .recipientId("acme-corp.beta-testers") .build()) .build(); client.broadcasts().send(params); ``` ```php PHP theme={null} $client->broadcasts->send( 'YOUR_BROADCAST_ID', recipientType: 'list', recipientID: 'acme-corp.beta-testers', ); ``` ```csharp C# theme={null} BroadcastSendParams parameters = new() { BroadcastID = "YOUR_BROADCAST_ID", RecipientType = "list", RecipientID = "acme-corp.beta-testers", }; await client.Broadcasts.Send(parameters); ``` ```bash CLI theme={null} courier broadcasts send \ --api-key "$COURIER_API_KEY" \ --broadcast-id YOUR_BROADCAST_ID \ --recipient-type list \ --recipient-id acme-corp.beta-testers ``` **4. Or schedule it** [Schedule Broadcast](/docs/api-reference/broadcasts/schedule-broadcast) takes a wall-clock `scheduled_to` with no offset, so the zone comes from `timezone`. ```javascript Node.js highlight={4} theme={null} await client.broadcasts.schedule('YOUR_BROADCAST_ID', { recipient_type: 'audience', recipient_id: 'active-business-users', scheduled_to: '2026-07-21T20:00:00', timezone: 'America/New_York', }); ``` ```python Python highlight={5} theme={null} client.broadcasts.schedule( broadcast_id="YOUR_BROADCAST_ID", recipient_type="audience", recipient_id="active-business-users", scheduled_to="2026-07-21T20:00:00", timezone="America/New_York", ) ``` ```bash cURL highlight={8} theme={null} curl --request POST \ --url https://api.courier.com/broadcasts/YOUR_BROADCAST_ID/schedule \ --header "Authorization: Bearer $COURIER_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "recipient_type": "audience", "recipient_id": "active-business-users", "scheduled_to": "2026-07-21T20:00:00", "timezone": "America/New_York" }' ``` ```ruby Ruby highlight={5} theme={null} courier.broadcasts.schedule( "YOUR_BROADCAST_ID", recipient_type: "audience", recipient_id: "active-business-users", scheduled_to: "2026-07-21T20:00:00", timezone: "America/New_York" ) ``` ```go Go highlight={8} theme={null} _, err := client.Broadcasts.Schedule( context.TODO(), "YOUR_BROADCAST_ID", courier.BroadcastScheduleParams{ ScheduleBroadcastRequest: courier.ScheduleBroadcastRequestParam{ RecipientType: "audience", RecipientID: "active-business-users", ScheduledTo: "2026-07-21T20:00:00", Timezone: courier.String("America/New_York"), }, }, ) ``` ```java Java highlight={6} theme={null} BroadcastScheduleParams params = BroadcastScheduleParams.builder() .broadcastId("YOUR_BROADCAST_ID") .scheduleBroadcastRequest(ScheduleBroadcastRequest.builder() .recipientType(ScheduleBroadcastRequest.RecipientType.AUDIENCE) .recipientId("active-business-users") .scheduledTo("2026-07-21T20:00:00") .timezone("America/New_York") .build()) .build(); client.broadcasts().schedule(params); ``` ```php PHP highlight={5} theme={null} $client->broadcasts->schedule( 'YOUR_BROADCAST_ID', recipientType: 'audience', recipientID: 'active-business-users', scheduledTo: '2026-07-21T20:00:00', timezone: 'America/New_York', ); ``` ```csharp C# highlight={6} theme={null} BroadcastScheduleParams parameters = new() { BroadcastID = "YOUR_BROADCAST_ID", RecipientType = "audience", RecipientID = "active-business-users", ScheduledTo = "2026-07-21T20:00:00", Timezone = "America/New_York", }; await client.Broadcasts.Schedule(parameters); ``` ```bash CLI highlight={6} theme={null} courier broadcasts schedule \ --api-key "$COURIER_API_KEY" \ --broadcast-id YOUR_BROADCAST_ID \ --recipient-type audience \ --recipient-id active-business-users \ --scheduled-to "2026-07-21T20:00:00" \ --timezone America/New_York ``` [Cancel Broadcast schedule](/docs/api-reference/broadcasts/cancel-broadcast-schedule) cancels a scheduled send, and [Duplicate Broadcast](/docs/api-reference/broadcasts/duplicate-broadcast) copies one for a repeat. # Brands Source: https://www.courier.com/docs/design/brands Apply one logo, color set, header, and footer across every template you send. A brand is the visual appearance Courier applies to your notifications: logo, colors, header, footer, and custom styling. Attach a brand to a template and every send inherits it, so styling stays consistent. Brands cover white-labeling, multiple product identities, and per-tenant styling. ## How it works ### The brand object A brand's `settings` hold the stored configuration: | Path | What it holds | | - | - | | `settings.colors` | `primary` and `secondary`, plus any extra key you add. The designer offers `tertiary`. | | `settings.email.header` | `barColor`, and `logo.image` with an optional `logo.href` | | `settings.email.footer` | `markdown`, `inheritDefault`, and a `social` URL for `facebook`, `instagram`, `linkedin`, `medium`, or `twitter` | | `settings.email.head` | `content`, injected into the email's `` | | `settings.email.templateOverride` | A custom MJML or Handlebars template, with its own background colors | | `settings.inapp` | In-app styling: colors, icons, placement, border radius, font family | A brand ID is prefixed `bnd_`. Legacy brands may use a slug like `default`. Every workspace has a **default brand** that can't be deleted. Every email uses it unless you disable brands in template settings. It is also the fallback when a send enables brands without naming one. Manage brands through the [Brands API](/docs/api-reference/brands/list-brands) or the [console](https://app.courier.com/content/brands). ### Brand variables in a template At render time Courier flattens the brand into the `brand` namespace, **without** the `settings` level: | Variable | Resolves to | | - | - | | `brand.id` | The brand's ID | | `brand.colors.primary`, `.secondary`, `.tertiary` | The brand's colors | | `brand.email.header.barColor` | The header bar color | | `brand.email.header.logo.image`, `.logo.href` | The logo image and the link it wraps | | `brand.email.backgroundColor`, `.blocksBackgroundColor`, `.footerBackgroundColor` | Layout colors from the active template override | | `brand.social.facebook`, `.instagram`, `.linkedin`, `.medium`, `.twitter` | Each social URL. A network with no URL set is absent. | | `brand.inapp` | The in-app settings | Write it double-braced, so `{{brand.colors.primary}}` in template content, or `{{var "brand.colors.primary"}}` inside a brand's own MJML/Handlebars template. See [variables DOC](/docs/design/templates/variables). ### Brand designer The brand designer is the visual editor for a brand's logo, colors, header, and footer. It also takes a custom MJML/Handlebars template, so you can define the exact header and footer markup. ![The brand designer: brand logo and primary, secondary, and tertiary colors on the left, the head, header, and footer editors in the middle, and social URLs on the right](https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/brand-designer.webp?fit=max\&auto=format\&n=LdpdyPjJHKHJqFY9\&q=85\&s=f4266505e6dc078f069075640b362f29) ### Brand snippets > [!NOTE] > > **Brand snippets are legacy and not recommended for new work.** They are a **Snippets** tab on the older brand designer that stores reusable Handlebars fragments on a brand. The new brand designer doesn't carry them forward, and they aren't being developed further. Existing snippets keep rendering. For shared content in new templates, use the brand's header, footer, and **Head** editors, or a template override. ### CSS class names Courier puts a class on every part of a rendered email. Restyle any of them from the brand's **Head** editor. Every rule takes the same shape: a `c--` class, a `*`, and `!important`. ```html theme={null} .c--block-text * { font-family: Georgia, serif !important; } ``` Both are load-bearing. The class sits on a wrapper, so `*` reaches the text inside it, and MJML's inline styles beat a plain stylesheet. | Target | Classes | | - | - | | Layout | `c--email-body`, `c--email-content`, `c--email-header`, `c--email-footer`, `c--social` | | Blocks | `c--block`, and one of `c--block-text`, `c--block-image`, `c--block-action`, `c--block-list`, `c--block-quote`, `c--block-divider`, `c--block-markdown`, `c--block-template`, `c--block-columns` | | Text | `c--text-text`, `c--text-h1`, `c--text-h2`, `c--text-h3`, `c--text-subtext`, `c--text-quote` | ### Email domain white-labeling Sending from your own domain is configured on the **email provider integration**, not on the brand. Set your verified sending domain and From address on the provider (SendGrid, SES, and so on). Courier enforces the allowed domain at send time. A branded **link and open tracking** domain is a separate setup, a CNAME to Courier's tracking infrastructure. See [tracking DOC](/docs/monitor/tracking#custom-tracking-domain). ### Applying a brand to a send Courier picks the brand in this order: 1. The `brand_id` on the send message. 2. The template's configured default brand, if brands are enabled on it. 3. The workspace default brand. Override the brand per channel with `channels..brand_id`. In multi-tenant apps, a [tenant DOC](/docs/tenants/context) can carry a default brand, so sends in that tenant's context pick it up automatically. ## FAQ > **How do I send from my own domain?** > > Set a verified sending domain and From address on your email provider integration. The brand controls visual styling only. A branded tracking domain is a separate CNAME setup. > **What CSS classes can I target?** > > Layout classes like `c--email-body` and `c--email-footer`, `c--block` with a per-type variant such as `c--block-text`, and text classes from `c--text-text` to `c--text-h3`. Write `.c--block-text * { ... !important }`. The `*` reaches the text inside the wrapper, and `!important` beats the compiler's inline styles. > **How do I brand notifications per tenant?** > > Give each tenant a brand and set it as the tenant's default, or pass `brand_id` on the send. Sends in a tenant's context pick up its brand automatically. # Elemental control flow Source: https://www.courier.com/docs/design/elemental/control-flow Use if, loop, ref, and channels on any element to show, repeat, reuse, or target content. Elemental has four control flow properties: * **`if`** - Conditionally render elements based on data or conditions * **`loop`** - Repeat elements for each item in an array * **`ref`** - Reference elements to check their visibility or properties * **`channels`** - Show elements only on specific channels All four are optional, available on every element, and can be combined on the same element. > [!NOTE] > > Control flow properties are evaluated at render time using Handlebars expressions. You can read `message.data`, `message.to.data`, and other message context. ## If The `if` property renders an element only when a Handlebars expression is truthy. Otherwise the element is skipped. **When to use:** * Show different content based on user type, subscription status, or feature flags * Display elements only when certain data exists * Create personalized experiences based on user context * Hide elements that aren't relevant to the current recipient **Applies to**: All Elemental elements **Basic Example** ```json Elemental highlight={4} theme={null} { "type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === 'premium'" } ``` ```javascript Node.js highlight={12} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "text", content: "Welcome, premium member!", if: "data.user_tier === 'premium'", }, ], }, }, }); ``` ```python Python highlight={12} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === 'premium'", }, ], }, }, ) ``` ```bash cURL highlight={15} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === '\''premium'\''" } ] } } }' ``` ```ruby Ruby highlight={12} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "text", content: "Welcome, premium member!", if: "data.user_tier === 'premium'" } ] } } ) ``` ```go Go highlight={8} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === 'premium'" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={10} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "text", "content", "Welcome, premium member!", "if", "data.user_tier === 'premium'"))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={12} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'text', 'content' => 'Welcome, premium member!', 'if' => 'data.user_tier === \'premium\'', ], ], ], ], ); ``` ```csharp C# highlight={15} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === 'premium'" } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === '\''premium'\''"}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com and show the discount line only for premium users. ``` **Realistic Examples** **Conditional welcome message based on user type:** ```json theme={null} { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Welcome back, {{first_name}}!", "if": "data.is_returning_user" }, { "type": "text", "content": "Welcome to {{company_name}}, {{first_name}}!", "if": "!data.is_returning_user" }, { "type": "action", "content": "Upgrade to Premium", "href": "https://example.com/upgrade", "if": "data.user_tier !== 'premium'" } ] } ``` **Show elements only when data exists:** ```json theme={null} { "type": "group", "if": "data.items && data.items.length > 0", "elements": [ { "type": "text", "content": "Your order contains {{data.items.length}} item(s)" } ] } ``` **Structured conditions** The `if` field also accepts a **structured condition array**: one or more condition groups evaluated at render time. The element renders if **any** group matches, so groups are OR'd together. Within a group, conditions combine using the group's `logical_operator`. **Supported property namespaces** (dot-paths only): | Namespace | Description | Example | | - | - | - | | `data.*` | Values from the send `data` payload | `data.order_total` | | `profile.*` | Recipient profile fields | `profile.plan` | | `refs.*` | Visibility of another element by its `ref` | `refs.promo_banner.visible` | **Supported operators:** | Operator | Type | `value` required | | - | - | - | | `equals`, `not_equals` | binary | yes | | `greater_than`, `less_than`, `greater_than_or_equals`, `less_than_or_equals` | binary | yes | | `contains`, `not_contains` | binary | yes | | `is_empty`, `is_not_empty` | unary | no | **Design Studio labels these operators in prose, and three don't match their JSON key.** The editor builds the same object either way. This is the mapping: | Editor reads | JSON key | | - | - | | doesn't equal | `not_equals` | | is at least | `greater_than_or_equals` | | is at most | `less_than_or_equals` | The other seven read as their key: equals, is greater than, is less than, contains, doesn't contain, is empty, isn't empty. **A condition group always holds at least one condition, and `if` always holds at least one group.** Delete a group's last condition and the group goes with it. Delete the last group and the `if` field is removed rather than left as `[]`. To express "no conditions," omit `if`: an element whose `if` is `[]` is malformed. **Single group (all conditions AND'd):** ```json theme={null} { "type": "text", "content": "You qualify for the enterprise plan.", "if": [ { "logical_operator": "and", "conditions": [ { "property": "data.account_type", "operator": "equals", "value": "enterprise" }, { "property": "profile.email_verified", "operator": "equals", "value": "true" } ] } ] } ``` **Multiple groups (OR between groups):** ```json theme={null} { "type": "text", "content": "Upgrade available!", "if": [ { "logical_operator": "and", "conditions": [{ "property": "data.plan", "operator": "equals", "value": "free" }] }, { "logical_operator": "and", "conditions": [{ "property": "data.plan", "operator": "equals", "value": "starter" }] } ] } ``` **Unary operator (no `value` needed):** ```json theme={null} { "type": "text", "content": "No promo code applied.", "if": [ { "logical_operator": "and", "conditions": [{ "property": "data.promo_code", "operator": "is_empty" }] } ] } ``` **Using `refs` to check element visibility:** ```json theme={null} { "type": "text", "content": "Here is what you unlocked:", "if": [ { "logical_operator": "and", "conditions": [{ "property": "refs.promo_block.visible", "operator": "equals", "value": "true" }] } ] } ``` > [!NOTE] > > String `if` and structured `if` are mutually exclusive on the same element. Use one or the other. String expressions remain fully supported. ## Ref The `ref` property names an element so other elements can reference it. A referenced element exposes its properties plus `visible`, which says whether it rendered. **When to use:** * Check if another element was rendered before showing related content * Create dependencies between elements * Build complex conditional logic based on element visibility * Access element properties from other elements **Applies to**: All Elemental elements **Note**: An element can only reference elements defined earlier in the `elements` array. **Basic Example** ```json Elemental highlight={4,9} theme={null} { "type": "text", "content": "Hello, {{first_name}}", "ref": "greeting" }, { "type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible" } ``` ```javascript Node.js highlight={12,17} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "text", content: "Hello, {{first_name}}", ref: "greeting", }, { type: "text", content: "This shows if greeting is visible", if: "refs.greeting.visible", }, ], }, }, }); ``` ```python Python highlight={12,17} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Hello, {{first_name}}", "ref": "greeting", }, { "type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible", }, ], }, }, ) ``` ```bash cURL highlight={15,20} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Hello, {{first_name}}", "ref": "greeting" }, { "type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible" } ] } } }' ``` ```ruby Ruby highlight={12,17} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "text", content: "Hello, {{first_name}}", ref: "greeting" }, { type: "text", content: "This shows if greeting is visible", if: "refs.greeting.visible" } ] } } ) ``` ```go Go highlight={8,13} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "text", "content": "Hello, {{first_name}}", "ref": "greeting" }, { "type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={10,14} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "text", "content", "Hello, {{first_name}}", "ref", "greeting"), java.util.Map.of( "type", "text", "content", "This shows if greeting is visible", "if", "refs.greeting.visible"))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={12,17} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'text', 'content' => 'Hello, {{first_name}}', 'ref' => 'greeting', ], [ 'type' => 'text', 'content' => 'This shows if greeting is visible', 'if' => 'refs.greeting.visible', ], ], ], ], ); ``` ```csharp C# highlight={15,20} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Hello, {{first_name}}", "ref": "greeting" }, { "type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible" } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "Hello, {{first_name}}", "ref": "greeting"}, {"type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible"}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com that reuses an earlier element by reference. ``` **Realistic Example** **Show follow-up content only if initial element is visible:** ```json theme={null} { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Special offer for premium members!", "ref": "premium_offer", "if": "data.user_tier === 'premium'" }, { "type": "action", "content": "Claim Offer", "href": "https://example.com/claim", "if": "refs.premium_offer.visible" } ] } ``` ## Loop The `loop` property renders an element multiple times, once for each item in an iterable data source (typically an array). **When to use:** * Display lists of products, orders, notifications, or other array data * Create dynamic content that adapts to variable-length data * Build repeating patterns like product cards or notification items * Iterate over nested data structures **Applies to**: All Elemental elements **Loop variables:** * `$.item` - The current item in the iteration * `$.index` - The zero-based index of the current iteration **Basic Example** ```json Elemental highlight={4} theme={null} { "type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products" } ``` ```javascript Node.js highlight={12} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "text", content: "* {{$.item.name}} - {{$.item.price}}", loop: "data.products", }, ], }, data: { products: [ { name: "Notebook", price: 12.5 }, { name: "Pen", price: 3 }, ], }, }, }); ``` ```python Python highlight={12} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products", }, ], }, "data": { "products": [ {"name": "Notebook", "price": 12.5}, {"name": "Pen", "price": 3}, ], }, }, ) ``` ```bash cURL highlight={15} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products" } ] }, "data": { "products": [ { "name": "Notebook", "price": 12.5 }, { "name": "Pen", "price": 3 } ] } } }' ``` ```ruby Ruby highlight={12} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "text", content: "* {{$.item.name}} - {{$.item.price}}", loop: "data.products" } ] }, data: { products: [ { name: "Notebook", price: 12.5 }, { name: "Pen", price: 3 } ] } } ) ``` ```go Go highlight={8} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, Data: map[string]any{ "products": []any{ map[string]any{"name": "Notebook", "price": 12.5}, map[string]any{"name": "Pen", "price": 3}, }, }, }, }) ``` ```java Java highlight={10} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "text", "content", "* {{$.item.name}} - {{$.item.price}}", "loop", "data.products"))))) .data(JsonValue.from(java.util.Map.of( "products", java.util.List.of( java.util.Map.of("name", "Notebook", "price", 12.5), java.util.Map.of("name", "Pen", "price", 3))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={12} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'text', 'content' => '* {{$.item.name}} - {{$.item.price}}', 'loop' => 'data.products', ], ], ], 'data' => [ 'products' => [ ['name' => 'Notebook', 'price' => 12.5], ['name' => 'Pen', 'price' => 3], ], ], ], ); ``` ```csharp C# highlight={15} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products" } ] } """)), Data = new Dictionary() { { "products", JsonSerializer.SerializeToElement(new[] { new { name = "Notebook", price = 12.5 }, new { name = "Pen", price = 3 }, }) }, }, }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products"}]}' \ --message.data '{"products": [{"name": "Notebook", "price": 12.5}, {"name": "Pen", "price": 3}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com listing the two products in data.products, one line per item. ``` **Realistic Examples** **Product list with nested data:** ```json theme={null} { "version": "2022-01-01", "elements": [ { "type": "group", "loop": "data.products", "elements": [ { "type": "text", "content": "{{$.item.name}}", "text_style": "h2" }, { "type": "text", "content": "Price: {{$.item.price}}" }, { "type": "image", "src": "{{$.item.image_url}}", "alt_text": "{{$.item.name}}" }, { "type": "action", "content": "View Details", "href": "https://example.com/products/{{$.item.id}}" }, { "type": "divider" } ] } ] } ``` **Using `$.index` for item numbering:** > [!TIP] > > To show a 1-based item number, pass `$.index` to the `add` [Handlebars helper > DOC](/docs/design/templates/variables): > > ```handlebars theme={null} > Item {{add $.index 1}}: {{$.item.name}} > ``` > > This outputs "Item 1", "Item 2", and so on, instead of starting at zero. ```json theme={null} { "type": "text", "content": "Item {{add $.index 1}}: {{$.item.name}} - {{$.item.price}}", "loop": "data.products" } ``` ## Channels The `channels` property renders an element only on the channels you list. Use it to show different content on email, SMS, push, and other channels. **When to use:** * Show detailed content in email, concise content in SMS * Display channel-specific formatting or elements * Customize content per channel while maintaining a single template * Hide elements that don't work well on certain channels **Applies to**: All Elemental elements **Valid channels**: `email`, `push`, `direct_message`, `sms`, or provider-specific channels like `slack`, `discord`, etc. > [!NOTE] > > For a fully different content structure per channel, use [Channel elements > DOC](/docs/design/elemental/elements/channel). **Basic Example** ```json Elemental highlight={4} theme={null} { "type": "text", "content": "This only appears in email and push", "channels": ["email", "push"] } ``` ```javascript Node.js highlight={12} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "text", content: "This only appears in email and push", channels: [ "email", "push", ], }, ], }, }, }); ``` ```python Python highlight={12} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "This only appears in email and push", "channels": [ "email", "push", ], }, ], }, }, ) ``` ```bash cURL highlight={15} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "text", "content": "This only appears in email and push", "channels": [ "email", "push" ] } ] } } }' ``` ```ruby Ruby highlight={12} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "text", content: "This only appears in email and push", channels: [ "email", "push" ] } ] } } ) ``` ```go Go highlight={8} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "text", "content": "This only appears in email and push", "channels": [ "email", "push" ] } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={10} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "text", "content", "This only appears in email and push", "channels", java.util.List.of( "email", "push")))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={12} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'text', 'content' => 'This only appears in email and push', 'channels' => [ 'email', 'push', ], ], ], ], ], ); ``` ```csharp C# highlight={15} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "text", "content": "This only appears in email and push", "channels": [ "email", "push" ] } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "This only appears in email and push", "channels": ["email", "push"]}]}' ``` ```text MCP theme={null} With Courier MCP, send an email and a push to sarah@acme-corp.com, showing the footer on email only. ``` **Realistic Examples** **Channel-specific content:** ```json theme={null} { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Your order #{{order_number}} has shipped! Track it here: {{tracking_url}}", "channels": ["email"] }, { "type": "text", "content": "Order {{order_number}} shipped. Track: {{tracking_url}}", "channels": ["sms"] }, { "type": "text", "content": "Your order has shipped!", "channels": ["push"] } ] } ``` **Hide complex elements on SMS:** ```json theme={null} { "type": "columns", "channels": ["email"], "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "image", "src": "{{product_image}}" } ] }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "{{product_name}}" } ] } ] } ``` **Combining Control Flow Properties** You can combine multiple control flow properties on the same element: ```json theme={null} { "type": "text", "content": "Premium feature available!", "if": "data.user_tier === 'premium'", "channels": ["email", "push"], "ref": "premium_notice" } ``` **Evaluation order:** 1. `channels` - Element must match current channel 2. `if` - Condition must evaluate to truthy 3. `loop` - Element is repeated for each item (if present) 4. `ref` - Element is registered for reference (if present) ### Best practices * **Use `if` for conditional content**: Show/hide elements based on data or user context * **Use `loop` with `group`**: Wrap looped elements in a group for better organization * **Reference order matters**: Elements must be defined before they're referenced * **Test with real data**: Control flow expressions are evaluated at render time, so test with realistic data structures * **Combine with locales**: Use control flow with [localization DOC](/docs/design/elemental/locales) for fully dynamic, multi-language notifications * **Channel considerations**: some elements (like columns) may not render well on all channels # Elemental action element Source: https://www.courier.com/docs/design/elemental/elements/action Add a button or link with a label, href, and style, rendered per channel. The action element adds a clickable action to your notification. It renders as a button or a link, depending on the channel and styling options you choose. The type value is `"action"`. **When to use:** * Add call-to-action buttons (e.g., "Sign Up", "View Order", "Confirm Email") * Create clickable links within notifications * Provide interactive elements that direct users to specific URLs **Basic example** ```json Elemental theme={null} { "type": "action", "content": "Click me", "href": "https://example.com" } ``` ```javascript Node.js highlight={9-13} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "action", content: "Click me", href: "https://example.com", }, ], }, }, }); ``` ```python Python highlight={9-13} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "action", "content": "Click me", "href": "https://example.com", }, ], }, }, ) ``` ```bash cURL highlight={12-16} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "action", "content": "Click me", "href": "https://example.com" } ] } } }' ``` ```ruby Ruby highlight={9-13} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "action", content: "Click me", href: "https://example.com" } ] } } ) ``` ```go Go highlight={5-9} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "action", "content": "Click me", "href": "https://example.com" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-10} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "action", "content", "Click me", "href", "https://example.com"))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-13} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'action', 'content' => 'Click me', 'href' => 'https://example.com', ], ], ], ], ); ``` ```csharp C# highlight={12-16} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "action", "content": "Click me", "href": "https://example.com" } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "action", "content": "Click me", "href": "https://example.com"}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com with a button linking to our dashboard. ``` **Fields** **content** `string` *required* The label displayed on the button or link. **href** `string` *required* The target URL of the action. Clicking it sends the user to this URL. **action\_id** `string` A unique identifier for the action when it is executed. Useful for tracking and analytics. **align** `string` The alignment of the action button. One of `"center"`, `"left"`, `"right"`, or `"full"`. Defaults to `"center"`. **background\_color** `string` The background color of the action button. Any valid CSS color value (e.g., `"#007bff"`, `"rgb(0, 123, 255)"`). **style** `string` The visual style of the action. Defaults to `"button"`. * `"button"`: a filled button. `background_color` is the fill. * `"secondary"`: an outlined button. `background_color` is the border and the label. * `"tertiary"`: the quietest button. `background_color` is the label. * `"link"`: inline text rather than a button, and the only style that draws no button chrome. `background_color` is a fill only for `"button"`. Sending white with `"secondary"` asks for a white border and a white label, not a white button. **disable\_tracking** `boolean` When true, the action's `href` isn't rewritten for click-through tracking, even when click-through tracking is enabled for the workspace. **locales** `object` Translations of `content` and `href`, keyed by locale code. See [Template translations DOC](/docs/design/elemental/locales). **Examples and variants** **Styles** The default style renders a filled button: ```json theme={null} { "type": "action", "content": "Get Started", "href": "https://example.com/start", "style": "button" } ``` `"link"` renders inline text instead: ```json theme={null} { "type": "action", "content": "Learn more", "href": "https://example.com/docs", "style": "link" } ``` Each channel draws these as closely as its medium allows, so the three button styles are a hierarchy rather than a fixed set of pixels: | | Email | Inbox | Slack | | - | - | - | - | | `"button"` | filled | plain, hairline edge | button | | `"secondary"` | outlined | outlined | button, Slack's `primary` style | | `"tertiary"` | underlined | solid fill | button, Slack's `danger` style | | `"link"` | inline link | inline link | `mrkdwn` link | The Inbox reads the three buttons as a volume control rather than as email's fill, outline, and rule. `"button"` is the quiet default it has always drawn, and `"tertiary"` is the solid one. **Styled button** Customize the button appearance with colors and alignment: ```json theme={null} { "type": "action", "content": "Sign Up Now", "href": "https://example.com/signup", "style": "button", "background_color": "#007bff", "align": "center" } ``` **With localization** Localize action content and URLs: ```json theme={null} { "type": "action", "content": "View Dashboard", "href": "https://app.example.com/dashboard", "locales": { "es": { "content": "Ver Panel", "href": "https://app.example.com/es/dashboard" }, "fr": { "content": "Voir le Tableau de Bord", "href": "https://app.example.com/fr/dashboard" } } } ``` **With conditional logic** Show different actions based on conditions: ```json theme={null} { "type": "action", "content": "Upgrade Account", "href": "https://example.com/upgrade", "if": "{{user.plan}} === 'free'" } ``` **Channel support** Action elements are supported across all channels: * **Email**: filled, outlined, or underlined, following `style` * **Push**: a clickable action button * **SMS**: a clickable link * **Inbox**: an interactive button whose weight follows `style` # Elemental channel element Source: https://www.courier.com/docs/design/elemental/elements/channel Set different content per channel inside one document, and override that channel's routing. The channel element sets content per channel. Send a detailed message on email and a shorter one on push. The type value is `"channel"`. **When to use:** * Provide channel-specific content (different content for email vs push vs SMS) * Use raw channel data for provider-specific formats (MJML for email, Slack blocks, etc.) * Create channel-specific layouts and structures > [!NOTE] > > Channel elements are only valid at the top level and can't be nested. If one appears at the top level, every sibling must also be a channel element. > [!TIP] > > **Alternative**: most elements support a `channels` property that shows a single element on chosen channels only. See [Control flow > DOC](/docs/design/elemental/control-flow#channels). **Basic example** ```json Elemental theme={null} { "type": "channel", "channel": "email", "elements": [ { "type": "meta", "title": "My Subject" }, { "type": "text", "content": "My email body" } ] } ``` ```javascript Node.js highlight={9-22} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "channel", channel: "email", elements: [ { type: "meta", title: "My Subject", }, { type: "text", content: "My email body", }, ], }, ], }, }, }); ``` ```python Python highlight={9-22} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "channel", "channel": "email", "elements": [ { "type": "meta", "title": "My Subject", }, { "type": "text", "content": "My email body", }, ], }, ], }, }, ) ``` ```bash cURL highlight={12-25} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "channel", "channel": "email", "elements": [ { "type": "meta", "title": "My Subject" }, { "type": "text", "content": "My email body" } ] } ] } } }' ``` ```ruby Ruby highlight={9-22} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "channel", channel: "email", elements: [ { type: "meta", title: "My Subject" }, { type: "text", content: "My email body" } ] } ] } } ) ``` ```go Go highlight={5-18} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "channel", "channel": "email", "elements": [ { "type": "meta", "title": "My Subject" }, { "type": "text", "content": "My email body" } ] } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-16} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "channel", "channel", "email", "elements", java.util.List.of( java.util.Map.of( "type", "meta", "title", "My Subject"), java.util.Map.of( "type", "text", "content", "My email body"))))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-22} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'channel', 'channel' => 'email', 'elements' => [ [ 'type' => 'meta', 'title' => 'My Subject', ], [ 'type' => 'text', 'content' => 'My email body', ], ], ], ], ], ], ); ``` ```csharp C# highlight={12-25} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "channel", "channel": "email", "elements": [ { "type": "meta", "title": "My Subject" }, { "type": "text", "content": "My email body" } ] } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "channel", "channel": "email", "elements": [{"type": "meta", "title": "My Subject"}, {"type": "text", "content": "My email body"}]}]}' ``` ```text MCP theme={null} With Courier MCP, send an email and an SMS to sarah@acme-corp.com with different content on each channel. ``` **Fields** **channel** `string` *required* The channel the contents of this element should be applied to. Can be: * Standard channels: `"email"`, `"push"`, `"direct_message"`, `"sms"` * Provider names: `"slack"`, `"discord"`, `"teams"`, etc. * `"default"` - applies to all channels not explicitly specified **elements** `CourierElement[]` An array of Elemental elements to apply to the channel. If `raw` hasn't been specified, `elements` is required. **raw** `object` Raw data to apply to the channel. Use channel-specific formats like MJML for email, Slack blocks, or webhook payloads. If `elements` hasn't been specified, `raw` is required. **font\_family** `string` The CSS `font-family` stack to apply to the rendered email. Only valid when `channel` is `"email"`. Use the exact `fontFamily` value from the [font catalog DOC](/docs/design/elemental/fonts#font-catalog). Google Fonts are loaded automatically. Defaults to `Helvetica, Arial, sans-serif` if omitted. ```json theme={null} { "type": "channel", "channel": "email", "font_family": "Roboto, Arial, sans-serif", "elements": [...] } ``` **Examples and variants** **Using elements** Provide different Elemental content per channel: ```json theme={null} { "type": "channel", "channel": "email", "elements": [ { "type": "meta", "title": "Order Confirmation" }, { "type": "text", "content": "Your order #{{order_id}} has been confirmed. We'll send you tracking information once your order ships." }, { "type": "action", "content": "View Order", "href": "https://app.example.com/orders/{{order_id}}" } ] }, { "type": "channel", "channel": "push", "elements": [ { "type": "meta", "title": "Order Confirmed" }, { "type": "text", "content": "Order #{{order_id}} confirmed" } ] } ``` **Using raw data** Use raw channel data for provider-specific formats. Email with MJML: ```json theme={null} { "type": "channel", "channel": "email", "raw": { "subject": "Order Confirmation", "html": "Your order has been confirmed!", "text": "Your order has been confirmed!", "transformers": ["handlebars", "mjml"] } } ``` Slack: ```json theme={null} { "type": "channel", "channel": "slack", "raw": { "text": "Hello World!", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Your order has been confirmed!" } } ] } } ``` Webhook: ```json theme={null} { "type": "channel", "channel": "webhook", "raw": { "payload": { "body": { "event": "order.confirmed", "order_id": "{{order_id}}", "timestamp": "{{timestamp}}" } } } } ``` **Default channel** Use `"default"` to provide content for all channels not explicitly specified: ```json theme={null} { "type": "channel", "channel": "email", "elements": [ { "type": "text", "content": "Detailed email content here" } ] }, { "type": "channel", "channel": "default", "elements": [ { "type": "text", "content": "Content for push, SMS, and other channels" } ] } ``` **Channel-specific considerations** **Email** * Supports full Elemental elements or raw HTML/MJML * Can use `raw.subject` for email subject line * Supports `transformers` array for templating engines **Push** * Typically uses `meta.title` for notification title * Content should be concise due to character limits * Supports action buttons via action elements **SMS** * Very limited character count * Best for short, essential messages * No rich formatting support **Direct message (Slack, Discord, Teams, etc.)** * Provider-specific formats via `raw` property * Can use provider-specific block structures * Supports rich interactive elements per provider # Elemental columns element Source: https://www.courier.com/docs/design/elemental/elements/columns Lay out content side by side in columns with their own width and styling. The columns element holds a row of columns for side-by-side content. Each column can set its own styling, width, and content. The type value is `"columns"`, and each child must be a `"column"` element. **When to use:** * Create side-by-side layouts (e.g., product images with descriptions) * Build responsive multi-column grids * Organize content in structured layouts * Display data in tabular-like formats > [!NOTE] > > **Email client compatibility**: column styling uses MJML patterns. Borders and border radius render correctly in most modern email clients and degrade gracefully in older ones. The `gap` property inserts spacer columns between content columns to get the spacing right. **Basic example** ```json Elemental theme={null} { "type": "columns", "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "image", "src": "https://example.com/product.png" } ] }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Product Name", "text_style": "h2" }, { "type": "text", "content": "$99.99" } ] } ] } ``` ```javascript Node.js highlight={9-38} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "columns", elements: [ { type: "column", width: "50%", elements: [ { type: "image", src: "https://example.com/product.png", }, ], }, { type: "column", width: "50%", elements: [ { type: "text", content: "Product Name", text_style: "h2", }, { type: "text", content: "$99.99", }, ], }, ], }, ], }, }, }); ``` ```python Python highlight={9-38} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "columns", "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "image", "src": "https://example.com/product.png", }, ], }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Product Name", "text_style": "h2", }, { "type": "text", "content": "$99.99", }, ], }, ], }, ], }, }, ) ``` ```bash cURL highlight={12-41} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "columns", "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "image", "src": "https://example.com/product.png" } ] }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Product Name", "text_style": "h2" }, { "type": "text", "content": "$99.99" } ] } ] } ] } } }' ``` ```ruby Ruby highlight={9-38} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "columns", elements: [ { type: "column", width: "50%", elements: [ { type: "image", src: "https://example.com/product.png" } ] }, { type: "column", width: "50%", elements: [ { type: "text", content: "Product Name", text_style: "h2" }, { type: "text", content: "$99.99" } ] } ] } ] } } ) ``` ```go Go highlight={5-34} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "columns", "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "image", "src": "https://example.com/product.png" } ] }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Product Name", "text_style": "h2" }, { "type": "text", "content": "$99.99" } ] } ] } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-27} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "columns", "elements", java.util.List.of( java.util.Map.of( "type", "column", "width", "50%", "elements", java.util.List.of( java.util.Map.of( "type", "image", "src", "https://example.com/product.png"))), java.util.Map.of( "type", "column", "width", "50%", "elements", java.util.List.of( java.util.Map.of( "type", "text", "content", "Product Name", "text_style", "h2"), java.util.Map.of( "type", "text", "content", "$99.99"))))))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-38} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'columns', 'elements' => [ [ 'type' => 'column', 'width' => '50%', 'elements' => [ [ 'type' => 'image', 'src' => 'https://example.com/product.png', ], ], ], [ 'type' => 'column', 'width' => '50%', 'elements' => [ [ 'type' => 'text', 'content' => 'Product Name', 'text_style' => 'h2', ], [ 'type' => 'text', 'content' => '$99.99', ], ], ], ], ], ], ], ], ); ``` ```csharp C# highlight={12-41} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "columns", "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "image", "src": "https://example.com/product.png" } ] }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Product Name", "text_style": "h2" }, { "type": "text", "content": "$99.99" } ] } ] } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "columns", "elements": [{"type": "column", "width": "50%", "elements": [{"type": "image", "src": "https://example.com/product.png"}]}, {"type": "column", "width": "50%", "elements": [{"type": "text", "content": "Product Name", "text_style": "h2"}, {"type": "text", "content": "$99.99"}]}]}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com laid out in two columns. ``` **Columns element fields** **elements** `CourierColumn[]` *required* An array of column elements. Each must be of type `"column"`. See the **Column element** section below. **background\_color** `string` Background color for the columns container. Any valid CSS color value (e.g., `"#f5f5f5"`, `"rgb(245,245,245)"`). **border\_width** `string` Border width for the columns container (e.g., `"1px"`, `"2px"`). **border\_color** `string` Border color for the columns container (e.g., `"#cccccc"`, `"rgb(204,204,204)"`). **border\_radius** `string` Border radius for rounded corners on the container (e.g., `"5px"`, `"10px"`). **gap** `string` Spacing between columns. Automatically inserts spacer columns with exact width (e.g., `"10px"`, `"15px"`). **padding** `string` Inner padding for the columns container. CSS shorthand works (e.g., `"10px"`, `"20px 10px"`). **vertical\_align** `string` Alignment of columns within the row. One of `"top"`, `"middle"`, or `"bottom"`. Defaults to `"top"`. **Column element** Each child in a columns element's `elements` array must be of type `column`. Each one can set its own styling and width. **type** `string` *required* Must be `"column"`. **elements** `CourierElement[]` *required* The content blocks inside this column. Any Elemental elements (text, image, action, etc.). **width** `string` The width of the column. Can be: * Percentage: `"50%"`, `"33.33%"` * Fixed: `"200px"`, `"300px"` * Auto: `"auto"` (takes remaining space) **background\_color** `string` Hex code or CSS color for the column background (e.g., `"#F4F7FF"`, `"rgb(244,247,255)"`). **border\_width** `string` Border width for this specific column (e.g., `"1px"`, `"2px"`). **border\_color** `string` Border color for this specific column (e.g., `"#cccccc"`, `"rgb(204,204,204)"`). **border\_radius** `string` Border radius for rounded corners on this column (e.g., `"5px"`, `"10px"`). **padding** `string` CSS padding shorthand for the column (e.g., `"10px"`, `"20px 10px"`). **vertical\_align** `string` Overrides the parent's vertical alignment for this column. One of `"top"`, `"middle"`, or `"bottom"`. **Examples and variants** **Two-column layout** Basic sidebar and main content layout: ```json theme={null} { "type": "columns", "elements": [ { "type": "column", "width": "30%", "background_color": "#F4F7FF", "padding": "20px", "elements": [ { "type": "image", "src": "https://example.com/avatar.png", "width": "50px" }, { "type": "text", "content": "User Profile", "text_style": "subtext" } ] }, { "type": "column", "width": "70%", "padding": "20px", "elements": [ { "type": "text", "content": "Welcome to the platform!", "text_style": "h2" }, { "type": "text", "content": "We are excited to have you here. Your account is now active." }, { "type": "action", "content": "Get Started", "href": "https://example.com/start", "style": "button" } ] } ] } ``` **Equal-width columns** Three equal columns with middle vertical alignment: ```json theme={null} { "type": "columns", "vertical_align": "middle", "gap": "15px", "elements": [ { "type": "column", "width": "33.33%", "elements": [ { "type": "text", "content": "Column 1", "text_style": "h3" }, { "type": "text", "content": "Content for first column" } ] }, { "type": "column", "width": "33.33%", "elements": [ { "type": "text", "content": "Column 2", "text_style": "h3" }, { "type": "text", "content": "Content for second column" } ] }, { "type": "column", "width": "33.33%", "elements": [ { "type": "text", "content": "Column 3", "text_style": "h3" }, { "type": "text", "content": "Content for third column" } ] } ] } ``` **Styled container with borders** Columns container with styling: ```json theme={null} { "type": "columns", "padding": "20px", "background_color": "#f5f5f5", "border_width": "2px", "border_color": "#cccccc", "border_radius": "8px", "gap": "15px", "elements": [ { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Left Column" } ] }, { "type": "column", "width": "50%", "elements": [ { "type": "text", "content": "Right Column" } ] } ] } ``` **Product card layout** Complete product card with image and details: ```json theme={null} { "type": "columns", "padding": "25px", "background_color": "#ffffff", "border_width": "1px", "border_color": "#e0e0e0", "border_radius": "12px", "gap": "20px", "vertical_align": "middle", "elements": [ { "type": "column", "width": "40%", "border_width": "1px", "border_color": "#4CAF50", "border_radius": "8px", "padding": "20px", "background_color": "#f1f8f4", "elements": [ { "type": "image", "src": "https://example.com/product-image.jpg", "href": "https://example.com/product", "width": "100%" } ] }, { "type": "column", "width": "60%", "elements": [ { "type": "text", "content": "**Premium Product Name**", "text_style": "h2" }, { "type": "text", "content": "High-quality product with exceptional features. Perfect for everyday use." }, { "type": "text", "content": "**$149.99**", "bold": true }, { "type": "action", "content": "Add to Cart", "href": "https://example.com/cart", "background_color": "#4CAF50", "style": "button" } ] } ] } ``` **Channel support** * **Email**: Full support with MJML rendering for maximum compatibility * **Push**: Limited support. Columns may render as stacked content * **SMS**: Not supported. Use single-column layouts * **Inbox**: Full support with responsive behavior # Elemental comment element Source: https://www.courier.com/docs/design/elemental/elements/comment Add notes to a template that never render in the output, for documenting the document. The comment element holds notes that never render in the output. Use it to document templates, leave notes for teammates, or label sections. The type value is `"comment"`. **When to use:** * Document complex template logic * Add notes for other developers * Organize and label sections of your template * Include metadata that shouldn't appear in notifications > [!NOTE] > > Comments are ignored at render time and never appear in the output, on any channel. **Basic example** ```json Elemental theme={null} { "type": "comment", "comment": "This is a comment that will not be rendered in the output" } ``` ```javascript Node.js highlight={9-12} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "comment", comment: "This is a comment that will not be rendered in the output", }, ], }, }, }); ``` ```python Python highlight={9-12} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "comment", "comment": "This is a comment that will not be rendered in the output", }, ], }, }, ) ``` ```bash cURL highlight={12-15} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "comment", "comment": "This is a comment that will not be rendered in the output" } ] } } }' ``` ```ruby Ruby highlight={9-12} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "comment", comment: "This is a comment that will not be rendered in the output" } ] } } ) ``` ```go Go highlight={5-8} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "comment", "comment": "This is a comment that will not be rendered in the output" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-9} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "comment", "comment", "This is a comment that will not be rendered in the output"))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-12} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'comment', 'comment' => 'This is a comment that will not be rendered in the output', ], ], ], ], ); ``` ```csharp C# highlight={12-15} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "comment", "comment": "This is a comment that will not be rendered in the output" } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "comment", "comment": "This is a comment that will not be rendered in the output"}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com and leave a comment element noting who owns the copy. ``` **Fields** **comment** `string` The comment text. It isn't rendered in the output. **object** `any` An optional object alongside the comment. Holds metadata, structured notes, or other context that isn't rendered. **Examples and variants** **Simple comment** Add a basic text comment: ```json theme={null} { "type": "comment", "comment": "This section handles order confirmations" } ``` **Comment with metadata** Include structured data with your comment: ```json theme={null} { "type": "comment", "comment": "A/B test variant A - shows promotional content", "object": { "test_id": "promo_ab_test", "variant": "A", "author": "team-marketing", "last_updated": "2024-01-15" } } ``` **Organizing template sections** Use comments to organize and label different sections: ```json theme={null} { "version": "2022-01-01", "elements": [ { "type": "comment", "comment": "=== HEADER SECTION ===" }, { "type": "meta", "title": "Order Confirmation" }, { "type": "image", "src": "https://example.com/logo.png" }, { "type": "comment", "comment": "=== MAIN CONTENT ===" }, { "type": "text", "content": "Your order has been confirmed!" }, { "type": "comment", "comment": "=== FOOTER SECTION ===" }, { "type": "text", "content": "Questions? Contact support@acme-corp.com" } ] } ``` # Elemental divider element Source: https://www.courier.com/docs/design/elemental/elements/divider Render a horizontal rule between content sections, with color and spacing. The divider element renders a horizontal rule between elements. Use it to separate content sections. The type value is `"divider"`. **When to use:** * Separate distinct sections of content * Create visual breaks between related items * Improve readability in long notifications * Organize grouped content visually **Basic example** ```json Elemental theme={null} { "type": "divider", "color": "#800080" } ``` ```javascript Node.js highlight={9-12} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "divider", color: "#800080", }, ], }, }, }); ``` ```python Python highlight={9-12} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "divider", "color": "#800080", }, ], }, }, ) ``` ```bash cURL highlight={12-15} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "divider", "color": "#800080" } ] } } }' ``` ```ruby Ruby highlight={9-12} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "divider", color: "#800080" } ] } } ) ``` ```go Go highlight={5-8} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "divider", "color": "#800080" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-9} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "divider", "color", "#800080"))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-12} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'divider', 'color' => '#800080', ], ], ], ], ); ``` ```csharp C# highlight={12-15} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "divider", "color": "#800080" } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "divider", "color": "#800080"}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com with a divider between two sections. ``` **Fields** **color** `string` The CSS color of the divider line. Any valid CSS color value (e.g., `"#e0e0e0"`, `"rgb(224, 224, 224)"`, `"gray"`). Defaults to a standard gray. **border\_width** `string` Border width in pixels for the divider line (e.g., `"1px"`, `"2px"`). **padding** `string` Padding around the divider. Any valid CSS padding value (e.g., `"10px"`, `"20px 10px"`). **channels** `string[]` An array of channel names. The divider renders only for the listed channels. See [Control flow DOC](/docs/design/elemental/control-flow#channels). **Examples and variants** **Basic divider** Simple divider with default styling: ```json theme={null} { "type": "divider" } ``` **Styled divider** Divider with custom color: ```json theme={null} { "type": "divider", "color": "#800080" } ``` **Thick divider** Divider with custom width: ```json theme={null} { "type": "divider", "color": "#007bff", "border_width": "3px" } ``` **Divider with padding** Divider with spacing: ```json theme={null} { "type": "divider", "color": "#e0e0e0", "padding": "20px 0" } ``` **Section separator** Using dividers to separate content sections: ```json theme={null} { "version": "2022-01-01", "elements": [ { "type": "text", "content": "Order Summary", "text_style": "h2" }, { "type": "text", "content": "Item 1: $29.99" }, { "type": "text", "content": "Item 2: $49.99" }, { "type": "divider", "color": "#cccccc" }, { "type": "text", "content": "Total: $79.98", "bold": true } ] } ``` **Channel support** * **Email**: ✅ Full support with styling * **Push**: ⚠️ Limited support (may render as plain text or be ignored) * **SMS**: ❌ Not supported (dividers aren't rendered) * **Inbox**: ✅ Full support with styling # Elemental group element Source: https://www.courier.com/docs/design/elemental/elements/group Wrap several elements so one if or loop condition applies to all of them. The group element wraps multiple elements into one unit. Combine it with `if` or `loop` to render or repeat the whole set at once. The type value is `"group"`. **When to use:** * Apply conditional logic to multiple elements at once * Loop over multiple elements together * Organize related elements into logical sections * Create reusable element blocks **Basic example** ```json Elemental theme={null} { "type": "group", "elements": [ { "type": "text", "content": "Welcome!" }, { "type": "action", "content": "Get Started", "href": "https://example.com/start" } ] } ``` ```javascript Node.js highlight={9-22} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "group", elements: [ { type: "text", content: "Welcome!", }, { type: "action", content: "Get Started", href: "https://example.com/start", }, ], }, ], }, }, }); ``` ```python Python highlight={9-22} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "group", "elements": [ { "type": "text", "content": "Welcome!", }, { "type": "action", "content": "Get Started", "href": "https://example.com/start", }, ], }, ], }, }, ) ``` ```bash cURL highlight={12-25} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "group", "elements": [ { "type": "text", "content": "Welcome!" }, { "type": "action", "content": "Get Started", "href": "https://example.com/start" } ] } ] } } }' ``` ```ruby Ruby highlight={9-22} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "group", elements: [ { type: "text", content: "Welcome!" }, { type: "action", content: "Get Started", href: "https://example.com/start" } ] } ] } } ) ``` ```go Go highlight={5-18} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "group", "elements": [ { "type": "text", "content": "Welcome!" }, { "type": "action", "content": "Get Started", "href": "https://example.com/start" } ] } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-16} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "group", "elements", java.util.List.of( java.util.Map.of( "type", "text", "content", "Welcome!"), java.util.Map.of( "type", "action", "content", "Get Started", "href", "https://example.com/start"))))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-22} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'group', 'elements' => [ [ 'type' => 'text', 'content' => 'Welcome!', ], [ 'type' => 'action', 'content' => 'Get Started', 'href' => 'https://example.com/start', ], ], ], ], ], ], ); ``` ```csharp C# highlight={12-25} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "group", "elements": [ { "type": "text", "content": "Welcome!" }, { "type": "action", "content": "Get Started", "href": "https://example.com/start" } ] } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "group", "elements": [{"type": "text", "content": "Welcome!"}, {"type": "action", "content": "Get Started", "href": "https://example.com/start"}]}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com with a grouped heading and button block. ``` **Fields** **elements** `CourierElement[]` *required* An array of Elemental elements. They render together as one unit. **if** `string | object[]` A condition that determines whether the whole group renders. Accepts a string expression or a structured condition array. See [Control flow DOC](/docs/design/elemental/control-flow#if). **loop** `string` An expression that repeats the whole group. See [Control flow DOC](/docs/design/elemental/control-flow#loop). **ref** `string` A reference identifier for the group. See [Control flow DOC](/docs/design/elemental/control-flow#ref). **channels** `string[]` An array of channel names. The group renders only for the listed channels. See [Control flow DOC](/docs/design/elemental/control-flow#channels). **Examples and variants** **Conditional group** Show or hide multiple elements based on a condition: ```json theme={null} { "type": "group", "if": "{{user.plan}} === 'premium'", "elements": [ { "type": "text", "content": "**Premium Features**", "text_style": "h2" }, { "type": "text", "content": "You have access to all premium features!" }, { "type": "action", "content": "Explore Features", "href": "https://example.com/premium" } ] } ``` **Looping group** Repeat a group of elements for each item in an array: ```json theme={null} { "type": "group", "loop": "data.products", "elements": [ { "type": "text", "content": "# {{$.item.name}}", "text_style": "h2" }, { "type": "divider" }, { "type": "text", "content": "Description: {{$.item.description}}" }, { "type": "text", "content": "Price: ${{$.item.price}}", "bold": true }, { "type": "action", "content": "View Product", "href": "https://example.com/products/{{$.item.id}}" } ] } ``` **Nested groups** Group elements within groups for complex structures: ```json theme={null} { "type": "group", "if": "{{order.items.length}} > 0", "elements": [ { "type": "text", "content": "Order Items", "text_style": "h2" }, { "type": "group", "loop": "data.order.items", "elements": [ { "type": "text", "content": "{{$.item.name}} - ${{$.item.price}}" } ] }, { "type": "text", "content": "Total: ${{order.total}}", "bold": true } ] } ``` # Elemental HTML element Source: https://www.courier.com/docs/design/elemental/elements/html Embed raw HTML when no other element produces the structure you need. The HTML element embeds raw HTML in your notification. Use it when the other Elemental elements can't produce the structure or styling you need. It renders on email only. The type value is `"html"`. **When to use:** * Custom HTML structures (tables, complex layouts) * HTML that requires specific formatting not available in Elemental elements * Legacy HTML content that needs to be preserved * Custom styling that requires direct HTML/CSS **Basic example** ```json Elemental theme={null} { "type": "html", "content": "

Hello, World!

" } ``` ```javascript Node.js highlight={9-12} theme={null} const { requestId } = await client.send.message({ message: { to: { email: "sarah@acme-corp.com", }, content: { version: "2022-01-01", elements: [ { type: "html", content: "

Hello, World!

", }, ], }, }, }); ``` ```python Python highlight={9-12} theme={null} response = client.send.message( message={ "to": { "email": "sarah@acme-corp.com", }, "content": { "version": "2022-01-01", "elements": [ { "type": "html", "content": "

Hello, World!

", }, ], }, }, ) ``` ```bash cURL highlight={12-15} theme={null} curl -X POST https://api.courier.com/send \ -H "Authorization: Bearer $COURIER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": { "to": { "email": "sarah@acme-corp.com" }, "content": { "version": "2022-01-01", "elements": [ { "type": "html", "content": "

Hello, World!

" } ] } } }' ``` ```ruby Ruby highlight={9-12} theme={null} response = courier.send_.message( message: { to: { email: "sarah@acme-corp.com" }, content: { version: "2022-01-01", elements: [ { type: "html", content: "

Hello, World!

" } ] } } ) ``` ```go Go highlight={5-8} theme={null} // Elemental nodes carry no typed content fields, so pass the document as raw JSON. content := param.Override[shared.ElementalContentParam](json.RawMessage(`{ "version": "2022-01-01", "elements": [ { "type": "html", "content": "

Hello, World!

" } ] }`)) response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{ Message: courier.SendMessageParamsMessage{ To: courier.SendMessageParamsMessageToUnion{ OfUserRecipient: &shared.UserRecipientParam{ Email: courier.String("sarah@acme-corp.com"), }, }, Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content}, }, }) ``` ```java Java highlight={7-9} theme={null} SendMessageParams params = SendMessageParams.builder() .message(SendMessageParams.Message.builder() .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com"))) .content(JsonValue.from(java.util.Map.of( "version", "2022-01-01", "elements", java.util.List.of( java.util.Map.of( "type", "html", "content", "

Hello, World!

"))))) .build()) .build(); SendMessageResponse response = client.send().message(params); ``` ```php PHP highlight={9-12} theme={null} $response = $client->send->message( message: [ 'to' => [ 'email' => 'sarah@acme-corp.com', ], 'content' => [ 'version' => '2022-01-01', 'elements' => [ [ 'type' => 'html', 'content' => '

Hello, World!

', ], ], ], ], ); ``` ```csharp C# highlight={12-15} theme={null} // Elemental nodes expose no typed content fields, so build the document from raw JSON. SendMessageParams parameters = new() { Message = new() { To = new UserRecipient { Email = "sarah@acme-corp.com" }, Content = ElementalContent.FromRawUnchecked( JsonSerializer.Deserialize>(""" { "version": "2022-01-01", "elements": [ { "type": "html", "content": "

Hello, World!

" } ] } """)), }, }; var response = await client.Send.Message(parameters); ``` ```bash CLI highlight={4} theme={null} courier send message \ --api-key "$COURIER_API_KEY" \ --message.to '{"email": "sarah@acme-corp.com"}' \ --message.content '{"version": "2022-01-01", "elements": [{"type": "html", "content": "

Hello, World!

"}]}' ``` ```text MCP theme={null} With Courier MCP, send an email to sarah@acme-corp.com with a raw HTML block. ``` **Fields** **content** `string` *required* The raw HTML. Any valid HTML markup, including CSS styles. **locales** `object` Translations of `content`, keyed by locale code. See [Template translations DOC](/docs/design/elemental/locales). **channels** `string[]` An array of channel names. The HTML element renders only for the listed channels. See [Control flow DOC](/docs/design/elemental/control-flow#channels). **Examples and variants** **Simple HTML** Basic HTML content: ```json theme={null} { "type": "html", "content": "

Hello, World!

" } ``` **HTML table** Create custom HTML tables: ```json theme={null} { "type": "html", "content": "
ItemPrice
Product A$99.99
" } ``` **HTML with Handlebars** Use Handlebars variables in HTML: ```json theme={null} { "type": "html", "content": "

Order #{{order_id}}

Total: ${{order_total}}

" } ``` **HTML with localization** Localize HTML content: ```json theme={null} { "type": "html", "content": "

Welcome!

", "locales": { "es": { "content": "

¡Bienvenido!

" }, "fr": { "content": "

Bienvenue!

" } } } ``` **Channel-specific HTML** Only render HTML for email: ```json theme={null} { "type": "html", "channels": ["email"], "content": "
Email-only HTML content
" } ``` ### Best practices * **Use sparingly**: prefer standard Elemental elements for cross-channel compatibility * **Test thoroughly**: HTML rendering varies across email clients. * **Use inline styles**: email clients often strip ` ``` > [!NOTE] > > If you only use [`CourierClient`](#courierclient) APIs directly, skip the `initialize` and theme steps. ## Authentication Courier authenticates with a **JWT** that your backend mints with your Courier API key, never in client code. [Authenticate users DOC](/docs/in-app/authenticate-users?lang=Android) covers the token flow, scope strings, reading auth state, signing out, and token refresh. Using the EU datacenter? See [EU setup DOC](/docs/in-app/authenticate-users?lang=Android#connect-to-the-eu-datacenter). Credentials persist across app sessions, so you sign in once per user rather than on every launch. ```kotlin theme={null} lifecycleScope.launch { Courier.shared.signIn(accessToken = jwt, userId = "your_user_id") Courier.shared.signOut() } ``` ## Inbox [Add an inbox DOC](/docs/in-app/add-an-inbox?lang=Android) is the setup flow. These are the types it uses on Android. | Type | What it is | | :- | :- | | `CourierInbox()` | The prebuilt inbox as a Jetpack Compose composable | | `com.courier.android.ui.inbox.CourierInbox` | The same inbox as an XML layout view | | `CourierInboxTheme` | Fonts, colors, unread indicators, swipe actions, button styles | | `addInboxListener` | Raw message data and events, for a UI you render yourself | Every callback and action these expose is documented per platform in [Data and actions DOC](/docs/in-app/build-a-custom-inbox?lang=Android#data-and-actions). ### Inbox theme `CourierInboxTheme` is a constructor argument on the composable, and a property on the view. Light and dark themes are separate. ```kotlin theme={null} CourierInbox( modifier = Modifier.padding(innerPadding), canSwipePages = true, lightTheme = yourLightTheme, darkTheme = yourDarkTheme, ... ) ``` ![Default Inbox](https://mintcdn.com/courier-4f1f25dc/A-IH_41Pkuff3UAy/assets/android-inbox-default.webp?fit=max\&auto=format\&n=A-IH_41Pkuff3UAy\&q=85\&s=817cf39df7f5c01b47a61a6d267257fb) ![Styled Inbox](https://mintcdn.com/courier-4f1f25dc/A-IH_41Pkuff3UAy/assets/android-inbox-styled.webp?fit=max\&auto=format\&n=A-IH_41Pkuff3UAy\&q=85\&s=3c22892996909ce834922f4b4434f391) Default and styled Inbox on Android Branding from [Courier Studio](https://app.courier.com/designer/brands) also applies. The SDK reads primary color and footer visibility from your brand settings. ## Push notifications [Set up push notifications GUIDE](/docs/guides/set-up-mobile-push) is the setup flow, from connecting FCM to a device that receives push. It covers the `FirebaseMessagingService` and `CourierActivity` wiring, `setToken`, permission, and reading the payload on tap. > [!NOTE] > > **Firebase is a separate dependency.** From Courier Android 6.x the SDK no longer bundles Firebase Messaging. Declare your own Firebase BoM and `firebase-messaging` artifact so you can subclass `FirebaseMessagingService`. ```gradle theme={null} dependencies { implementation platform('com.google.firebase:firebase-bom:33.1.2') implementation "com.google.firebase:firebase-messaging" } ``` > [!NOTE] > > Push notifications require a physical device and a release build for reliable token registration and delivery. Emulators are best-effort. Two entry points are Android-only, and neither has a counterpart on the other platforms. | Call | When | | :- | :- | | `Courier.shared.refreshFcmToken()` | Force Firebase to hand the SDK a fresh token | | `Courier.shared.isPushPermissionGranted(context)` | Read permission without prompting | ## Preferences [Add a preference center DOC](/docs/in-app/add-a-preference-center?lang=Android) is the setup flow. These are the types it uses on Android. | Type | What it is | | :- | :- | | `CourierPreferences()` | The prebuilt preference center as a composable | | `com.courier.android.ui.preferences.CourierPreferences` | The same view for XML layouts | | `mode` | `Mode.Topic` or `Mode.Channels(listOf(PUSH, SMS, EMAIL))` | | `CourierPreferencesTheme` | Fonts, colors, toggle styles, section headers | > [!NOTE] > > The composable renders the default channels mode. Set `mode` on the XML view for topic mode. > [!NOTE] > > Topics and sections are configured in the [Preferences Editor > GUIDE](/docs/guides/build-a-preference-center#configure-the-topics). See [Preferences Overview > DOC](/docs/recipients/preferences/overview) for how preference enforcement works at send time. ### Preferences theme Light and dark themes are separate, and [Courier Studio branding](https://app.courier.com/designer/brands) applies automatically when you supply a `brandId`.