A SaaS product expanding across borders faces a quiet but persistent risk: the emails your Stripe webhook fires off after every payment event. Receipts, renewal confirmations, failed-payment alerts, and plan-change notices can all land in the wrong language if the system lacks a reliable way to infer user locale. In one recent case, four critical email templates remained hardcoded to Japanese for months, sending Japanese receipts and failure notices to English-speaking customers overseas.
The fix didn’t need a database overhaul or a multi-month migration. Instead, the team built a lightweight language detector that runs directly inside the Stripe webhook handler, using the currency field that’s already present in every event payload. The result: zero database changes, immediate impact for all users, and a template for handling multilingual webhook integrations without adding technical debt.
Why most solutions fail—three common traps before you choose one
When deciding how to pick the language for each outgoing email, developers typically evaluate three approaches, each with trade-offs:
- Store language in the database. Create a dedicated
languagecolumn during user signup, then query it when sending messages. This method adds a database lookup per event and requires a data migration to cover existing users. Without a fallback, first-time users still see default messages until the backfill completes.
- Fetch language from the Stripe API. Use the
preferred_localesfield returned bystripe.Customer.retrieve(). While authoritative in theory, this introduces an extra network call per email and still omits customers who haven’t set their locale.
- Infer language from the currency field. Stripe webhooks include a
currencyvalue for every transaction (e.g.,usd,jpy,eur). Because currency is assigned at purchase and never changes, it acts as a stable, zero-cost signal that applies to every user—new or existing—without any database writes.
The team chose the third option. Currency is always present, never changes after purchase, requires no additional API calls, and avoids migration risks entirely. Existing users automatically benefit from the new logic on the next renewal or payment event, eliminating silent bugs that can persist for months.
A two-line helper that replaces a risky migration
The entire language-detection logic fits into a single helper function:
/** Map a currency code to a display language code. */
function lang_from_currency(string $currency): string
{
$en_currencies = ['usd']; // USD → English
return in_array(strtolower($currency), $en_currencies, true) ? 'en' : 'ja';
}The function treats USD as English and all other currencies (including JPY) as Japanese. Additional currencies such as EUR or GBP can be appended to the $en_currencies list as new markets open. The design is intentionally simple: English speakers rarely purchase in JPY, and the rare Japanese-speaking customer on a USD plan receives English emails, aligning better with their explicit purchase intent than browser headers or other heuristics.
Extracting currency from four different webhook events
Stripe webhooks trigger four core events that can each generate emails. Each event’s payload stores the currency in a slightly different location, so the integration must handle variations:
- `checkout.session.completed` — purchase confirmation
$checkout_lang = lang_from_currency($session['currency'] ?? 'jpy');
send_license_email($email, $client_name, $key, $plan, $period, $checkout_lang);- `invoice.payment_succeeded` — renewal success
$renewal_lang = lang_from_currency($invoice['currency'] ?? 'jpy');
send_renewal_email($email, $client_name, $plan, $renewal_lang);- `invoice.payment_failed` — payment failure
$failed_lang = lang_from_currency($invoice['currency'] ?? 'jpy');
send_payment_failed_email($email, $client_name, $plan, $failed_lang);- `customer.subscription.updated` — plan change (currency reached via the subscription object)
$changed_lang = lang_from_currency($sub_currency);
send_plan_changed_email($email, $client_name, $old_plan, $new_plan, $changed_lang);A fallback value of 'jpy' ensures the helper never throws if a test event lacks the currency field, defaulting to Japanese as a safe baseline.
The sneaky subject-encoding trap in PHP mail
PHP’s mb_send_mail() function encodes email subjects based on the current mb_language() setting. Two settings produce markedly different outcomes:
mb_language('Japanese')encodes the subject using ISO-2022-JP, a Japan-specific MIME encoding based on JIS X 0208.mb_language('uni')encodes the subject using UTF-8 Base64, compliant with RFC 2047, which is universally supported.
When an English subject like "Your license key for WP Maintenance Manager" gets ISO-2022-JP encoded, major email clients such as Gmail and Outlook flag it as suspicious, raising spam scores. Users see garbled subject lines even though the message itself is correct. The fix is to set mb_language() dynamically based on the inferred language:
function send_license_email($email, $client_name, $key, $plan, $period, $lang = 'ja')
{
mb_language($lang === 'en' ? 'uni' : 'Japanese');
// ...subject and body generation
}Note that mb_language('uni') requires PHP 7.2 or later, as it implements RFC 2047 compliant Base64 UTF-8 encoding. For multilingual webhook integrations, this subtle detail is effectively mandatory for clean deliverability.
What zero-migration architecture looks like in practice
The entire change lived in one file—webhook.php—and required no schema updates. New users receive emails in the correct language from day one, while existing users receive corrected renewal and failure notices automatically on their next billing cycle. Rows that would have been missed during a manual backfill stay correct indefinitely.
This outcome highlights the power of stateless, event-driven design paired with a strong inference signal already present in the webhook payload. A database-centric approach would have mandated a backfill script, risked partial coverage, and introduced ongoing maintenance overhead for a field that rarely changes after purchase.
Three patterns to apply to your next webhook i18n project
Consider these principles when building multilingual email flows from webhook events:
- Use payload signals before database state. If your webhook already carries a stable, purchase-time signal like currency, routing logic through that field reduces complexity and avoids migration debt. Fewer moving parts mean fewer places to break.
- Set `mb_language('uni')` for non-Japanese subjects. Avoid ISO-2022-JP encoding for English content to prevent spam-score penalties and garbled subjects in major email clients.
- Design for forward compatibility. Add new currencies to the whitelist as markets open, but keep the inference logic simple and deterministic—currency is fixed at purchase and rarely changes.
AI summary
Learn how to infer user language from Stripe webhook currency fields—zero database changes, immediate multilingual emails, and a PHP encoding trap to avoid.