Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d15e134
docs(workflows): add the transactional email API page
mayteio Oct 24, 2026
810cbed
docs(workflows): document the single send route, email status, and th…
mayteio Oct 29, 2026
80fa5f8
docs(workflows): fix vale findings on the transactional email page
mayteio Oct 29, 2026
ded4a9a
docs(workflows): address SDK review on the transactional email page
mayteio Oct 29, 2026
3fed7e4
docs(workflows): split transactional emails into overview and platfor…
mayteio Oct 29, 2026
b087284
docs(workflows): add a quick start and the Transactional section to t…
mayteio Oct 29, 2026
d55c648
docs(workflows): mention the per-send max_retries in Python
mayteio Oct 29, 2026
38f138c
docs(workflows): send template_id and distinct_id at the top of the body
mayteio Oct 30, 2026
76ad37a
docs(workflows): send content from your code on the transactional ema…
Silthus Oct 31, 2026
831841b
docs(workflows): list every reserved header and the header and tag li…
Silthus Nov 1, 2026
b42086e
docs(workflows): say that a header name has no spaces
Silthus Nov 1, 2026
acd4a4b
docs(workflows): say that a header name has visible ASCII characters …
Silthus Nov 1, 2026
2502eb6
docs(workflows): say what makes a code send the same request, and wha…
Silthus Nov 1, 2026
673346e
docs(workflows): list capture_engagement on the template send, and sp…
Silthus Nov 1, 2026
2885c5b
Merge branch 'master' into docs/transactional-email-api-code-form
Silthus Nov 2, 2026
3f33a25
docs(workflows): cut cc, bcc, headers, tags and attachments from the …
Silthus Nov 2, 2026
7295a66
docs(workflows): match the code form to the first send release
Silthus Nov 2, 2026
2610e8d
docs(workflows): name the first release's field errors and the sender…
Silthus Nov 2, 2026
5cffbd0
docs(workflows): link transactional emails to your users with the ide…
Silthus Nov 3, 2026
51f427b
docs(workflows): say which signup email can leave out the distinct ID
Silthus Nov 3, 2026
c42c7c3
docs(workflows): match the link-emails section to the docs style rules
Silthus Nov 3, 2026
01f7e5f
docs(workflows): fill the SDK examples with the reader's token and host
Silthus Nov 4, 2026
86a0e43
docs(workflows): name the transactional_email scopes for sends and st…
Silthus Nov 5, 2026
8d35205
docs(workflows): pick Transactional email in the key scopes list
Silthus Nov 5, 2026
54968e2
docs(workflows): make a template transactional with the transactional…
Silthus Nov 5, 2026
bd17219
docs(workflows): say to retry a status read right after a send
Silthus Nov 5, 2026
054a8a4
docs(workflows): Python code sends start at 7.84.0
Silthus Nov 5, 2026
6fab67b
docs(workflows): send React Email components through PostHog
Silthus Nov 5, 2026
6c924a7
docs(workflows): say how long a retry with the same key stays safe
Silthus Nov 5, 2026
595a711
Merge branch 'master' into docs/react-email-transactional-email
Silthus Nov 13, 2026
69297a2
docs(workflows): send React Email components with react
Silthus Nov 13, 2026
ee73a15
docs(workflows): fix React Email review findings
Silthus Nov 13, 2026
b68a98a
docs(workflows): name sensitive as an invalid_form cause in the React…
Silthus Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contents/docs/workflows/transactional-emails/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ You can write the email in PostHog, or send content that your code renders. Both
| | Write it in PostHog | Send content from your code |
| --- | --- | --- |
| Who edits it | Anyone on your team, in the PostHog editor, with no deploy | Engineers, in your codebase |
| Best for | Copy that changes often, and teams that want drafts, versions, and a preview | [React Email](https://react.email) or other templates that you already have, and a move from another email provider |
| Best for | Copy that changes often, and teams that want drafts, versions, and a preview | [React Email](/docs/workflows/transactional-emails/react-email) or other templates that you already have, and a move from another email provider |
| What a send carries | `template_id` and the template's variables | `template_key`, `subject`, and `html` or `text` |
| Copies and attachments | Not on the send | `cc` and `bcc`, on the API. Attachments are [temporarily unavailable](/docs/workflows/transactional-emails/api#copies-and-attachments). |

Expand Down
9 changes: 4 additions & 5 deletions contents/docs/workflows/transactional-emails/node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,17 @@ Pass the distinct ID that you pass to `identify()` as `distinctId`. This is usua

From version 5.62.0, `send` also takes content that your code rendered, with no template in PostHog. Use it when your emails already live in your codebase, or when you move from another email provider. Name the email with `templateKey`, and pass `subject` with `html`, `text`, or both. PostHog sends the content as you wrote it.

For a [React Email](https://react.email) component, render it to HTML with `render` from `@react-email/render`, and pass the result as `html`:
From version 5.69.0, you can pass a [React Email](https://react.email) component as `react`, in place of `html`. The SDK renders its HTML and plain text with `@react-email/render`, which you install next to `posthog-node`:

```jsx
import { render } from "@react-email/render"
import { PasswordResetEmail } from "./emails/password-reset"

export async function sendPasswordReset({ userId, email, firstName, resetLink, resetRequestId }) {
const html = await render(<PasswordResetEmail firstName={firstName} resetLink={resetLink} />)
const { data, error } = await posthog.emails.send({
to: email,
templateKey: "password-reset",
subject: "Reset your password",
html,
react: <PasswordResetEmail firstName={firstName} resetLink={resetLink} />,
from: "Hedgebox <security@example.com>",
distinctId: userId,
idempotencyKey: `password-reset-${resetRequestId}`,
Expand All @@ -175,7 +173,8 @@ export async function sendPasswordReset({ userId, email, firstName, resetLink, r
```

- **`templateKey`** - Names the email, such as `password-reset`. PostHog groups the email's metrics and sends under this key, so use the same key every time you send this email.
- **`text`** - If you leave it out, PostHog makes it from `html`.
- **`react`** - A React Email element. See [send React Email components](/docs/workflows/transactional-emails/react-email) for the install, the errors, and Next.js.
- **`text`** - If you leave it out, PostHog makes it from `html`, or the SDK makes it from the rendered `react` element.
- **`from`** - Needs an address on a domain that your project verified. Without it, PostHog uses the project's default sender. `replyTo` takes one address or a list.
- **`sensitive`** - Marks the email as sensitive, for login links, reset tokens, or codes. If you leave it out, the email takes the setting of the template with the same key, or is sensitive when no template has that key. See [sensitive emails](/docs/workflows/transactional-emails/api#sensitive-emails).
- **`tracking`** - What PostHog tracks for the email. If you leave it out, PostHog tracks opens and captures engagement events, and tracks clicks only when the email isn't sensitive. See [open and click tracking](#open-and-click-tracking).
Expand Down
138 changes: 138 additions & 0 deletions contents/docs/workflows/transactional-emails/react-email.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: Send React Email components with PostHog
sidebar: Docs
showTitle: true
---

[React Email](https://react.email) lets you write emails as React components. Keep React Email to write your emails, and send them with the [Node.js SDK](/docs/workflows/transactional-emails/node). Pass the component to `send` as `react`, and the SDK renders its HTML and plain text. PostHog sends the content as you rendered it. You don't need a template in PostHog.

The Node.js SDK takes `react` from version 5.69.0. Before you start, [create a project secret API key](/docs/workflows/transactional-emails#create-an-api-key) with the **Transactional email** scope.

## Install

```bash
npm install posthog-node@5 @react-email/render react-email react react-dom
```

The SDK renders the component with `@react-email/render`, version 2.1.0 or later. It's an optional peer dependency of the SDK, so install it next to `posthog-node`.

## Write an email

Write the email as a component, with the components from `react-email`:

```jsx
// emails/order-shipped.jsx
import { Body, Button, Html, Text } from "react-email"

export function OrderShippedEmail({ firstName, trackingUrl }) {
return (
<Html lang="en">
<Body>
<Text>Hi {firstName}, your order is on its way.</Text>
<Button href={trackingUrl}>Track your order</Button>
</Body>
</Html>
)
}
```

## Set up the client

Reuse the PostHog client your application already creates, and set `secretKey` to your project secret API key. See [set up the client](/docs/workflows/transactional-emails/node#set-up-the-client) for the host and for local evaluation.

```js
// posthog.js
import { PostHog } from "posthog-node"

export const posthog = new PostHog("<ph_project_token>", {
host: "<ph_client_api_host>",
secretKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY,
// This client only sends email, so it doesn't poll flag definitions with the secret key
enableLocalEvaluation: false,
})
```

## Send an email

Pass the component as `react`. The SDK renders it to HTML, makes the plain text version from the HTML, and sends both:

```jsx
// send-order-shipped.jsx
import { OrderShippedEmail } from "./emails/order-shipped"
import { posthog } from "./posthog"

export async function sendOrderShipped({ userId, email, firstName, trackingUrl, orderId }) {
const { data, error } = await posthog.emails.send({
to: email,
templateKey: "order-shipped",
subject: "Your order shipped",
react: <OrderShippedEmail firstName={firstName} trackingUrl={trackingUrl} />,
from: "Shop <shop@example.com>",
distinctId: userId,
// Identifies this one email, so a retry doesn't send it twice
idempotencyKey: `order-shipped-${orderId}`,
})
if (error) {
// An EmailError. Branch on error.code
throw error
}
return data.id
}
```

- **Key:** `templateKey` names the email. PostHog groups the email's metrics and sends under it, so use one key for each component, such as `order-shipped`.
- **Text:** The SDK makes the plain text with `toPlainText` from `@react-email/render`, which keeps the link of each button and drops the markup. To send your own plain text, pass `text`. The SDK then sends your `text` and renders only the HTML.
- **Props:** The SDK renders the element after you call `send`, before it sends the request. Don't change the objects that you pass as props until the send finishes.
- **Sender:** `from` needs an address on a domain that your project verified. Without it, PostHog uses the project's default sender.
- **User:** Pass the distinct ID that you pass to `identify()` as `distinctId`, so PostHog records the send and its [engagement events](/docs/workflows/engagement-events) against that user. See [link emails to your users](/docs/workflows/transactional-emails/node#link-emails-to-your-users).
- **Tracking:** An email with no template in PostHog is sensitive by default, so PostHog doesn't track its clicks. An order email carries no secrets, so you can pass `sensitive: false` to track its clicks. `sensitive` needs version 5.71.0 or later. See [open and click tracking](/docs/workflows/transactional-emails/node#open-and-click-tracking).
- **Retries:** The idempotency key identifies the email. A retry with the same key in the 24 hours after PostHog queues the email doesn't send it again. See [retry safely](/docs/workflows/transactional-emails#retry-safely-with-an-idempotency-key) and [timeouts and retries](/docs/workflows/transactional-emails/node#timeouts-and-retries).

`send` returns `{ data, error }` and doesn't throw. `data.id` is the message ID, which you can pass to [`get`](/docs/workflows/transactional-emails/node#check-an-emails-status) to read the email's status.

## Errors from a React send

A send with `react` can return these error codes. For each one, the SDK sends nothing:

| Code | Cause | Fix |
| --- | --- | --- |
| `invalid_form` | The send has `react` and `html`, or `react` and `templateId`. Or it has `sensitive` on an SDK version earlier than 5.71.0. `error.message` names the field. | `react` replaces `html`, so pass one of them. A template in PostHog takes no `react`, because PostHog renders the template. For `sensitive`, upgrade to version 5.71.0 or later. |
| `missing_react_renderer` | The SDK can't load `@react-email/render`. | Install `@react-email/render` with `react` and `react-dom` next to `posthog-node`. |
| `react_render_failed` | The component threw an error, or didn't finish rendering before the email request timeout. The timeout is 15 seconds by default, and `emailRequestTimeoutMs` on the client changes it. | If the component threw, `error.cause` holds its error. Fix the component, and render it in a test with the same props to see the error. |

For every other error code, see the [errors table](/docs/workflows/transactional-emails#errors).

## Send from Next.js

Send from a route handler or a server action on the Node.js runtime, which is the default. React Email doesn't render in a Next.js route on the edge runtime, whether the SDK renders it or you do. A send with `react` from a route with `export const runtime = "edge"` returns `react_render_failed` after the email request timeout, and sends nothing.

## Render the email yourself

To keep the rendered HTML, such as to store a copy, or to send with an SDK version earlier than 5.69.0, render the component yourself. Render it with `render`, make the plain text with `toPlainText`, and pass both as `html` and `text`:

```jsx
// send-order-shipped-html.jsx
import { render, toPlainText } from "react-email"
import { OrderShippedEmail } from "./emails/order-shipped"
import { posthog } from "./posthog"

export async function sendOrderShipped({ userId, email, firstName, trackingUrl, orderId }) {
const html = await render(<OrderShippedEmail firstName={firstName} trackingUrl={trackingUrl} />)
const { data, error } = await posthog.emails.send({
to: email,
templateKey: "order-shipped",
subject: "Your order shipped",
html,
text: toPlainText(html),
from: "Shop <shop@example.com>",
distinctId: userId,
idempotencyKey: `order-shipped-${orderId}`,
})
if (error) {
throw error
}
return data.id
}
```

The SDK sends content from your code from version 5.62.0. If you leave out `text`, PostHog makes it from `html`.
4 changes: 4 additions & 0 deletions src/navs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8400,6 +8400,10 @@ export const docsMenu = {
name: 'API',
url: '/docs/workflows/transactional-emails/api',
},
{
name: 'React Email',
url: '/docs/workflows/transactional-emails/react-email',
},
{
name: 'Examples',
url: '/docs/workflows/transactional-emails/examples',
Expand Down
Loading