This guide covers the developer setup for email delivery in PropOps Web — including sending methods, template customisation, and environment configuration.
Choosing a sending method
PropOps Web supports two email sending methods. Each template can be configured independently.
Method When to use Local (built-in) Development, self-hosted, or when you don’t need Brevo. Sends via PHP mail() or Mailpit. Works out of the box with no API key. Brevo API Production environments where you want deliverability tracking, domain verification, and analytics. Requires a BREVO_API_KEY.
You can mix both methods — use Brevo for customer-facing emails and local templates for internal notifications.
Setting up local email
Local email works immediately with no configuration. Templates are rendered from views/emails/ and sent via PHP mail().
Set your sender details
In your .env file: MAIL_FROM_ADDRESS=noreply@yourcompany.com
MAIL_FROM_NAME=Your Company Name
Configure your branding
Set these so templates display your logo and colour: APP_NAME=Your Company
APP_URL=https://yourcompany.com
APP_LOGO_PATH=/assets/images/logo.png
BRAND_PRIMARY_COLOR=#0071e3
Leave BREVO_API_KEY empty
When no Brevo key is set, PropOps Web automatically uses local templates:
Setting up Brevo
Create a Brevo account
Sign up at brevo.com if you don’t already have an account.
Generate an API key
In the Brevo dashboard, go to SMTP & API → API Keys and generate a new key.
Add to your environment
BREVO_API_KEY=your-api-key-here
MAIL_FROM_ADDRESS=noreply@yourcompany.com
MAIL_FROM_NAME=Your Company Name
Verify your sender domain
In Brevo, go to Senders, Domains & Dedicated IPs and verify the domain you’re sending from. This improves deliverability and prevents emails being marked as spam.
Set template values to Brevo IDs
For any email you want to send via Brevo, change the template value from a name to the numeric Brevo template ID: # Local template (default)
EMAIL_TEMPLATE_WELCOME=welcome
# Brevo template
EMAIL_TEMPLATE_WELCOME=30
How template resolution works
Each email type is mapped to an EMAIL_TEMPLATE_* variable in .env. The value determines how PropOps Web sends that email:
Value type Behaviour String (e.g. welcome)Renders the local HTML template from views/emails/welcome.html Number (e.g. 30)Sends via the Brevo API using that template ID Empty Falls back to a local template derived from the env key name
Local templates use {"{{ params.COLUMN_NAME }}"} syntax — the same format Brevo uses — so migrating between local and Brevo is seamless.
Customising templates
Editing an existing template
All 47 pre-built templates live in views/emails/. Each template is a standalone HTML fragment that gets wrapped in layout.html.
Open the template file
Find the template in views/emails/ — for example welcome.html.
Edit the HTML
Templates are plain HTML. Use {"{{ params.VARIABLE_NAME }}"} placeholders for dynamic content. Check the existing template to see which variables are available.
Test locally
If you’re using Mailpit (via DDEV or Docker), emails will appear in the Mailpit inbox at http://localhost:8025 so you can preview your changes.
Creating a new template
Create the HTML file
Create a new .html file in views/emails/. The filename should match the template name you’ll use in .env — for example my_custom_email.html.
Add a subject line
Include a subject in an HTML comment at the top of the file: <!-- Subject: Your Email Subject Here -->
You can use placeholders in the subject: <!-- Subject: Job Update — {{ params.JOB_REF }} -->
Write your template content
Add your HTML content. The template will be automatically wrapped in layout.html (logo, footer, styling). You only need to write the inner content: <!-- Subject: Custom Notification -->
< h1 style = "margin: 0 0 8px 0; font-size: 22px; font-weight: 600;
color: #1d1d1f; letter-spacing: -0.02em;"
class = "email-heading" > Your heading </ h1 >
< p style = "margin: 0 0 28px 0; font-size: 15px; color: #424245;
line-height: 1.6;" class = "email-text" >
Hi {{ params.RECIPIENT_NAME }}, here is your notification.
</ p >
<!-- Info box -->
< table role = "presentation" cellpadding = "0" cellspacing = "0" width = "100%"
style = "margin: 0 0 28px 0; background-color: #f5f5f7;
border-radius: 12px;" >
< tr >
< td style = "padding: 20px 24px;" >
< p style = "margin: 0 0 4px 0; font-size: 12px; font-weight: 600;
color: #86868b; text-transform: uppercase;
letter-spacing: 0.05em;" > Label </ p >
< p style = "margin: 0; font-size: 15px; color: #1d1d1f;"
class = "email-text" > {{ params.VALUE }} </ p >
</ td >
</ tr >
</ table >
<!-- CTA button -->
< table role = "presentation" cellpadding = "0" cellspacing = "0" >
< tr >
< td style = "background-color: {{ params.BRAND_PRIMARY_COLOR }};
border-radius: 10px;" >
< a href = "{{ params.ACTION_URL }}"
style = "display: inline-block; padding: 13px 28px;
font-size: 15px; font-weight: 600; color: #ffffff;
text-decoration: none;" > View Details </ a >
</ td >
</ tr >
</ table >
Register in .env
Add a new EMAIL_TEMPLATE_* variable pointing to your template name: EMAIL_TEMPLATE_MY_CUSTOM_EMAIL=my_custom_email
Send from code
Use EmailService::sendTemplate() with the env key: $templateRef = $emailService -> getTemplateId ( 'MY_CUSTOM_EMAIL' );
$emailService -> sendTemplate ( $templateRef , $recipient , $params );
Editing the layout
The shared layout in views/emails/layout.html wraps every template. It contains:
The outer email structure (background, centring)
Your brand logo (centred)
The white content card
The footer with APP_URL and copyright
Dark mode styles
Responsive breakpoints
Edit layout.html to change the look of all emails at once. The placeholder {"{{ TEMPLATE_CONTENT }}"} is where each template’s content is injected.
Auto-injected variables
These variables are available in every template automatically:
Variable Source APP_NAMEFrom .env APP_URLFrom .env APP_LOGO_PATHFrom .env BRAND_PRIMARY_COLORFrom .env (default: #2563eb) CURRENT_YEARAuto-generated
Environment configuration
Core settings
BREVO_API_KEY= # Optional — leave empty for local-only sending
MAIL_FROM_ADDRESS=noreply@yourcompany.com
MAIL_FROM_NAME=Your Company Name
Template mappings
Each email type has an EMAIL_TEMPLATE_* variable. Set it to a local template name or a Brevo template ID :
# Jobs
EMAIL_TEMPLATE_JOB_CREATED=job_created
EMAIL_TEMPLATE_JOB_UPDATE=job_update
EMAIL_TEMPLATE_JOB_UPDATED=job_updated
EMAIL_TEMPLATE_JOB_UPDATED_AGENT=job_updated_agent
EMAIL_TEMPLATE_JOB_UPDATED_CONTRACTOR=job_updated_contractor
EMAIL_TEMPLATE_JOB_RECALLED=job_recalled
EMAIL_TEMPLATE_JOB_RECALL_COMPLETED=job_recall_completed
EMAIL_TEMPLATE_JOB_RECALL_CANCELLED=job_recall_cancelled
EMAIL_TEMPLATE_QUOTE_READY=quote_ready
EMAIL_TEMPLATE_JOB_COMPLETED=job_completed
EMAIL_TEMPLATE_JOB_STATUS_REMINDER=job_status_reminder
EMAIL_TEMPLATE_JOB_TYPE_CREATED=job_type_created
EMAIL_TEMPLATE_JOB_TYPE_STATUS_CHANGED=job_type_status_changed
# Users & Authentication
EMAIL_TEMPLATE_WELCOME=welcome
EMAIL_TEMPLATE_EMAIL_ACTIVATION=email_activation
EMAIL_TEMPLATE_PASSWORD_RESET=password_reset
EMAIL_TEMPLATE_PASSWORD_CHANGED=password_changed
EMAIL_TEMPLATE_USER_CREATED=user_created
EMAIL_TEMPLATE_USER_DELETED=user_deleted
EMAIL_TEMPLATE_USER_STATUS_CHANGED=user_status_changed
EMAIL_TEMPLATE_USER_ROLE_CHANGED=user_role_changed
EMAIL_TEMPLATE_USER_BLACKLISTED=user_blacklisted
EMAIL_TEMPLATE_USER_UNBLACKLISTED=user_unblacklisted
EMAIL_TEMPLATE_ONBOARDING_REMINDER=onboarding_reminder
EMAIL_TEMPLATE_LOGIN_IP_VERIFICATION_PIN=login_ip_verification_pin
# Financial
EMAIL_TEMPLATE_INVOICE_READY=invoice_ready
EMAIL_TEMPLATE_PAYMENT_UPDATE=payment_update
# Notifications & Communication
EMAIL_TEMPLATE_CASE_NOTE=case_note
EMAIL_TEMPLATE_CASE_NOTE_ADDED=case_note_added
EMAIL_TEMPLATE_APPOINTMENT_REMINDER=appointment_reminder
EMAIL_TEMPLATE_CERTIFICATE_REMINDER=certificate_reminder
EMAIL_TEMPLATE_DOCUMENT_SHARED=document_shared
EMAIL_TEMPLATE_ATTACHMENT_UPLOADED=attachment_uploaded
# Media
EMAIL_TEMPLATE_VIDEO_READY=video_ready
EMAIL_TEMPLATE_VIDEO_FAILED=video_failed
# Security Alerts
EMAIL_TEMPLATE_PASSWORD_BREACH_DETECTED=password_breach_detected
EMAIL_TEMPLATE_PASSWORD_BREACH_RESET=password_breach_reset
EMAIL_TEMPLATE_WEBHOOK_TOKEN_SECURITY_ALERT=webhook_token_security_alert
EMAIL_TEMPLATE_BRUTE_FORCE_ACCOUNT_LOCKED=brute_force_account_locked
# System & Infrastructure
EMAIL_TEMPLATE_DEPLOYMENT_NOTIFICATION=deployment_notification
EMAIL_TEMPLATE_API_ENDPOINT_FAILURE=api_endpoint_failure
EMAIL_TEMPLATE_API_DISABLED=api_disabled
EMAIL_TEMPLATE_API_NOT_RESPONDING=api_not_responding
EMAIL_TEMPLATE_API_CREATED=api_created
EMAIL_TEMPLATE_API_RATE_LIMIT_EXCEEDED=api_rate_limit_exceeded
EMAIL_TEMPLATE_BRANCH_STATUS_CHANGED=branch_status_changed
To switch any email to Brevo, replace the template name with the numeric Brevo template ID (e.g. EMAIL_TEMPLATE_WELCOME=30). To switch back to local, set it to the template name (e.g. EMAIL_TEMPLATE_WELCOME=welcome).
Troubleshooting
If using Brevo, check that BREVO_API_KEY is set correctly in your .env
If using local, check your server’s mail() function is working (php -r "mail('test@example.com','Test','Body');")
Verify the sender domain is confirmed in Brevo (if applicable)
Check the Email Log in Admin → Email Log for error messages
Ensure your sender domain has SPF, DKIM, and DMARC records configured
Verify the domain in Brevo’s sender settings
Use a professional sender address (avoid free email providers)
Template not rendering correctly
Check the template file exists in views/emails/ with the correct filename
Verify placeholder names match exactly (case-sensitive): {"{{ params.USER_NAME }}"} not {"{{ params.user_name }}"}
If the template falls back to the generic template, the specific file may be missing or misspelt
Use Mailpit to preview rendered HTML during development
Switching between local and Brevo
To use a local template: set the env value to the template name (e.g. welcome)
To use Brevo: set the env value to the numeric Brevo template ID (e.g. 30)
You can mix both — each template is independent