Transactional email examples

Contents

These examples cover the transactional emails that most products send. Each one says which settings to use, how to build its idempotency key, and whether to pass a distinct ID. Then it shows the send.

The examples use Node.js and Python. Every SDK page shows the same calls in its language. Each example assumes that you already set up the client and created the email with the variables that the example sends.

Sign-in code

Send a one-time code when someone signs in without a password.

SettingValue
Template variablescode, expires_in_minutes
Protect sensitive dataOn, so PostHog keeps no copy of the code
Distinct IDThe user's ID
Idempotency keysign-in-code-{code_id}, one key for each code that you issue

liquid
Your sign-in code is {{ variables.code }}. It expires in {{ variables.expires_in_minutes }} minutes.
export async function sendSignInCode({ user, signInCode }) {
const { error } = await posthog.emails.send({
to: user.email,
templateId: "<template_id>",
templateData: { code: signInCode.code, expires_in_minutes: 10 },
distinctId: user.id,
idempotencyKey: `sign-in-code-${signInCode.id}`,
})
if (error) {
throw error
}
}

When the person asks for a new code, issue a new code with a new ID. The new ID makes a new key, so PostHog sends a new email. A retry of the same code sends nothing new.

Email address check

Send a link that confirms that a new user owns their email address.

SettingValue
Template variablesfirst_name, verify_link
Protect sensitive dataOn, so the link's token never goes through a tracking redirect
Distinct IDThe new user's ID. Call identify() with the same ID when the user signs up.
Idempotency keyverify-email-{verification_id}

export async function sendEmailCheck({ user, verification }) {
const { error } = await posthog.emails.send({
to: user.email,
templateId: "<template_id>",
templateData: {
first_name: user.firstName,
verify_link: `https://example.com/verify?token=${verification.token}`,
},
distinctId: user.id,
idempotencyKey: `verify-email-${verification.id}`,
})
if (error) {
throw error
}
}

Send it from a background job after the user's row is saved, so a slow send never holds up the signup. Your SDK's page shows a job that retries only the errors that a later attempt can fix.

Team invite

Send an invite to someone who doesn't have an account yet.

SettingValue
Template variablesinviter_name, team_name, accept_link
Protect sensitive dataOn, because the link carries the invite token
Distinct IDLeave it out. The invitee isn't a user yet, so you have no distinct ID for them.
Idempotency keyinvite-{invite_id}

export async function sendInvite({ invite, inviter, team }) {
const { error } = await posthog.emails.send({
to: invite.email,
templateId: "<template_id>",
templateData: {
inviter_name: inviter.name,
team_name: team.name,
accept_link: `https://example.com/invites/${invite.token}`,
},
idempotencyKey: `invite-${invite.id}`,
})
if (error) {
throw error
}
}

Without a distinct ID, PostHog records the email on a person whose distinct ID is the email address. To resend an invite, issue a new invite with a new ID.

Receipt with a PDF

Send a receipt with the invoice attached, and a copy to the customer's billing contact. Attachments and copies work only with content from your code, so your code renders the email. The SDKs don't send copies and attachments yet, so this example uses the API.

SettingValue
Template keyreceipt
Copiescc to the billing contact, if the customer has one
AttachmentThe invoice PDF, 512 KB at most for all files together
Distinct IDThe customer's user ID
Idempotency keyreceipt-{order_id}

cURL
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: receipt-ord_1042" \
-d '{
"template_key": "receipt",
"to": "jane@example.com",
"cc": ["billing@example.com"],
"subject": "Your receipt for order 1042",
"html": "<p>Thanks for your order. Your invoice is attached.</p>",
"distinct_id": "user_123",
"attachments": [
{"filename": "invoice-1042.pdf", "content": "'"$(base64 < invoice-1042.pdf | tr -d '\n')"'", "content_type": "application/pdf"}
]
}'

Send the file's content in Base64. See copies and attachments for the limits.

Measure whether an email works

Each send lands on the recipient's person, so you can measure what people do after an email with the rest of your product data. First, turn on Capture email engagement events in your Workflows settings.

For example, to see how many people finish a password reset after they get the email, build a funnel with these steps:

  1. $workflows_email_delivered, filtered to $email_subject equals your reset email's subject
  2. Your own event for a finished reset, such as password_reset_completed

Use $workflows_email_delivered, not $workflows_email_link_clicked, as the first step. An email with Protect sensitive data turned on doesn't track link clicks, so a click step counts nobody.

For this funnel to work, pass the same distinct ID in the send and in the event that your app captures.

Still have questions?

Was this page useful?