Repository navigation
docs(workflows): both send forms, idempotency and the send checks on the transactional email API page - #1
Closed
Silthus wants to merge 6 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
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
…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>
Silthus
force-pushed
the
docs/transactional-email-api-code-form
branch
from
October 31, 2026 09:50
641b3bd to
76ad37a
Compare
Silthus
changed the base branch from
docs/transactional-email-api
to
base/transactional-email-docs
October 31, 2026 09:50
…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>
13 of 16 tasks
Owner
Author
|
Moved to PostHog#21819. |
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.
Executive summary
This PR adds the code form of the transactional email send to the API page that PostHog/posthog.com#21564 adds. With the code form, your code sends the subject and the body, and PostHog needs no template. The PR also adds the checks that apply only to the code form, and a
delivery_unavailablesection. Thedocs_urlof the send route's503opens that section.The PR stacks on
base/transactional-email-docs, which is the head of PostHog#21564. It changes one page:contents/docs/workflows/transactional-emails/api.mdx, +59/-1. Before, this PR rewrote our own copy of the page. PostHog#21564 now owns that page, so this PR keeps only the part that PostHog#21564 does not have. Silthus/posthog.com#2 duplicated PostHog#21564 and is closed.Slice 13 of Silthus/posthog#461.
Impact
Pays into Grow Workflows through billed
workflows_emailsfrom API sends, read as Code-sending orgs and Switched orgs. An engineer who moves email off another provider sends from code, so this page is the first page they read. Expected move: small, up.Note
🔵 Merge risk: low · two-way door
workflows-email-from-code.Changes
delivery_unavailable. A new section under "Send an email". It tells the reader that PostHog maybe queued the email, and to retry with the sameIdempotency-Key. The SDKs retry this on their own. The old sentence about the503moved into this section.template_key,to,distinct_id,subject,html,text,from,reply_to,cc,bcc,headers,tags,attachments,capture_engagement), the rules for headers, tags, attachments, and suppressed addresses, and a cURL example.capture_engagement. The code form and a template send both take it, so both tables list it. For content from your code, the default istrue. For a template send, the default is the project's Capture email engagement events setting, and a retry with the sameIdempotency-Keymust send the field as the first request did. Like the code form, this documents the send route of the server stack, notmaster.invalid_request,sender_not_verified, andsender_not_allowed. The errors of every send stay on the overview page.831841b73,b42086e4c,acd4a4b88):reply_totakes addresses without a display name. The reserved headers includeDKIM-SignatureandReceived. A header name has 1 to 126 visible ASCII characters, with no spaces and no colon; a value has at most 870 printable ASCII characters and can have spaces. A tag name or value has at most 256 characters.2502eb65aand673346e75, review fixes of rounds 5 and 6): a code-form retry with the sameIdempotency-Keyis the same request when every field of the table has the same value.template_keygroups the email's metrics and sends. PostHog refuses a tag name that starts withph_in uppercase or lowercase. The template-send table gets acapture_engagementrow. A double blank line after the checks table is gone (scripts/fix-mdx.js).Evidence
#send-an-email,#checks-for-content-from-your-code,/docs/workflows/transactional-emails#errors,/docs/workflows/engagement-events,/docs/workflows/opt-outs#the-suppression-list, and/docs/workflows/configure-channels. I checked each one in the source.masterand the code-form branch of the stack. The same-request rule follows the idempotency fingerprint of the CDP service (transactionalSendFingerprint). Theph_rule follows the tag name check of the route and of the CDP schema, which both ignore case. Thecapture_engagementdefaults follow the route's serializer. The header name rule was checked against the route'sHEADER_NAMEpattern and length limit for every ASCII code point, the empty name, and 126- and 127-character names.673346e75:prettier --checkpasses (.prettierignoreskips.mdx).scripts/fix-mdx.js, theformat:docsstep, leaves the page unchanged (it removed one double blank line in2502eb65a). Vale 3.13.1 with the repo's.vale.ini: 0 alerts on the page.pnpm format's glob does not include.mdx.673346e75: Vale, markdownlint, spelling, semgrep, and the.mdredirect check pass. auto-label fails on the fork (nodocslabel), and the preview build skips on forks.node_modulesfor posthog.com.docslabel.Checklist
vercel.json(no page moved)Decisions
capture_engagementon a template send?capture_engagement, its checks, and thedelivery_unavailableanchor. Close #2.Open: accept the live-run gap: no dev server or rendered page on this box. Recommendation: accept, and check the page in the Vercel preview of PostHog#21564's stack before the merge.
Involvement: no grilling rounds, no prototypes. GPT-6.1 Sol (high) reviewed this revision in round 1 (one finding on the reserved headers) and in three reads of the fixes: the first two found one gap each in the header name rule, both fixed, and the third was clean. QA swarm 2026-11-01: panel round 5 (fresh-eyes, correctness, risk, design, anti-overengineering, sweep, michael): 5 findings, 4 fixed, 1 rejected (the dev-server load, a recorded gap); fixes round 6 (fresh-eyes): 2 findings, both fixed; final read 1 (fresh-eyes) on
673346e75: one nit on this description, fixed here. 7 fixed, 1 rejected. Proof: source checks and Vale; no rendered page.Follow-up:
capture_engagementon a template send is not live: PostHog/posthogmasterdoes not take the field yet. It comes with the code-form send route of the server stack, which this PR waits for (see the merge-risk callout). When that route ships, the overview's same-request list (index.mdx, the base page) becomes wrong for a template send that setscapture_engagement, so its fix must land with this PR or before it.History: the earlier revision on our own page (before the rebase onto PostHog#21564)
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.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