From 44029fdd7c69e28ffc875650ebc22eeac6c06271 Mon Sep 17 00:00:00 2001 From: Harley Alexander <43975092+mayteio@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:47:37 +0000 Subject: [PATCH 1/8] feat: send transactional email from posthog templates Add posthog.emails.send() on Client and AsyncClient, plus standalone Emails and AsyncEmails in posthog.emails for processes that only send email. Sends go to the app host with a project secret API key and an Idempotency-Key, and retry network errors, rate limits, an in-flight key and 503 with the same key. Errors raise EmailError with PostHog's code and a Resend-style name. Co-Authored-By: Claude Opus 5.5 Generated-By: PostHog Desktop Task-Id: c62b30cd-eb2e-496c-a73d-2a6e7a52a62e --- .sampo/changesets/transactional-emails.md | 5 + posthog/async_client.py | 25 + posthog/client.py | 28 + posthog/emails.py | 590 ++++++++++++++++++++++ posthog/request.py | 13 + posthog/test/test_emails.py | 346 +++++++++++++ typings/requests/__init__.pyi | 2 + 7 files changed, 1009 insertions(+) create mode 100644 .sampo/changesets/transactional-emails.md create mode 100644 posthog/emails.py create mode 100644 posthog/test/test_emails.py diff --git a/.sampo/changesets/transactional-emails.md b/.sampo/changesets/transactional-emails.md new file mode 100644 index 000000000..d9c7d5a32 --- /dev/null +++ b/.sampo/changesets/transactional-emails.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +Send transactional email from PostHog email templates with `posthog.emails.send()`, or with `Emails` and `AsyncEmails` from `posthog.emails` in a process that only sends email. A send needs a project secret API key, goes to the app host, sends an idempotency key, and retries network errors, rate limits and an unavailable service with the same key. Errors raise `EmailError` with PostHog's error `code` and a Resend-style `name`. diff --git a/posthog/async_client.py b/posthog/async_client.py index 186bff727..97ee0dc7d 100644 --- a/posthog/async_client.py +++ b/posthog/async_client.py @@ -70,6 +70,7 @@ try_attach_code_variables_to_frames, ) from .consumer import MAX_MSG_SIZE +from .emails import AsyncEmails from .feature_flag_evaluations import ( FeatureFlagEvaluations, _EvaluatedFlagRecord, @@ -247,6 +248,30 @@ def __init__( self._duplicate_client_registry_key: Optional[tuple[str, str]] = None self._register_duplicate_client() + _emails: Optional[AsyncEmails] = None + + @property + def emails(self) -> AsyncEmails: + """ + Send transactional email from PostHog email templates. + + Needs ``secret_key``, a project secret API key with the ``hog_flow:write`` + scope, and ``posthog[async]``. A disabled client sends nothing. + + Examples: + ```python + await posthog.emails.send( + to="user@example.com", + template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", + distinct_id="user_123", + ) + ``` + """ + if self._emails is None: + self._emails = AsyncEmails(self.secret_key, host=self.host) + self._emails.disabled = self.disabled + return self._emails + async def __aenter__(self) -> AsyncClient: self._ensure_workers_started() return self diff --git a/posthog/client.py b/posthog/client.py index bff6def5d..7099e42c9 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -53,6 +53,7 @@ set_context_session as _context_set_context_session, tag as _context_tag, ) +from posthog.emails import Emails from posthog.exception_capture import ExceptionCapture from posthog._logging import _configure_posthog_logging from posthog.exception_utils import ( @@ -1131,6 +1132,33 @@ def _set_library_identity(self, library_id: str, library_version: str) -> None: def _request_identity_kwargs(self) -> Dict[str, str]: return {"_user_agent": self._sdk_info} if self._sdk_info != _USER_AGENT else {} + _emails: Optional[Emails] = None + + @property + def emails(self) -> Emails: + """ + Send transactional email from PostHog email templates. + + Needs ``secret_key``, a project secret API key with the ``hog_flow:write`` + scope. Sends go to the app host of ``host``. A disabled client sends + nothing. To send email without capturing events, create + ``posthog.emails.Emails`` directly. + + Examples: + ```python + posthog.emails.send( + to="user@example.com", + template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", + template_data={"name": "Ada"}, + distinct_id="user_123", + ) + ``` + """ + if self._emails is None: + self._emails = Emails(self.secret_key, host=self.host) + self._emails.disabled = self.disabled + return self._emails + @property def queue(self) -> Queue: """The analytics lane's queue (kept for backwards compatibility).""" diff --git a/posthog/emails.py b/posthog/emails.py new file mode 100644 index 000000000..11e298e1b --- /dev/null +++ b/posthog/emails.py @@ -0,0 +1,590 @@ +"""Transactional email, sent from an email template in PostHog. + +Use ``posthog.emails`` on a client created with a project secret API key, or +create :class:`Emails` directly in a process that only sends email: + +```python +from posthog.emails import Emails + +emails = Emails("phs_...") +result = emails.send( + to="user@example.com", + template_id="