Skip to content

Transactional Email

Cloud Backend sends transactional email (user invitations, vendor assessment OTP, maintenance notifications) through Azure Communication Services (ACS) Email, authenticated with the app’s user-assigned managed identity. No API key is stored in Key Vault for this path.

In scopeOut of scope
Backend/internal/services/email.go and its consumersMarketing website contact form (still uses Resend via WebsiteContactApi/)
Cloud + Worker Container AppsMultiple sender domains per environment (future)
Verified domains cloud.monozu.io (prd) / cloud-dev.monozu.io (dev)Delivery status polling after 202 Accepted
LayerPath / resource
Email clientBackend/internal/services/email.go
ConfigCOMMUNICATION_SERVICES_ENDPOINT, EMAIL_FROM_ADDR, AZURE_CLIENT_ID
Consumerssettings (invites), vendors (assessment invite + OTP), maintenance (window notifications), notifications dispatcher, weekly digest
InfrastructureInfrastructure/infrastructure/modules/components/{emailService,communicationService}.bicep
DNS automationInfrastructure/scripts/setup_acs_email_dns.py
ItemValue
REST endpointPOST {COMMUNICATION_SERVICES_ENDPOINT}/emails:send?api-version=2025-09-01
AuthBearer token from azidentity.ManagedIdentityCredential, scope https://communication.azure.com/.default
SuccessHTTP 202 Accepted (fire-and-accept; no delivery polling)
From (prd)noreply@cloud.monozu.io
From (dev)noreply@cloud-dev.monozu.io

Request body shape:

{
"senderAddress": "noreply@cloud.monozu.io",
"recipients": {
"to": [{ "address": "user@example.com" }],
"bcc": [{ "address": "other@example.com" }]
},
"content": {
"subject": "...",
"html": "..."
}
}
sequenceDiagram
    participant Handler as Domain_Handler
    participant Email as services.EmailService
    participant Entra as Managed_Identity
    participant ACS as ACS_Email

    Handler->>Email: SendHTML / SendInvitation / ...
    Email->>Entra: GetToken communication.azure.com
    Entra-->>Email: Bearer token
    Email->>ACS: POST emails:send
    ACS-->>Email: 202 Accepted
    Email-->>Handler: nil error
SymptomLikely causeCheck
Configured() == false locallyEmpty COMMUNICATION_SERVICES_ENDPOINTExpected without ACS in dev; set endpoint + run against Azure MI
HTTP 401 from ACSUAMI missing Contributor on Communication Service resourcecommunicationServiceContributorRole in apps.bicep; role propagation delay
HTTP 4xx on sendDomain not verified or not linkedaz communication email domain show; linkedDomains on Communication Service
Invite succeeds in logs but no mailFire-and-accept — ACS accepted but delivery failed downstreamAzure Portal → Email → send logs; SPF/DKIM on Cloudflare
Wrong From domainEMAIL_FROM_ADDR mismatch with verified domainBicep emailFromAddr + ACA plainEnv