Send transactional emails with Go
Contents
The Go SDK sends transactional emails from version 1.40.0. You need a transactional email and a project secret API key first. The quick start sets up both and sends a test email in a few minutes.
Prompt for your coding agent
Paste this into Claude Code, Cursor, or another coding agent. It sends the agent to this page and gives it the rules that are easy to get wrong.
Install
Set up the client
Add your project secret API key to the PostHog client that your app already has. The project token keeps capturing events. The secret key sends email, so keep it on your server.
When SecretKey is set, the client also polls flag definitions with it for local evaluation. Give the key the Feature flag read scope too, or the client logs a failed poll each time it polls.
Close the client when your app exits, not after each send. If your app sends through a reverse proxy or a self-hosted PostHog, see use a proxy or self-hosted PostHog.
Send an email
Send returns a *SendEmailResult and an error. result.ID is the message ID. Every error is a *posthog.EmailError. Read it with errors.As, and branch on its Code, one of the codes in the errors table. A wrong argument, such as a template ID that isn't a UUID, returns an EmailError before any request.
A network_error or a 503 doesn't mean that the email failed, because PostHog can still have queued it. Retry with the same idempotency key. If you resend the email through another email provider, the recipient can get it twice.
Timeouts and retries
The client retries for you. It retries network errors, rate limits, an in-flight idempotency key, and a 503 up to two times, with the same idempotency key. It waits about 1 second and then 2 seconds, or longer if the server's Retry-After asks for it. When Retry-After asks for more than 30 seconds, the client returns the error at once. Each request times out after 15 seconds.
To change this, set EmailRequestTimeout and EmailRequestMaxRetries in posthog.Config, or MaxRetries on one send. The retry settings take a pointer, so pass posthog.Ptr(0) to turn retries off.
Send from a background job
Send from a job queue when the email shouldn't slow down the request, such as an email address check after a signup. Let the queue own the retries:
- Set
MaxRetries: posthog.Ptr(0), so that the client doesn't retry inside the job. - Keep the idempotency key the same on every attempt.
- Finish the retries within 24 hours. PostHog remembers an idempotency key for 24 hours after it queues the email, so a later retry can send the email twice.
- Retry only the errors that a later attempt can fix. Every other code fails the same way again.
This example uses River, which keeps its jobs in Postgres. River retries a job when Work returns an error. river.JobCancel stops the job for good, even when it has attempts left.
Register the worker with river.AddWorker(workers, &PasswordResetWorker{PostHog: client}).
Link emails to your users
Pass the distinct ID that you pass to identify() as DistinctID. This is usually your user's ID. PostHog then records the email's engagement events, such as sent, opened, and clicked, on that user's person, in the same timeline as their product events.
- No distinct ID - PostHog records the email on a person whose distinct ID is the email address. That's a different person from your user, unless your app identifies users by their email address.
- Not a user yet - For an invite, or an email address check before the account exists, leave
DistinctIDout.
Send content from your code
Send also takes content that your code rendered, with no template in PostHog. Use it when your emails already live in your codebase, or when you move from another email provider. Pass a CodeEmail, name the email with TemplateKey, and set Subject with HTML, Text, or both. PostHog sends the content as you wrote it, so render it first with your template engine, such as html/template.
TemplateKey- Names the email, such aspassword-reset. PostHog groups the email's metrics and sends under this key, so use the same key every time you send this email.Text- If you leave it out, PostHog makes it fromHTML.From- Needs an address on a domain that your project verified. Without it, PostHog uses the project's default sender.ReplyTotakes a list of addresses.CaptureEngagement- PostHog captures the email's engagement events unless you set it toposthog.Ptr(false).
A TemplateEmail sends a template made in PostHog, and a CodeEmail sends your content. A CodeEmail takes no template data, because PostHog renders nothing. See content from your code for each field and its checks.
Check an email's status
Get returns an *EmailStatus with ID, Status, CreatedAt, StatusAt, and Reason. See statuses and reasons for what each value means.
A not_found right after a send usually means that the first status hasn't arrived yet. It doesn't mean that the email failed. The client doesn't retry a not_found, so try again a few times with a short wait before you treat the email as missing.
Test without sending email
Start an httptest.Server and set its URL as both Endpoint and AppHost, to see exactly what your code sends with no request to PostHog. This tests your integration, not delivery. To test delivery, send a real email to your own address and check the email's Sends tab.
Use a proxy or self-hosted PostHog
On PostHog Cloud, the client sends email to your region's app host, even when Endpoint is an ingestion host such as us.i.posthog.com. If Endpoint is a reverse proxy or a self-hosted PostHog, set AppHost to your PostHog app URL. Without it, a send fails with missing_app_host. The client doesn't send email requests to a host that might be a proxy.
Flag definitions for local evaluation still load from Endpoint with the secret key. If that host is a proxy that must not see the key, leave SecretKey off that client. Send email from a second client whose Endpoint is your PostHog host.





