Skip to content

docs(workflows): attach files to emails by upload, then send - #22022

Merged
Silthus merged 59 commits into
masterfrom
docs/transactional-email-sdk-attachments
Nov 13, 2026
Merged

Silthus merged 59 commits into
masterfrom
docs/transactional-email-sdk-attachments

Conversation

@Silthus

@Silthus Silthus commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Merge gate: attachments are open to every project in US and EU, and all eight SDK releases are published. Michael gives the merge word.

Problem

A developer who sends a transactional email from code can now attach files with a new flow: the files go to PostHog first, and the send names them by their hash. The docs still describe the old flow, where the send carries each file as Base64. They also name the old 512 KB limit and say that attachments are temporarily unavailable. A developer who follows them gets a refusal.

This PR rewrites the attachments parts of the transactional email docs for the new flow. It is the docs stack of PostHog/michaelr-context#88.

Impact

Part of the map's goal: developers send larger files from code. The docs teach the flow, so success shows as code sends with files after the feature opens to every project. The map measures it.

Merge Danger

Note

🔵 Merge risk: low · two-way door

  • Docs only: 11 pages in contents/docs/workflows/transactional-emails/. No component, navigation, or build change.
  • Each SDK page names the first release that sends copies and attachments, and the Rust and .NET install lines pin that release. All eight releases are on their registries.
  • Not run: the SDK examples are not compiled or sent. Each SDK section is checked against the SDK's merged code and its registry release.
  • Switch-off: none: live on deploy, fix forward.
  • Tier: non-critical (docs only)

Summary

  • API page: the attachments field now takes {sha256, filename, content_type?, content_id?}. A new Send files section gives the four steps: hash each file, ask where to upload, upload the missing files, then send. It documents the upload route POST /api/projects/@current/messaging/attachments/uploads/ with its request, its present and upload answers, and the upload PUT. Allow the upload host names the Amazon S3 hosts that outgoing-traffic rules must allow. A calendar invite shows content_type with parameters, text/calendar; charset=utf-8; method=REQUEST, and that PostHog refuses a name or filename parameter. Checks for files lists every refusal code with its cause and fix, with the 404 that the upload route returns when content from your code is off or the PostHog version has no upload route.
  • Limit: every page names it the same way as the SDK docstrings: "10 files and 40 MB per message after Base64 encoding, about 29 MB of files". There is no limit for each file.
  • Examples page: the receipt example uploads the invoice, then sends it. The cURL tab is a complete script. The Node.js and Python tabs pass the file's bytes.
  • Overview page: the comparison table lists attachments without the "temporarily unavailable" note.
  • SDK pages (Node.js, Python, Go, .NET, Rust, Ruby, Elixir, PHP): each SDK hashes and uploads for you, checks the limit before it uploads, and sends nothing when an upload fails. Each page links the upload host and names the first SDK version with copies and attachments: Node.js 5.72.0, Python 7.91.0, Go 1.44.0, .NET 2.25.0, Rust 0.33.0, Ruby 3.34.0, Elixir 2.22.0, and PHP 4.25.0.
  • Merge with master: master added "Open and click tracking" to the SDK pages. Each page keeps both sections, attachments first, with master's wording for tracking and sensitive. The bullets that this PR adds use an en dash, as master does since docs(workflows): use en dashes in the transactional email docs #22166.
  • The "temporarily unavailable" notes from docs: Add warning that email attachments are temporarily unavailable #22032 go.

Before the merge

  • Check each SDK section against its merged SDK code: field names, the limit check, and what the SDK does when an upload fails. Add the "from version" of each release to its section.
  • All eight SDK releases are on their registries: npm, PyPI, NuGet, the Go proxy, crates.io, RubyGems, Hex, and Packagist.
  • Check that the upload route and the refusal codes on the API page agree with the live API. The route is live in US and EU: without a key it answers 401, and an unknown path next to it answers 404. The refusal codes need a call with a key.

Evidence

  • Checks on HEAD: green on 0070f3e: 23 checks, including the preview build, Vale (0 errors), Markdown lint, Spelling, Docs .md redirects, CodeQL, and semgrep.
  • Page check (red, then green): a script checks the rules of the ticket: the limit wording, the upload route, every refusal code with the upload route's 404, both upload hosts, no 512 KB, no "temporarily unavailable", no file_too_large, no limit for each file, on each SDK page the hash, the upload, and the host link, that the daily upload limit counts when PostHog gives the upload URL, and that the HTML and text count at their Base64 size. On master, 57 checks fail. On 0070f3e, all pass.
  • The cURL receipt script runs: the script from the examples page, word for word, runs against a fake API that applies the contract: the request shape, the signed checksum header on the upload, no API key on the upload, and attachment_not_uploaded for an unknown hash. The only change is the host. Run 1 uploads the file and gets 202. Run 2 gets present, uploads nothing, and gets 202.
  • Upload host: the presigner and its settings as PostHog's store uses them sign https://<bucket>.s3.us-east-1.amazonaws.com/… for US and https://<bucket>.s3.eu-central-1.amazonaws.com/… for EU. The docs leave the bucket name out and name the host suffix.
  • Formatting: pnpm format covers no .mdx file, so it changes nothing here.
  • Pages load: on the deploy preview of 0070f3e, all 11 pages load at 1440 px in light and dark mode and at 640 px. Every anchor that the pages link exists, such as #send-files, #allow-the-upload-host, #checks-for-files, and #delivery_unavailable. The calendar invite example renders in the list, with its content_type on its own line. No page shows "temporarily unavailable", "512 KB", or file_too_large. The console errors are the same kinds as on posthog.com today.
  • Gaps: an end-to-end send through the real API is not possible yet, because the upload route and the send with references are not live. The opening stack proves it with a real send in each region.

