diff --git a/contents/docs/workflows/transactional-emails/api.mdx b/contents/docs/workflows/transactional-emails/api.mdx index 9f88ac03256b..c94dbf6b7c07 100644 --- a/contents/docs/workflows/transactional-emails/api.mdx +++ b/contents/docs/workflows/transactional-emails/api.mdx @@ -34,7 +34,7 @@ Send a JSON body with these fields: | `template_id` | Yes | The ID of the transactional template to send. | | `to` | Yes | The email address of the recipient. One address for each request. | | `message_data` | No | An object with the values for the template's `{{ variables.* }}` placeholders. For example, `{"reset_link": "https://example.com/reset"}` fills `{{ variables.reset_link }}`. The template can't read person properties, so pass all the data that the email needs here. | -| `distinct_id` | No | The distinct ID of the recipient's person. PostHog records the send and its [engagement events](/docs/workflows/engagement-events) against this person. If you leave it out, PostHog uses the email address as the distinct ID, which might not match the person in your product. We recommend that you always send it. | +| `distinct_id` | No | The distinct ID of the recipient's person. PostHog records the send and its [engagement events](/docs/workflows/engagement-events) against this person. Pass the distinct ID that you pass to `identify()`. See [link emails to your users](#link-emails-to-your-users). | In the template, use the same names that you send in `message_data`: @@ -74,7 +74,64 @@ curl -X POST "https://us.posthog.com/api/projects/@current/messaging/send/email/ The older route, `POST /api/projects/@current/messaging_templates/:template_id/send/`, still works. It takes the template ID in the URL and the distinct ID as `identifiers.distinct_id`. -When a send fails, the response carries a `code`. See [errors](/docs/workflows/transactional-emails#errors). After a `503`, PostHog might have queued the email. Retry with the same `Idempotency-Key` rather than sending the email through another provider, or the recipient can get it twice. +When a send fails, the response carries a `code`. See [errors](/docs/workflows/transactional-emails#errors) and the [checks for content from your code](#checks-for-content-from-your-code). + +### `delivery_unavailable` + +A `503` with the code `delivery_unavailable` means that PostHog couldn't confirm that it queued the email. PostHog might still send it. Retry with the same `Idempotency-Key` rather than sending the email through another provider, or the recipient can get it twice. The SDKs retry this on their own. + +## Link emails to your users + +Pass the distinct ID that you pass to `identify()`, usually the ID your app gives that user, as `distinct_id`. PostHog then records the send, and its [engagement events](/docs/workflows/engagement-events) such as sent, opened, and clicked, against that user's person, next to their product events. + +- **If you leave it out,** PostHog records the email against a person whose distinct ID is the recipient's email address. Unless your app identifies users by their email address, that's a separate person from your user. +- **For someone who isn't a user yet,** such as an invite or an email address check before the account exists, there's no distinct ID to pass. Leave `distinct_id` out. + +## Send content from your code + +You can also send the subject and the body from your code, without a template in PostHog. Use this when your code renders the email. For a [React Email](https://react.email) component, render it to HTML with `render` from `@react-email/render`, and send the result as `html`. Name the email with `template_key`, and send the content in the body. Use the same `POST` route and headers as for a [template send](#send-an-email). + +| Field | Required | Description | +| --- | --- | --- | +| `template_key` | Yes | A name for this email, such as `password-reset`. It has at most 400 letters, digits, hyphens (`-`), and underscores (`_`). PostHog groups the metrics and the sends of this email under the key, so use the same key every time you send this email. | +| `to` | Yes | The email address of the recipient. | +| `distinct_id` | No | The distinct ID of the recipient's person, as for a template send. See [link emails to your users](#link-emails-to-your-users). | +| `subject` | Yes | The subject of the email. | +| `html` | One of `html` and `text` | The HTML body. PostHog sends it as you wrote it and renders no placeholders. | +| `text` | One of `html` and `text` | The plain-text body. If you leave it out, PostHog makes it from `html`. | +| `from` | No | The sender, as `security@example.com` or `Hedgebox `, on a domain that your project verified. If you leave it out, PostHog uses the project's default sender. | +| `reply_to` | No | A list of up to 10 addresses that replies go to, without a display name. | +| `capture_engagement` | No | Whether PostHog captures the email's [engagement events](/docs/workflows/engagement-events). The default is `true`. Send `false` to turn capture off for this email. If the project's **Capture email engagement events** setting is off, PostHog captures nothing, whatever you send. | + +A request has `template_id` or `template_key`, not both. Content from your code takes no `message_data`, because PostHog renders nothing. A template send takes none of `subject`, `html`, `text`, `from`, `reply_to`, and `capture_engagement`, because the template uses its own settings. PostHog returns a `400` that names the field in `attr`. + +If the `to` address is on the [suppression list](/docs/workflows/opt-outs#the-suppression-list), PostHog sends nothing and returns `recipient_suppressed`. + +The response is the same `202` as for a template send. A retry with the same `Idempotency-Key` is the [same request](/docs/workflows/transactional-emails#retry-safely-with-an-idempotency-key) when every field in the table above has the same value as in the first request. PostHog also compares the sender that it chose: if you leave out `from` and the project's default sender changes before the retry, PostHog returns `idempotency_key_reused`. + +```bash +curl -X POST "https://us.posthog.com/api/projects/@current/messaging/send/email/" \ + -H "Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: password-reset-8f14e45f" \ + -d '{ + "template_key": "password-reset", + "to": "jane@example.com", + "distinct_id": "user_123", + "subject": "Reset your password", + "html": "

Hi Jane,

Reset your password.

", + "from": "Hedgebox " + }' +``` + +### Checks for content from your code + +These checks apply to content from your code, in addition to the [errors](/docs/workflows/transactional-emails#errors) of every send. PostHog sends nothing when a check fails. + +| Status | Code | Cause | What to do | +| --- | --- | --- | --- | +| `400` | A code for the field, such as `required` or `invalid` | A field is missing or breaks a rule above, or PostHog doesn't take the field. The `type` is `validation_error`, and `attr` names the field, such as `subject`. | Fix the field. If the field comes from a newer SDK, update PostHog or leave the field out. | +| `400` | `sender_not_verified` | No verified sender of the project can send from the `from` address. Or the request has no `from`, and the project has no verified default sender. | Send from an address on a verified domain, or [set a default sender](/docs/workflows/configure-channels) and leave out `from`. | ## Check an email's status @@ -84,7 +141,7 @@ To find out what happened to an email, read its status with the `message_id` fro GET https://us.posthog.com/api/projects/@current/messaging/emails/:message_id/ ``` -Send the same `Authorization` header as for a send. The key needs the `hog_flow:read` or `hog_flow:write` scope. Only a project secret API key can read a status. +Send the same `Authorization` header as for a send. The key needs the `transactional_email:read` or `transactional_email:write` scope. A key with a `hog_flow` scope still works. Only a project secret API key can read a status. A found email returns `200`: @@ -100,7 +157,7 @@ A found email returns `200`: See [statuses and reasons](/docs/workflows/transactional-emails#check-an-emails-status) for what each value means. -A `404` with the code `not_found` means the status isn't there yet, is older than 30 days, or the ID is wrong. See [when a status isn't found](/docs/workflows/transactional-emails#when-a-status-isnt-found). It returns `401` when the request has no project secret API key, `403` when the key doesn't have a Workflow scope, and `429` when you read too often. +A `404` with the code `not_found` right after a send means the status isn't there yet: a new send can take a few seconds to get its first status, so retry after a short wait. Later, a `404` means the email is older than 30 days or the ID is wrong. See [when a status isn't found](/docs/workflows/transactional-emails#when-a-status-isnt-found). It returns `401` when the request has no project secret API key, `403` when the key doesn't have a Workflow scope, and `429` when you read too often. Each project secret API key can read 600 statuses per minute and 20,000 per hour. All the keys of one project share the same limits in total. Reads have their own limits, so they don't use up the limits for sends. diff --git a/contents/docs/workflows/transactional-emails/index.mdx b/contents/docs/workflows/transactional-emails/index.mdx index 59db8c114a73..0236bedd054b 100644 --- a/contents/docs/workflows/transactional-emails/index.mdx +++ b/contents/docs/workflows/transactional-emails/index.mdx @@ -44,7 +44,7 @@ To send an email template you already have, open it in the **Templates** tab, op With the [PostHog MCP server](/docs/model-context-protocol) in your coding agent, ask the agent to set up a transactional email. It can design the email from your brand, make it transactional, and write the code that sends it. It asks you to confirm before it makes a template transactional, and you create the API key yourself. -You can also make a template transactional with the API: call `POST /api/projects/:project_id/messaging_templates/:template_id/make_transactional/` with a [personal API key](/docs/api/personal-api-keys) that has the `hog_flow:write` scope. +You can also make a template transactional with the API: call `POST /api/projects/:project_id/messaging_templates/:template_id/make_transactional/` with a [personal API key](/docs/api/personal-api-keys) that has the `transactional_email:write` scope. Keys with `hog_flow:write` still work. ### Edit a transactional email @@ -58,16 +58,16 @@ For emails with sign-in codes, reset links, or other secrets, turn on **Protect ## Create an API key -Send transactional emails with a [project secret API key](/docs/api/project-secret-api-keys) that has the `hog_flow:write` scope. The same key can also [read the status](#check-an-emails-status) of each email it sends. +Send transactional emails with a [project secret API key](/docs/api/project-secret-api-keys) that has the `transactional_email:write` scope. Keys with `hog_flow:write` still work. The same key can also [read the status](#check-an-emails-status) of each email it sends. 1. Go to [**Project secret API keys**](https://app.posthog.com/settings/project-secret-api-keys) in your project settings. 2. Click **Create project secret API key** and give the key a label. -3. In the scopes list, set **Workflow** to **Write**. +3. In the scopes list, set **Transactional email** to **Write**. 4. Copy the key. It starts with `phs_`, and you can't see it again after you leave the page. A project secret API key with this scope can send transactional emails and read their status, and nothing else. It can't read or change your templates, and it can't use any other part of the Workflows API. Keep the key on your server. Never put it in client-side code. -To give a service a key that can read statuses but not send, set **Workflow** to **Read** instead. +To give a service a key that can read statuses but not send, set **Transactional email** to **Read** instead. ## Send an email @@ -76,7 +76,7 @@ Your backend sends each email with one call. A send needs: - **The template ID.** The template's **API** tab shows it. - **The recipient.** One email address for each send. - **The template data.** The values for the template's `{{ variables.* }}` placeholders. The template can't read person properties, so pass all the data that the email needs. -- **The recipient's distinct ID.** PostHog records the send and its [engagement events](/docs/workflows/engagement-events) against this person. Without it, PostHog uses the email address as the distinct ID, which might not match the person in your product. We recommend that you always send it. +- **The recipient's distinct ID.** The distinct ID that you pass to `identify()`. PostHog records the send and its [engagement events](/docs/workflows/engagement-events) against this person. Without it, PostHog records the email against a person whose distinct ID is the email address, which is a separate person from your user unless you identify users by their email address. Leave it out for someone who isn't a user yet, such as an invite. - **An idempotency key.** It makes a retry safe. See [retry safely](#retry-safely-with-an-idempotency-key). In the template, use the same names that you send in the template data: @@ -129,7 +129,7 @@ Use `code` in your error handling. The text in `detail` can change. | `400` | `marketing_template` | The template is in a marketing category. | Move the template to a transactional category, or remove its category. | | `400` | `transactional_email_paused` | The workflow that sends this template isn't active, or it was deleted. | Don't retry. A retry gets the same error. | | `401` | `not_authenticated` or `authentication_failed` | The `Authorization` header is missing, or the key isn't valid. | Check that you send a valid project secret API key. | -| `403` | `permission_denied` | The key doesn't have the `hog_flow:write` scope, or the key belongs to a different project. | Use a key from this project with the correct scope. | +| `403` | `permission_denied` | The key doesn't have the `transactional_email:write` scope, or the key belongs to a different project. | Use a key from this project with the correct scope. | | `404` | `not_found` | The template doesn't exist in this project, or transactional emails aren't turned on for the project. | Check the template ID and the project. | | `409` | `idempotency_key_in_flight` | A request with the same `Idempotency-Key` is still in progress. | Retry with the same key after a few seconds. | | `422` | `idempotency_key_reused` | The `Idempotency-Key` was already used for a different request. | Use a new key for a new email. | diff --git a/contents/docs/workflows/transactional-emails/node.mdx b/contents/docs/workflows/transactional-emails/node.mdx index 7c47deb2d655..78f4d0669b36 100644 --- a/contents/docs/workflows/transactional-emails/node.mdx +++ b/contents/docs/workflows/transactional-emails/node.mdx @@ -19,15 +19,15 @@ Reuse the PostHog client your application already creates, and set `secretKey` t ```js import { PostHog } from "posthog-node" -const posthog = new PostHog("", { - host: "https://us.i.posthog.com", +const posthog = new PostHog("", { + host: "", secretKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY, // This client only sends email, so it doesn't poll flag definitions with the secret key enableLocalEvaluation: false, }) ``` -- **Local evaluation:** The client starts polling flag definitions with the secret key when it starts. A key with only the **Workflow** scope can't do that, so a client that only sends email needs `enableLocalEvaluation: false`. If your application [evaluates flags locally](/docs/feature-flags/local-evaluation) with the same client, give the key the **Feature flag** read scope too, and leave local evaluation on. +- **Local evaluation:** The client starts polling flag definitions with the secret key when it starts. A key with only the **Transactional email** scope can't do that, so a client that only sends email needs `enableLocalEvaluation: false`. If your application [evaluates flags locally](/docs/feature-flags/local-evaluation) with the same client, give the key the **Feature flag** read scope too, and leave local evaluation on. - **Host:** On PostHog Cloud, the client sends email to the app host of your region, even when `host` is an ingestion host such as `us.i.posthog.com`. For any other host, such as a reverse proxy or a self-hosted instance, set `appHost` to your PostHog app URL. The client refuses to send the secret key to a host that might be a proxy. ## Send an email @@ -58,6 +58,49 @@ Each request times out after 15 seconds. The client retries network errors, rate To change this, set `emailRequestTimeoutMs` and `emailRequestMaxRetries` on the client, or `maxRetries` on one send. When a job queue already retries the send, set the retries to `0` and pass an `idempotencyKey` that identifies the email. +## Link emails to your users + +Pass the distinct ID that you pass to `identify()`, usually the ID your app gives that user, as `distinctId`. PostHog then records the send, and its [engagement events](/docs/workflows/engagement-events) such as sent, opened, and clicked, against that user's person, next to their product events. + +- **If you leave it out,** PostHog records the email against a person whose distinct ID is the recipient's email address. Unless your app identifies users by their email address, that's a separate person from your user. +- **For someone who isn't a user yet,** such as an invite or an email address check before the account exists, there's no distinct ID to pass. Leave `distinctId` out. + +## Send content from your code + +From version 5.62.0, `send` also takes content that your code rendered, without a template in PostHog. Name the email with `templateKey`, and pass `subject` and `html`, `text`, or both. PostHog sends the content as you wrote it. + +For a [React Email](https://react.email) component, render it to HTML with `render` from `@react-email/render`, and pass the result as `html`: + +```jsx +import { render } from "@react-email/render" +import { PasswordResetEmail } from "./emails/password-reset" + +export async function sendPasswordReset({ userId, email, firstName, resetLink, resetRequestId }) { + const html = await render() + const { data, error } = await posthog.emails.send({ + to: email, + templateKey: "password-reset", + subject: "Reset your password", + html, + from: "Hedgebox ", + distinctId: userId, + idempotencyKey: `password-reset-${resetRequestId}`, + }) + if (error) { + throw error + } + return data.id +} +``` + +- **Key:** `templateKey` names the email, such as `password-reset`. PostHog groups the email's metrics and sends under it, so use the same key every time you send this email. +- **Text:** If you leave out `text`, PostHog makes it from `html`. +- **Sender:** `from` needs an address on a domain that your project verified. Without it, PostHog uses the project's default sender. `replyTo` takes one address or a list. +- **Engagement:** PostHog captures the email's engagement events unless you pass `captureEngagement: false`. +- **One form per send:** A send has `templateId` or `templateKey`, not both. Content from your code takes no `templateData`, because PostHog renders nothing. + +See [content from your code](/docs/workflows/transactional-emails/api#send-content-from-your-code) for each field and its checks. If your app sends email with Nodemailer, [use the PostHog transport](/docs/workflows/transactional-emails/nodemailer) and keep your `sendMail` calls. + ## Check an email's status ```js diff --git a/contents/docs/workflows/transactional-emails/nodemailer.mdx b/contents/docs/workflows/transactional-emails/nodemailer.mdx new file mode 100644 index 000000000000..03e8ab509d4e --- /dev/null +++ b/contents/docs/workflows/transactional-emails/nodemailer.mdx @@ -0,0 +1,125 @@ +--- +title: Send transactional emails with Nodemailer +sidebar: Docs +showTitle: true +--- + +If your app sends email with [Nodemailer](https://nodemailer.com), you can send that email through PostHog. Change the transport, and keep every `sendMail` call. Each message becomes one [email sent from your code](/docs/workflows/transactional-emails/node#send-content-from-your-code), so PostHog shows its sends, metrics, and engagement events. + +Before you start, [create a project secret API key](/docs/workflows/transactional-emails#create-an-api-key) with the **Transactional email** scope, and [configure a channel](/docs/workflows/configure-channels) with a verified sender. + +## Install + +```bash +npm install posthog-node nodemailer +``` + +## Create the transport + +Pass a PostHog client with your secret key to `createPostHogTransport`, and give the result to Nodemailer: + +```js +import nodemailer from "nodemailer" +import { PostHog } from "posthog-node" +import { createPostHogTransport } from "posthog-node/nodemailer" + +const posthog = new PostHog("", { + host: "", + secretKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY, + // This client only sends email, so it doesn't poll flag definitions with the secret key + enableLocalEvaluation: false, +}) + +export const transporter = nodemailer.createTransport(createPostHogTransport({ client: posthog })) +``` + +The client sends email to the app host of your PostHog Cloud region. For a reverse proxy or a self-hosted instance, set `appHost` on the client. See [set up the client](/docs/workflows/transactional-emails/node#set-up-the-client) for the key scopes and hosts. + +## Send an email + +Your `sendMail` calls stay the same: + +```js +const info = await transporter.sendMail({ + from: "Hedgebox ", + to: "jane@example.com", + subject: "Your order shipped", + html: "

Your order is on its way.

", + text: "Your order is on its way.", +}) +``` + +`info.messageId` is PostHog's message ID. Use it with [`posthog.emails.get`](/docs/workflows/transactional-emails/node#check-an-emails-status) to check the email's status. + +- **Sender:** `from` needs an address on a domain that your project verified. Without `from`, PostHog uses the project's default sender. +- **Recipients:** A message has one `to` address. `cc`, `bcc`, and `replyTo` take a list. PostHog keeps the display name of `from` and sends the other addresses without their names. +- **Attachments:** You can attach files, and inline images that have a `cid`. + +## Set PostHog options + +Put PostHog's options for one message in `posthog`: + +```js +await transporter.sendMail({ + from: "Hedgebox ", + to: "jane@example.com", + subject: "Your order shipped", + html: "

Your order is on its way.

", + posthog: { + template_key: "order-shipped", + distinct_id: user.id, + idempotency_key: `order-shipped-${order.id}`, + capture_engagement: true, + }, +}) +``` + +- **`template_key`** names the email, such as `order-shipped`. PostHog groups the email's metrics and sends under it. Without it, the transport uses `defaultTemplateKey` from `createPostHogTransport`, or `nodemailer`. Use letters, digits, hyphens, and underscores. +- **`distinct_id`** records the email against the person with this distinct ID. Without it, PostHog uses a person whose distinct ID is the `to` address. See [link emails to your users](/docs/workflows/transactional-emails/node#link-emails-to-your-users). +- **`idempotency_key`** identifies this one email. PostHog sends an email only once for each key, for 24 hours after it queues the email. See [retry safely](/docs/workflows/transactional-emails#retry-safely-with-an-idempotency-key). +- **`capture_engagement`** turns the email's [engagement events](/docs/workflows/engagement-events) on or off. It defaults to `true`. + +In TypeScript, importing `posthog-node/nodemailer` adds `posthog` to Nodemailer's message type. + +## Send mail from a queue + +The client retries network errors, rate limits, and a `503` up to two times, with the same idempotency key. When a job queue retries a failed send, each retry is a new `sendMail` call. Without an `idempotency_key`, each call gets a new random key, and the recipient can get the email twice. + +So when you queue mail, set `idempotency_key` from a stable ID of the email, such as the order ID and the email's name: + +```js +posthog: { idempotency_key: `order-shipped-${order.id}` } +``` + +PostHog remembers a key for 24 hours after it queues the email. If a queue retries a job later than that, the same key doesn't stop a second email, so stop retrying a send after 24 hours. + +A failed `sendMail` rejects with an error that tells you if a retry can succeed: + +- `error.retryable` is `true` when a retry with the same `idempotency_key` can succeed. After a network error or a `503`, PostHog might have queued the email, so retry with the same key, not through another provider. +- `error.retry_after` is the number of seconds that PostHog asked you to wait, when it sent one. +- `error.code`, `error.status`, `error.detail`, and `error.attr` come from the [errors table](/docs/workflows/transactional-emails#errors). + +## Messages PostHog can't send + +PostHog never drops part of a message. If PostHog can't send part of a message, `sendMail` rejects before PostHog gets a request, and sends nothing. The error has `code: "EPOSTHOGUNSUPPORTED"`, and `error.refusal` tells you why: + +| `refusal` | The message has | +| --- | --- | +| `multiple_to` | no `to` address, or more than one. Send one message for each recipient. | +| `unsupported_header` | a custom header, an attachment with its own `headers`, `inReplyTo`, `references`, `list`, or a `sender` that isn't the `from` address. | +| `unsupported_part` | an `amp`, `icalEvent`, `watchHtml`, `alternatives`, or `raw` part, or an attachment with a `raw` source. | +| `unsupported_priority` | a `priority` other than `normal`. | +| `envelope_mismatch` | an `envelope` with a different sender or other recipients than the message. | +| `inline_without_cid` | an inline attachment with no `cid`. | +| `invalid_template_key` | a `posthog.template_key` with characters other than letters, digits, hyphens, and underscores. | + +```js +try { + await transporter.sendMail(message) +} catch (error) { + if (error.code === "EPOSTHOGUNSUPPORTED") { + console.error(`PostHog can't send this email: ${error.refusal}`) + } + throw error +} +``` diff --git a/contents/docs/workflows/transactional-emails/python.mdx b/contents/docs/workflows/transactional-emails/python.mdx index e017761d6f33..db9d12c339dd 100644 --- a/contents/docs/workflows/transactional-emails/python.mdx +++ b/contents/docs/workflows/transactional-emails/python.mdx @@ -26,8 +26,8 @@ import os from posthog import Posthog posthog = Posthog( - "", - host="https://us.i.posthog.com", + "", + host="", secret_key=os.environ["POSTHOG_PROJECT_SECRET_API_KEY"], ) ``` @@ -38,8 +38,8 @@ import os from posthog import AsyncPosthog posthog = AsyncPosthog( - "", - host="https://us.i.posthog.com", + "", + host="", secret_key=os.environ["POSTHOG_PROJECT_SECRET_API_KEY"], ) ``` @@ -92,6 +92,57 @@ Each request times out after 15 seconds. The client retries network errors, rate To change this, set `email_request_timeout_seconds` and `email_request_max_retries` on the client, or `max_retries` on one send. When a job queue already retries the send, set the retries to `0` and pass an `idempotency_key` that identifies the email. +## Link emails to your users + +Pass the distinct ID that you pass to `identify()`, usually the ID your app gives that user, as `distinct_id`. PostHog then records the send, and its [engagement events](/docs/workflows/engagement-events) such as sent, opened, and clicked, against that user's person, next to their product events. + +- **If you leave it out,** PostHog records the email against a person whose distinct ID is the recipient's email address. Unless your app identifies users by their email address, that's a separate person from your user. +- **For someone who isn't a user yet,** such as an invite or an email address check before the account exists, there's no distinct ID to pass. Leave `distinct_id` out. + +## Send content from your code + +From version 7.84.0, `send` also takes content that your code rendered, without a template in PostHog. Name the email with `template_key`, and pass `subject` and `html`, `text`, or both. PostHog sends the content as you wrote it, so render it with your template engine first, such as Jinja or Django templates. + + + +```python file=Sync +def send_password_reset(user_id: str, email: str, html: str, reset_request_id: str) -> str: + result = posthog.emails.send( + to=email, + template_key="password-reset", + subject="Reset your password", + html=html, + from_="Hedgebox ", + distinct_id=user_id, + idempotency_key=f"password-reset-{reset_request_id}", + ) + return result.id +``` + +```python file=Async +async def send_password_reset(user_id: str, email: str, html: str, reset_request_id: str) -> str: + result = await posthog.emails.send( + to=email, + template_key="password-reset", + subject="Reset your password", + html=html, + from_="Hedgebox ", + distinct_id=user_id, + idempotency_key=f"password-reset-{reset_request_id}", + ) + return result.id +``` + + + +- **Key:** `template_key` names the email, such as `password-reset`. PostHog groups the email's metrics and sends under it, so use the same key every time you send this email. +- **Text:** If you leave out `text`, PostHog makes it from `html`. +- **Sender:** The sender is `from_`, because `from` is a Python keyword. It needs an address on a domain that your project verified. Without it, PostHog uses the project's default sender. `reply_to` takes one address or a list. +- **Engagement:** PostHog captures the email's engagement events unless you pass `capture_engagement=False`. +- **One form per send:** A send has `template_id` or `template_key`, not both, or it raises `TypeError`. Content from your code takes no `template_data`, because PostHog renders nothing. + +See [content from your code](/docs/workflows/transactional-emails/api#send-content-from-your-code) for each field and its checks. + ## Check an email's status ```python diff --git a/src/navs/index.js b/src/navs/index.js index 1aaa999767c4..cdb1a1df2ecd 100644 --- a/src/navs/index.js +++ b/src/navs/index.js @@ -8305,6 +8305,10 @@ export const docsMenu = { name: 'Node.js', url: '/docs/workflows/transactional-emails/node', }, + { + name: 'Nodemailer', + url: '/docs/workflows/transactional-emails/nodemailer', + }, { name: 'Python', url: '/docs/workflows/transactional-emails/python',