Please see the main PostHog docs.
SDK usage examples and code snippets live in the official documentation so they stay up to date.
- PHP library docs
- Laravel integration docs
- Upgrade notes, including GeoIP configuration and compatibility
The optional PostHog\OpenTelemetry\ContextSpanProcessor adds session and person IDs to application spans inside this client's request context. Register it on the application's existing OpenTelemetry provider. OpenTelemetry remains an optional application dependency.
A client with no project token keeps request context but adds no span attributes. A client with a valid token and with the noop consumer still adds them.
The registered processor also enables request span enrichment when withContext() starts. This lookup skips Fibers because uninitialized OpenTelemetry Fiber storage can return another execution path's span. Child spans still use the active Fiber's PostHog context. New Fibers and queue jobs need explicit context propagation.
Existing span attributes take precedence. The processor preserves the application's provider, sampling, and exporters. See the PHP tracing guide and Laravel guide for setup.
emails() sends one email to one recipient, from an email template made transactional in PostHog or from content your code rendered, and reads what happened to it.
The client needs a project secret API key (phs_...) as secretKey. Create it with the "Send transactional email" preset (transactional_email:write) in your project settings, and add transactional_email:read if you read statuses with a key that can't send. Keys made with hog_flow:write still work. A client whose host is PostHog Cloud sends to its region's app host. For any other host, such as a reverse proxy, set the app_host option to your PostHog app host: email requests never go to a custom host. Loading flag definitions still sends the secret key to host, so when your proxy must not see the key, build the client with loadFeatureFlags: false and don't call loadFlags():
$client = new \PostHog\Client('phc_...', [
'host' => 'ph.example.com', // your proxy
'app_host' => 'https://eu.posthog.com',
], loadFeatureFlags: false, secretKey: 'phs_...');
PostHog::init(client: $client);PostHog::init('phc_...', ['host' => 'https://eu.i.posthog.com'], secretKey: 'phs_...');
// A template made in PostHog, by its ID
$result = PostHog::emails()->send([
'to' => 'user@example.com',
'template_id' => '0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b',
'template_data' => ['name' => 'Ada'], // fills {{ variables.name }}
'distinct_id' => 'user_123',
'idempotency_key' => 'welcome:user_123',
]);
// Content your code rendered, named by a key
PostHog::emails()->send([
'to' => 'user@example.com',
'template_key' => 'password-reset',
'subject' => 'Reset your password',
'html' => $html,
'distinct_id' => 'user_123',
]);
// Later, such as in a job: for a few seconds after the send, get() throws not_found
$status = PostHog::emails()->get($result->getId()); // queued, sent, delivered, bounced, ...- Two forms. Pass
template_idwith optionaltemplate_data, ortemplate_keywithsubjectandhtml,textor both. The content form also takesfrom(a verified sender),reply_to,sensitiveandtracking. PostHog derives a missingtextfrom thehtml. To send an email built with a template library such as React Email, render it to HTML in your app and passhtml. The SDK adds no rendering dependency. - Sensitive emails. Pass
'sensitive' => truewhen the email carries login links, reset tokens or codes. A sensitive email keeps its links unchanged unlesstrackingturns clicks on, leaves its subject off engagement events, and keeps its content out of logs. Left out, the email takes the setting of the template made in PostHog whose key matchestemplate_key, and is sensitive when no template matches. - Tracking. Left out, PostHog counts the email's opens and captures its engagement events, and tracks clicks only when the email isn't sensitive.
'tracking' => trueturns on opens, clicks and engagement events.'tracking' => falseturns them all off: no open pixel, no rewritten links, no engagement events. An array such as['clicks' => true]setsopens(defaulttrue),clicks(default: on unless the email is sensitive) andengagement(defaulttrue) one by one. Turn clicks on for a sensitive email only when its links are safe to rewrite. Click tracking needs open tracking.capture_engagementis deprecated in favor oftrackingwithengagement. It still works, but a send can't set both. - Pass
distinct_id. Without it, the email's engagement events go to a person whose distinct ID is the email address. - A result means queued, not delivered.
send()returns aSendEmailResultwith the message ID.get()reads the status for 30 days after the send. For a few seconds after the send, it throwsnot_found. - Errors. A send that is not queued throws
PostHog\EmailError. Branch ongetErrorCode(), such asrecipient_suppressedormissing_app_host, or ongetErrorName(), such asvalidation_error. An invalid argument throws before any request, with aninvalid_*code. - Retries. Network errors, rate limits and an unavailable service are retried up to
email_request_max_retriestimes (default 2) with the same idempotency key, so a retry never sends twice. Each request times out afteremail_request_timeout_ms(default 15000). When a job runner already retries the send, pass'max_retries' => 0and anidempotency_keyderived from what the email is for, such aswelcome:user_123. A generated key does not survive a job retry. - Moving from another provider. Set the
migrated_fromoption, such as'resend', so PostHog knows where your sends came from.
See CONTRIBUTING.md for local setup and test instructions.
See RELEASING.md.