Repository navigation
docs(workflows): both send forms, idempotency and the send checks on the transactional email API page - #1
Draft
Silthus wants to merge 8 commits into
Conversation
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
Silthus
added a commit
that referenced
this pull request
Oct 22, 2026
|
Vale prose linter → found 0 errors, 38 warnings, 1 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 |
|---|---|---|---|
| 22:170 | warning | Capitalize 'Logs' for PostHog's product. Use 'logs' for the general industry concept. | PostHogBase.ProductNames |
| 57:1 | warning | Use 'PostHog' instead of 'posthog'. | Vale.Terms |
| 76:68 | warning | 'awaitable' is a possible misspelling. | PostHogBase.Spelling |
| 120:273 | warning | Capitalize 'Logs' for PostHog's product. Use 'logs' for the general industry concept. | PostHogBase.ProductNames |
| 221:190 | warning | Capitalize 'Logs' for PostHog's product. Use 'logs' for the general industry concept. | PostHogBase.ProductNames |
| 304:98 | suggestion | Address the reader directly. Use 'you' instead of 'the user'. | PostHogDocs.DirectAddress |
| 308:139 | warning | 'admin's' is a possible misspelling. | PostHogBase.Spelling |
| 319:31 | warning | Use 'ID' instead of 'id'. | Vale.Terms |
| 412:45 | warning | 'Resend's' is a possible misspelling. | PostHogBase.Spelling |
| 421:86 | warning | 'Resend's' is a possible misspelling. | PostHogBase.Spelling |
| 421:170 | warning | 'Resend's' is a possible misspelling. | PostHogBase.Spelling |
| 472:207 | warning | 'Resend's' is a possible misspelling. | PostHogBase.Spelling |
| 476:268 | warning | Use 'PostHog' instead of 'posthog'. | Vale.Terms |
| 482:208 | warning | 'send's' is a possible misspelling. | PostHogBase.Spelling |
| 508:32 | warning | Use 'ID' instead of 'id'. | Vale.Terms |
| 561:4 | warning | Use 'ID' instead of 'id'. | Vale.Terms |
| 561:54 | warning | 'send's' is a possible misspelling. | PostHogBase.Spelling |
| 588:60 | warning | 'preheader' is a possible misspelling. | PostHogBase.Spelling |
| 588:192 | warning | 'preheader' is a possible misspelling. | PostHogBase.Spelling |
| 593:42 | warning | 'preheader' is a possible misspelling. | PostHogBase.Spelling |
| 596:131 | warning | 'preheader' is a possible misspelling. | PostHogBase.Spelling |
| 676:55 | warning | 'preheader' is a possible misspelling. | PostHogBase.Spelling |
| 763:54 | warning | 'preheader' is a possible misspelling. | PostHogBase.Spelling |
| 773:4 | warning | 'upload's' is a possible misspelling. | PostHogBase.Spelling |
| 841:44 | warning | 'send's' is a possible misspelling. | PostHogBase.Spelling |
| 902:134 | warning | Use 'ID' instead of 'id'. | Vale.Terms |
| 915:256 | warning | Use 'PostHog' instead of 'posthog'. | Vale.Terms |
| 925:404 | warning | Use 'ID' instead of 'id'. | Vale.Terms |
| 944:30 | warning | Capitalize 'Workflows' for PostHog's product. Use 'workflows' for the general industry concept. | PostHogBase.ProductNames |
| 945:30 | warning | Capitalize 'Workflows' for PostHog's product. Use 'workflows' for the general industry concept. | PostHogBase.ProductNames |
| 1083:80 | warning | 'Resend's' is a possible misspelling. | PostHogBase.Spelling |
| 1121:97 | warning | Use 'PostHog' instead of 'posthog'. | Vale.Terms |
| 1190:311 | warning | Capitalize 'Logs' for PostHog's product. Use 'logs' for the general industry concept. | PostHogBase.ProductNames |
| 1224:21 | warning | Use 'PostHog' instead of 'posthog'. | Vale.Terms |
| 1228:93 | warning | 'templateId' is a possible misspelling. | PostHogBase.Spelling |
| 1228:108 | warning | 'templateKey' is a possible misspelling. | PostHogBase.Spelling |
| 1228:137 | warning | Use 'HTML' instead of 'html'. | Vale.Terms |
| 1232:46 | warning | 'templateId' is a possible misspelling. | PostHogBase.Spelling |
| 1240:82 | warning | Use 'ID' instead of 'id'. | Vale.Terms |
…hecks The page now follows the flat send contract of PostHog/posthog#120921 for posthog-node and posthog-python: the template form by template_id, the code form by template_key with rendered content, React Email and local templates, client setup with secret_key, app_host and max_retries, what a 202 means, idempotency and the SDK retries, engagement capture, and the error names. New anchored sections cover the send checks, the idempotency codes and missing_app_host. The file helpers of the earlier posthog-node draft are gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fail the job in the job runner example, match the cURL hash to the upload example, and say that adoption needs an upload without if_code_managed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A send whose content PostHog already holds keeps working after people edit the template in PostHog. Only a send with changed content fails with template_edited_in_posthog. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Before an upload claims a template that people edit in PostHog, a local template send whose hash PostHog remembers still goes out, with template_superseded. Only content PostHog never published needs an upload and fails with template_edited_in_posthog. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…l email page Copies, headers, tags and attachments on the code form, the status read by ID and by recipient, the unknown-field `invalid_request`, the paused-workflow `transactional_email_paused` after the key lookup, the replay scope, `422 idempotency_key_reused` on both routes, HTML without text, `from_` in Python, `sender_not_allowed`, and the SDK's local check codes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… optional Co-Authored-By: Claude Opus 5.5 <[email protected]>
…22 on retry Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d template Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
added a commit
that referenced
this pull request
Oct 23, 2026
Silthus
force-pushed
the
docs/transactional-email-api
branch
from
October 23, 2026 14:25
2b0a7a2 to
8ee6d24
Compare
Silthus
force-pushed
the
docs/transactional-email-api-code-form
branch
from
October 23, 2026 14:25
6c55cf4 to
641b3bd
Compare
4 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No upstream PR: this page change lives only in the fork. It stacks on Silthus/posthog.com#2, which moved here from PostHog/posthog.com#21306 (closed on 2026-10-25).
Changes
Engineers who move transactional email off Resend need one page that shows how to send from code with the PostHog client they already use. The page taught the earlier
posthog-nodedraft (templateFile(),uploadTemplate(), "sends are not idempotent"). It now teaches the final send contract of PostHog/posthog#120921, in Node.js and Python, and every error code that the server and the SDKs emit has an anchor.Slice 13 of Silthus/posthog#461, strand Silthus/posthog#538.
Impact
Pays into Grow Workflows through billed
workflows_emailsfrom API sends, read as Code-sending orgs and Switched orgs (not measured yet: the send attribution of PostHog/posthog#120628 plus its insight on a dedicated dashboard). In scope: Messaging, email. Expected move: small, up. EverydocsUrlan SDK returns opens a section of this page.Note
🔵 Merge risk: low · two-way door
New in
aed49fa8cto641b3bd85: the final contractThe 2026-10-22 and 2026-10-23 entries of the contract, against the server and SDK branches of the PRs below.
ccandbccaddresses). None of them work withtemplate_id.400 invalid_request, with the server'sattr,detail,why, andfix. The HTTP section and theinvalid_requestsection quote it.transactional_email_paused: quotes the new server text. A paused workflow is checked after the idempotency key lookup, so a retry of a queued send still gets its202. When a stored template's workflow is deleted, the send refuses before the key lookup. Rendered content whose key names no stored template gets a new workflow instead, so a retry answers422 idempotency_key_reused.idempotency_key_reusedis the422code on both send routes.invalid_idempotent_requestis its SDKname. One set of keys covers both routes.textfromhtml. When the HTML has no text, such as an email that is only an image, the email goes out without a plain-text part.from_.sender_not_allowed(the address isn't on a verified domain) andsender_not_verified(nofrom, and no verified default sender) quote the server text.GET messaging/emails/{id}/andposthog.emails.get(id)in Node.js, the statuses in order, the optionalreason, 30 days of retention, and the404for a new email. posthog-python has no read yet.GET messaging/emails/?to=<address>, short. Newest first and paged, as the facade of PostHog/posthog#121898 returns them. The API route and its contract amendment are pending (see Open).invalid_form,invalid_template_id,invalid_attachment, andinvalid_message_idget anchors, with posthog-node's messages.network_errorgets thenameservice_unavailable.hog_flow:writealso reads an email's status. A project secret API key uploads templates, sends them, and reads an email's status.What the page covers
template_idwithtemplate_data, and the code form bytemplate_key: rendered content, and local templates that the SDK uploads when they change.secretKey/secret_key,appHost/app_hostfor any host that isn't PostHog Cloud (and why the secret key never goes to an ingestion proxy),maxRetries/max_retries.MultiLanguagetabs), rendered content, React Email rendered tohtml, local templates and their sync loop, rolling deploys, the sender, and copies, headers, tags, and attachments.distinct_id, and why. Every send example passes it.202is queued, not delivered. The checks that would skip a send run first.409, the422, what the SDKs retry,max_retries=0with a key from the email's purpose for job runners, and the promise: one send per key on retries, not exactly-once delivery.{ data, error }in Node.js,EmailErrorin Python, the status tonametable with Resend's names), read an email's status, and send over HTTP (cURL with the wire fields and theIdempotency-Keyheader, and a parameter-to-request table).templateFile()anduploadTemplate()sections, posthog-node's source detection,template_file_unreadable,template_missing_subject, and "Sends are not idempotent".Not merged yet
The page documents these as decided. Hold this PR until they ship:
template_edited_in_posthog,sender_not_verified, derivedtext.202(sender_not_allowed,recipient_suppressed,sending_suspended,template_paused).emails.get).Evidence
id, and its section quotes the server's or the SDK's strings;nameof the contract is on the page;templateFileor "so every email has a text version" (quoted server text in code blocks is exempt);#link resolves.origin/feat/email-status-read(2cb033ad613) andorigin/feat/email-send-attachments, posthog-node atfeat/node-emails-get, and posthog-python atfeat/emails-code-form.9e73da0bf(the page before this revision), checked on the page source: 20 failures. The newinvalid_request,sender_not_verified,not_found, andtransactional_email_pausedtexts were missing, the four local check codes had no heading, and the page still said "so every email has a text version".931fdb8f1, served withgatsby serve:48/48codes anchored with matching text, 14 send examples all pass a distinct ID, 142 in-page links resolve.641b3bd85changes three sentences of prose only.pnpm build:minimalpasses: 3,982 pages, exit 0.-q 8 -I .codespellignore): no change. markdownlint passed in the pre-commit hook. No em or en dashes.Screenshots
Production build (
pnpm build:minimal,gatsby serve) at931fdb8f1, light, 1440 px. Example data only:example.comaddresses and invented IDs.invalid_request: unknown fields, copies, headers, tags, attachmentstransactional_email_paused(before641b3bd85scoped the deleted-workflow sentence)docs_urlforinvalid_attachment, opened by plain navigationAnchor check output (green, production build of 931fdb8)
Anchor check output (red, source of 9e73da0)
Earlier evidence, before the final contract (ce1b5ea to 6c55cf4)
Local dev server (
GATSBY_MINIMAL=true pnpm start) atce1b5ea56, before the review fixes, light, 1440 px. Example data only:example.comaddresses and invented IDs.distinct_id, what a successful send meanssender_not_verifiedtosending_suspendedtemplate_pausedtoidempotency_key_reuseddocs_urlfortemplate_edited_in_posthog, opened by plain navigationAnchor check output (green, b7c65ae)
Anchor check output (red, 2b0a7a2)
Open contract questions
Each is a place where the page, the contract, and the code don't agree yet. The page follows the contract unless the line says otherwise.
app_host_required, notmissing_app_host, the client's retry option isemail_request_max_retries,sendtakes no per-sendmax_retries(so the job runner example doesn't run there yet), and the result field isid, where the contract saysmessage_id. The page documents the contract names, usesresult.id, and anchorsapp_host_requiredtoo.template_repository_missing. Confirm, or the code form keeps source detection.template_repository_missingfix says "posthog-node detects it from the git checkout or the CI environment", and the change prompt saysposthog.emails.templateFile(). Both need a server change in the change request stack.replayedin Node.js. The server returnsreplayed, and posthog-python exposes it. posthog-node'sdatais{ id, warnings }, as in the contract. The page mentionsreplayedfor Python and HTTP only.<title>. Sending it as a local template needs the app to passsubjectandhtmlseparately, and whetherhtmlkeeps the<title>changes the hash. The page says to pass the subject assubject. The byte-for-byte round trip isn't proven.Closed since the last revision
422code:idempotency_key_reusedon both send routes, with the SDKnameinvalid_idempotent_request.fromin Python:from_.sender_not_allowed: the send checks define it, with server text. The page quotes it.transactional_email_paused(the workflow) andtemplate_paused(the send policy) both stay, with separate meanings. So dosender_unavailable(the upload) andsender_not_verified(the send).nameofnetwork_error:service_unavailable.app_host: the client takes it now. The rest of that question stays open as question 1.Decisions
docs_urlthe server builds today and after the contract lands opens a section.from_, as the contract now names it. Replacesfromwith no Python example.Open: the 6 contract questions above.
Involvement: 0 grilling rounds, 0 prototypes, 2 GPT-6.1 Sol review rounds (3 findings in the first, 2 in the second, all fixed), 6 final reads of the fixes (reads 1, 2, 4, and 5 found 1 finding each, all fixed; read 3 ran after Michael overrode the arbiter and was clean; read 6 was clean), proof captured (anchor check red and green, production build, Vale, codespell, markdownlint, screenshots).
Checklist
vercel.jsonWritten by Claude Opus 5.5 (
claude-opus-5-5) as the docs-api build lane of the map. Reviewed by GPT-6.1 Sol (high).🤖 Generated with Claude Code