Decisions

Question Answer Who Where
Adopt this PR, or open a second one? Adopt this PR. It covers all 11 pages. 🤖 Agent ticket
Which upload host do the docs name? The region host suffix, such as .s3.us-east-1.amazonaws.com, and "the host of url". The alternative is the exact host. 🤖 Agent ticket
What do the SDK pages promise before the SDK changes are released? Only what all eight SDK PRs share. No SDK error names. The merge checklist checks each page against its release. 🤖 Agent ticket
Check the pages on the dev server or on the deploy preview? The deploy preview, a production build of HEAD. 🤖 Agent ticket
Use an en dash after bold labels, as Vale asks? No. All bullets of that kind on these pages use " - ". The alternative is to change all of them in a separate PR. 🤖 Agent ticket
Name each SDK's "from version" now? Not yet. Each SDK sets its version when it releases, so the merge checklist adds it. The alternative is a predicted version. 🤖 Agent ticket
Which version does each SDK page name? The first registry release whose package has the upload route. The Rust and .NET install lines move to that release. 🤖 Agent ticket
Fix the sensitive: false text that a reviewer flagged on the API page? No, not in this PR. The text is from master, and this PR keeps master's wording for sensitive. It goes to the page owners as a separate fix. 🤖 Agent ticket
Fix the Vale alerts? Fix the 12 alerts on lines that this PR adds or changes: en dashes on bold-label bullets. The other 19 alerts, 16 warnings and 3 suggestions, are on lines that this PR doesn't change. 🤖 Agent ticket
Publish the size of the daily upload limit? No. The API's own error gives no number. The 429 row says to link new files from the email body. 🤖 Agent ticket

Involvement: design pass (GPT-6.1 Sol, 1 finding, fixed); cross-model review, 2 rounds and 1 final read (GPT-6.1 Sol and Claude Opus 5.5: 6 fixed, 1 rejected as a merge-time item); ReviewHog, 4 reviews (1 fixed, 1 answered as a merge-time item, then no findings on 0aa1457, c39f467, and 7a46fb2); QA swarm, 7 lenses on 0aa1457 (1 minor finding, partly fixed, the rest a follow-up), then final reads of c39f467 and 7a46fb2 with no findings; proof captured on 7a46fb2. After the merge with master: final reads of cf97265 and 1ab42fb (GPT-6.1 Sol: 2 fixed, 1 rejected as out of scope), ReviewHog with no findings on cf97265; proof captured on cf97265 and 1ab42fb. After the calendar invite and the merge of #22166: final reads of b811a84, d597bf7, and 0070f3e with no findings, ReviewHog with no findings on d597bf7, and a UI walk that moved the invite's content_type onto its own line; proof captured on 0070f3e.

History: the original scope of this PR (an exact revert of 8196ea6, inline Base64 attachments)

This PR first added the "Copies and attachments" section back to the eight SDK pages, and the Node.js and Python tabs of the receipt example, as a revert of 8196ea65f from #21932. That content described inline Base64 attachments with a 512 KB limit. The API changed to upload, then send, so this PR now rewrites that content. The hold on the eight SDK PRs stays: posthog-js #5514, posthog-python #1172, posthog-go #408, posthog-dotnet #367, posthog-rs #326, posthog-ruby #325, posthog-elixir #239, and posthog-php #292.


🤖 Generated with Claude Code

mayteio and others added 30 commits October 24, 2026 10:38
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>
… Elixir and PHP

Add a page for each SDK with the template send, content from code, the
status read, and the distinct ID guidance. Copies and attachments are
marked as coming in a later version. The API page shows every SDK in its
language tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the placeholders the site fills in, pass the client to the Go and Ruby
helpers, tell "not yet" from "not bounced" in Go, add the .NET usings
and serde_json, and say that flag definitions still load from the
ingestion host with the secret key. Ruby's timeout covers each connect,
read, and write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd review

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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…email-all-sdks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ming-soon notes

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>
…Base64 size

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

@Silthus PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 7a46fb2: no findings.

Silthus and others added 3 commits November 13, 2026 08:03
…email-sdk-attachments

Keeps master's tracking and sensitive sections next to this branch's
copies and attachments sections on the eight SDK pages.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Rust page needs posthog-rs 0.33.0 for copies and attachments, so the
install line now selects 0.33 instead of 0.32.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@Silthus PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed cf97265: no findings.

Python 7.91.0 and .NET 2.25.0 are the first releases with the upload
route. The .NET install line now pins 2.25.0, so the attachments example
compiles.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@Silthus PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 1ab42fb: no findings.

content_type now takes MIME parameters, such as method=REQUEST, so mail
apps can show an .ics file as an invitation. A name or filename
parameter gets not_mime_type, because the file name comes from filename.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@Silthus PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed b811a84: no findings.

…roll

The one-line example hid the content_type value past the edge of the
code block at every width. The file object now takes one line for each
field, and the refusal of a name parameter follows the example.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@Silthus PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed d597bf7: no findings.

Silthus and others added 2 commits November 13, 2026 15:35
Vale's EnDash rule flags the bold-label bullets that this branch adds or
changes. They now use an en dash, as master does since #22166.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…email-sdk-attachments

Brings in the en dashes of #22166 on the transactional email pages.

Refs PostHog/michaelr-context#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@Silthus PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 0070f3e: no findings.

@Silthus
Silthus marked this pull request as ready for review November 13, 2026 15:05
@Silthus
Silthus merged commit b7be60e into master Nov 13, 2026
27 checks passed
@Silthus
Silthus deleted the docs/transactional-email-sdk-attachments branch November 13, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

content PR only touches files under contents/ docs Improvements or additions to product documentation, "Docs"

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants