Repository navigation
docs(workflows): send React Email components through PostHog - #21984
Conversation
Document how to make a Workflows email template transactional and send it with POST /api/projects/:project_id/messaging_templates/:template_id/send/. The page covers the API key, the request and response, idempotency keys, error codes, rate limits, and how transactional emails behave. The page is marked beta. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: a0274fa0-d9be-48bf-b601-f153decf241d
…e SDKs - Send through `POST /messaging/send/email/` with `template.id`. The template route still works. - Read an email's status with `GET /messaging/emails/:message_id/`: statuses, reasons, 404 cases, scopes and read limits. - Send and read with the Python and Node.js SDKs. - Add `recipient_unreachable` to the errors table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
- The Node.js example turns off local evaluation, and both examples keep the SDK's error. - The status examples handle the error first and explain retrying a not_found. - The SDK section adds install, client reuse, timeouts and retries, uncertain outcomes, and a no-network testing example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…m pages The overview keeps the template, key, idempotency, errors, statuses and behavior. Node.js, Python and API pages hold the setup, send, status, retries and testing for each platform, with a sidebar group like push notifications. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…he overview - A quick start links to the Node.js, Python and API pages. - Creating an email follows the Transactional section's wizard, and the hidden workflow explanation is gone. - Agents can set it up with the PostHog MCP server. - Protect sensitive data turns off click tracking and keeps deliveries, opens and statuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
The send endpoint now takes a flat body (PostHog/posthog#126287), so the field table, the curl example and the engagement events note use template_id and distinct_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…il API page Document the code form of the send route (template_key with the subject and body), capture_engagement, the checks that only apply to it, and a delivery_unavailable anchor for the 503's docs link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mits Add DKIM-Signature and Received to the reserved headers, the header name and value limits, the tag length, and that reply_to takes bare addresses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t template_key and ph_ tags do - A retry with the same Idempotency-Key is the same request when every field of the code-form table matches. - template_key groups the email's metrics and sends. - A tag name that starts with ph_, in any case, is refused. - capture_engagement on a template send defaults to the project's setting. - Drop a double blank line after the checks table (fix-mdx). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ell out the ph_ case rule - The template-send table gets a capture_engagement row with the project default, and says a retry sends the field the same way. - The code-form row keeps only its own default. - A ph_ tag name is refused in uppercase or lowercase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # contents/docs/workflows/transactional-emails/api.mdx # contents/docs/workflows/transactional-emails/node.mdx # contents/docs/workflows/transactional-emails/python.mdx
…code send The first release sends content from code with template_key, subject, html and text only. React Email output is rendered by the app and sent as html. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop capture_engagement from the template send: the send route refuses it there, with the other fields of content from your code. - Fold sender_not_allowed into sender_not_verified, the code the first release returns for a from address no verified sender can use. - Add "Send content from your code" to the Node.js and Python pages, with a React Email example rendered by the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in a replay - A field error is a 400 with type validation_error, a field code such as required or invalid, and attr naming the field. - A replay also compares the sender PostHog chose, so a retry without from after the default sender changed returns idempotency_key_reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntify distinct ID Add "Link emails to your users" to the API, Node.js and Python pages: pass the distinct ID you pass to identify(), what happens without one today (a separate person keyed by the email address), and that invites and signup checks can leave it out. The overview and the API tables point there instead of "always send it". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Node.js and Python client examples used <ph_project_api_key> and a hard-coded ingestion host, which the site doesn't replace. They now use <ph_project_token> and <ph_client_api_host>, as the SDK pages do, so a logged-in reader sees their own project token and host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atus reads A send needs transactional_email:write and a status read needs transactional_email:read or :write. Keys with a hog_flow scope still work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The key steps still picked Workflow, which creates a hog_flow key. The scopes list names the new scope Transactional email. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… email scope Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A status read right after a send can answer 404 for a few seconds, until the send's first status arrives. Say so where the API page lists the 404, and that a retry after a short wait is the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-python#1171 ships as a minor bump, and PyPI is already at 7.83.2, so the code send arrives in 7.84.0, not 7.83.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A React Email developer keeps React Email to write emails, renders each component in the app with render and toPlainText from react-email, and sends the HTML and the text with the Node.js SDK. The page sits under Send transactional emails, and the Node.js page links to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploy preview
Changed pages
|
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 6fab67b: 2 Should fix.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle reportTotal JS (gzip)12.06 MiB (-5.9 KiB / -0.0%) Largest changed named chunks
Eager graph (modules shipped in each entrypoint's initial chunks)
Largest modules in the
|
| Module | Size |
|---|---|
css ./node_modules/.pnpm/css-loader@5.2.7_webpack@5.101.3/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[1]!./node_modules/.pnpm/postcss-loader@4.3.0_postcss@8.5.6_webpack@5.101.3/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[2]!./src/styles/global.css |
1022.3 KiB |
./src/components/Stickers/Stickers.tsx |
696.4 KiB |
./node_modules/.pnpm/@posthog+icons@0.36.6_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
354.8 KiB |
./node_modules/.pnpm/react-markdown@8.0.7_@types+react@16.14.66_react@18.3.1/node_modules/react-markdown/lib/react-markdown.js + 88 modules |
351.4 KiB |
./node_modules/.pnpm/framer-motion@14.0.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/framer-motion/dist/es/render/components/motion/proxy.mjs + 126 modules |
287.2 KiB |
./node_modules/.pnpm/cloudinary-core@2.14.0_lodash@4.17.21/node_modules/cloudinary-core/cloudinary-core.js |
281.9 KiB |
./src/components/SearchUI/index.tsx + 87 modules |
273.1 KiB |
./gatsby-browser.tsx + 59 modules |
258.5 KiB |
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/construction-1.mjs |
248.5 KiB |
./src/components/Pricing/PricingSlider/Slider.tsx + 87 modules |
240.2 KiB |
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/construction-2.mjs |
228.2 KiB |
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/magnifying-glass.mjs |
221.7 KiB |
./src/hooks/useProducts.tsx + 24 modules |
155.6 KiB |
./src/hooks/useProduct.ts + 9 modules |
149.4 KiB |
./src/components/Icons/Icons.tsx |
147.4 KiB |
Eager-graph budgets are report-only until a baseline is established. Sizes are gzip of public/**/*.js; eager size is webpack module source bytes for the modules actually shipped in the entrypoint's initial chunks (post-tree-shake).
Take master's transactional email pages, which now hold the squashed base of this PR and the later tracking, sensitive and attachment changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-node 5.69.0 renders a React Email element passed as react, and makes the plain text from it. Teach that first, list its two error codes and the edge runtime limit, and keep the explicit render as the alternative. The Node.js page's React Email example now passes react too. Refs PostHog/michaelr-context#48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Vale prose linter → found 38 errors, 2 warnings, 0 suggestions in your markdown Full report → Copy the linter results into an LLM to batch-fix issues. Linter being weird? Update the rules!
|
| Line | Severity | Message | Rule |
|---|---|---|---|
| 113:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 114:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 115:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 116:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 133:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 134:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 135:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 158:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 159:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 160:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 161:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 162:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 188:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 189:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 190:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 191:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 255:42 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 256:74 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 257:42 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 258:39 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 259:35 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 283:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 284:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 285:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 286:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 287:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 288:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
contents/docs/workflows/transactional-emails/node.mdx — 11 errors, 2 warnings, 0 suggestions
| Line | Severity | Message | Rule |
|---|---|---|---|
| 12:127 | warning | Avoid trivializing words. 'easy to' can sound dismissive to the reader. | PostHogDocs.Trivializers |
| 54:72 | warning | Capitalize 'Feature Flags' for PostHog's product. Use 'feature flags' for the general industry concept. | PostHogBase.ProductNames |
| 146:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 147:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 175:20 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 176:14 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 178:13 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 179:18 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 180:17 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 181:26 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 198:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 199:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
| 200:1 | error | Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. | PostHogBase.EnDash |
List invalid_form in the React send's error table, name the timeout setting and what error.cause holds, say that sensitive needs 5.71.0, narrow the edge limit to Next.js edge routes, where the self-rendered path fails too, and link the overview's React Email mention to the page. Refs PostHog/michaelr-context#48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Email guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@PostHog review |
|
@Silthus PostHog Review is reviewing this pull request. |
Still owed after merge
|
#22088 merged and #22084 closed, so no open PR rewrites the capture_engagement, from, and engagement bullets any more. Change their hyphens to en dashes, and the hyphens in the new bullets that #22088 and #21984 added. The "Copies and attachments" list in api.mdx stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(workflows): use en dashes in the transactional email docs Replace the hyphen that separates a term from its description with an en dash, as the style guide and Vale's EnDash rule require. Change "rules that are easy to get wrong" to "rules that agents often get wrong". Leave the "Copies and attachments" list in api.mdx and the capture_engagement and engagement bullets alone: other open PRs rewrite those lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(workflows): use en dashes on the lines that are now free #22088 merged and #22084 closed, so no open PR rewrites the capture_engagement, from, and engagement bullets any more. Change their hyphens to en dashes, and the hyphens in the new bullets that #22088 and #21984 added. The "Copies and attachments" list in api.mdx stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(workflows): use en dashes on the bullets that Vale misses Vale's EnDash rule needs a letter before " - ", so it misses a bullet whose term ends in bold or code, such as "**API** - Copy" and "**`text`** - If". The review panel found 9 of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Changes
A developer who writes emails with React Email has no page that shows how to send them through PostHog. From posthog-node 5.69.0,
posthog.emails.sendtakes a React Email element asreactand renders the HTML and the plain text itself. The docs don't mentionreactyet, and the Node.js page's React Email example still renders the HTML by hand.This PR adds Send React Email components with PostHog under Send transactional emails, and a React Email sidebar entry after API. The page shows:
@react-email/render, the optional peer dependency that the SDK renders with;react-emailcomponents, and the client setup;react, with the plain text that the SDK makes, and how to send your owntext;missing_react_rendererandreact_render_failed, and theinvalid_formrefusals, includingsensitiveon an SDK earlier than 5.71.0;renderandtoPlainText, and sendhtmlandtext.The Node.js page's React Email example now passes
react, and its field list namesreactand links to the new page.Ticket: https://github.com/PostHog/michaelr-context/issues/48 (private). The base PR, #21819, merged. This branch merges
masterin and keeps the later changes of #22088 (tracking and sensitive) and #22032 (attachments).This update continues the first version of this PR, from 2026-11-05, on the same branch, with its commits and authorship kept.
Impact
Pays into the Workflows goal: a code send is a billed Workflows email. A React send reaches PostHog as the same request as any other code send, so no metric counts the sends that this page causes. The map's metric, code sends by integration, counts the framework adapters, not this page.
Note
🔵 Merge risk: low · two-way door
fetchstubbed. Each error in its table and its Next.js edge claim ran against the real SDK.reactsend in place of arendercall. That snippet is a fragment, and no script runs it. Its residual risk is cosmetic: a copy that fails shows its error at once.Sidebar seam.
src/navs/index.jsis a shared file. Three other docs PRs add one entry each under the same parent: Nodemailer (#21987) after Node.js, and Rails (#21986) and Better Auth after API. This PR adds one entry after API and reorders nothing. The PR that merges second resolves a one-line conflict.Sidebar before and after
The sidebar gets one entry, React Email, after API. The 640 px rows open the docs navigation.
b68a98a)Node.js page, Send content from your code
New page
Evidence
ui-walk(the screenshots above, from the preview ofb68a98a) andlive-run(gap: the map sends one live email from each branch at its head SHA before release, in one session. This PR runs no live send of its own).b68a98a: every check passes: the preview build, Vale, markdownlint, Spelling, broken links, Docs .md redirects, Semgrep, CodeQL, and Wiz..mdcopy. The preview serves no.mdcopy for any page, including unchanged ones, such as/docs/feature-flags.md.src/navs/index.js.pnpm formatdoesn't cover.mdxfiles.b68a98a. A script reads the page, installs exactly itsnpm installline with pnpm, writes each code block to the file that its first-line comment names, and runs both sends against a stubbedfetchwith the real posthog-node 5.71.0,@react-email/render2.3.1, andreact-email6.11.12. Each send makes onePOSTto/api/projects/@current/messaging/send/email/withtemplate_key,subject,to,distinct_id, the secret key, and theIdempotency-Keyheader. Thereactsend passes the element and nohtml. The SDK renders the HTML, and thetextistoPlainTextof the HTML, with the button's link. Noreactfield goes on the wire. The self-rendered send produces the same HTML and text.6c924a7, before this change, fails: that page has noreactsend and no self-rendered send to run.react_render_failedwith the cause.reactwithhtml, andreactwithtemplateId, returninvalid_form. On posthog-node 5.70.0,sensitivereturnsinvalid_form; on 5.71.0 it goes on the wire. A send with your owntextsends that text and the rendered HTML. In an app without@react-email/render, areactsend returnsmissing_react_renderer. None of these sends a request.react_render_failedafter the 15-second request timeout and sends nothing.Decisions
reactor an explicitrenderfirst?reactfirst, replacing the explicitrenderas the main send; the explicit render stays as the alternative@react-email/renderby name?Involvement: no grilling round for this PR; reviews: the first version had GPT-6.1 Sol (round 1 and a final read) and ReviewHog (two rounds). This update had a cross-model panel of seven lenses (Claude Opus 5.5 and GPT-6.1 Sol), a narrow round 2 on the fixes, and a final read; proof captured.
Checklist
vercel.json(no page moved)🤖 Generated with Claude Code