Keep mint/UTM in always-on core so email_sms and directmail stay optional, and brand utm_source from SITE_NAME (or UTM_SOURCE) instead of a hardcoded client name. Closes #3
Messaging
Campaign compose/send, SMTP2GO email + SMS, PCM Integrations postcards, and delivery webhooks.
SMTP2GO webhook setup
Campaign report page polls provider events every 10s. Create one SMTP2GO webhook (email + SMS share a URL — paid plans cap at 10 webhooks).
Auth (SMTP2GO_WEBHOOK_SECRET)
- Set
SMTP2GO_WEBHOOK_SECRETin.env/ prod env (long random string). - In SMTP2GO, set Authorization header to Bearer and paste that same secret (do not leave it as “None”).
- Fallback:
?token=<SMTP2GO_WEBHOOK_SECRET>on the webhook URL also works.
Unified email + SMS webhook
| Field | Value |
|---|---|
| URL | https://mkdrealtor.com/portal/messaging/webhooks/smtp2go/ |
| Authorization header | Bearer + SMTP2GO_WEBHOOK_SECRET |
| Output type | JSON |
| Users | email SMTP user(s) and the SMS API key (SMTP2GO_SMS_API_KEY) |
| Email events | processed, bounced, rejected, spam, delivered, unsub/resub, opened, clicked |
| Email headers | X-Monica-Message-Id |
| SMS events | Submitted, Sending, Delivered, Failed, Rejected (and Opt-out if shown) |
The handler classifies each POST from the payload (sms_* / destination_number
→ SMS; rcpt / email_id / X-Monica-Message-Id → email; inbound text=STOP
without event → SMS opt-out).
X-Monica-Message-Id is set on every campaign email send and is required so email
webhook events match the correct recipient row. Invalid / missing header values
no longer 500 the endpoint (SMTP2GO “Test this webhook” often sends a sample
non-UUID).
SMS correlation uses message_id (SMS id), then destination_number phone
fallback. Do not treat webhook id as the SMS id.
Opt-out: SMTP2GO auto-handles replies STOP / UNSUB / UNSUBSCRIBE.
This endpoint also accepts inbound POSTs without an event field
(text=STOP, from=…) and opts the contact out of SMS. Later sends to
opted-out numbers are typically sms_rejected.
Beta / other hosts: swap the hostname, keep the path.
Legacy aliases (same handler): /portal/messaging/webhooks/email/ and
/portal/messaging/webhooks/sms/ — prefer /smtp2go/ for new configs.
PCM Integrations (postcards)
Default postcard provider. Designer embeds PCM’s editor; orders use DirectMail API v3.
Env
| Var | Purpose |
|---|---|
PCM_API_KEY |
API key from PCM portal (My Account → API Keys) |
PCM_API_SECRET |
Matching API secret; used with key on POST /auth/login |
PCM_CHILD_REF_NBR |
Optional child-app ref for multi-account |
PCM_WEBHOOK_SECRETS |
Comma-separated signature secrets (one per PCM subscription) |
PCM_WEBHOOK_SECRET |
Optional single-secret alias (merged into the list above) |
PCM_RETURN_ADDRESS |
JSON return address on orders |
POSTCARD_PROVIDER |
pcm (default) |
Auth flow: POST /auth/login with {apiKey, apiSecret} → short-lived
token used as Authorization: Bearer … on design/order calls
(PCM Logging In).
Designer
Portal → Postcard design: create/list designs via API, edit in iframe
(POST /design/custom, GET /design/{id}/edit?mode=embed). Save as a
MessageTemplate (stores design_id) then pick it when composing a postcard campaign.
Postcard webhook
PCM allows one event per subscription, and each subscription gets its own signature secret (copy-only in the UI). Create one subscription per status you care about; point them all at the same URL and paste every secret into env.
| Field | Value |
|---|---|
| URL | https://mkdrealtor.com/portal/messaging/webhooks/postcard/ |
| Events | One subscription each: Pending, Processing, Processed, Delivered, Undeliverable, Canceled (skip QrCodeScan unless needed) |
| Environments | Sandbox and/or Production as needed |
| Secrets | Copy each subscription signature → PCM_WEBHOOK_SECRETS=sec1,sec2,… |
We accept Bearer, ?token=, or common signature headers (raw secret or
HMAC-SHA256 of body) matching any listed secret.
Correlation: we send extRefNbr=<Message.uuid> on each recipient; webhooks should
echo that (or orderID, matched to Message.provider_message_id).
Campaign completion email
When a campaign reaches completed (email, SMS, or postcard), one summary email
goes to campaign.created_by.email, else CONTACT_EMAIL. Guarded by
Campaign.notify_sent_at so it only sends once.
Local development
SMTP2GO / PCM cannot reach localhost. Use a tunnel (Cloudflare Tunnel / ngrok) to :8000,
or test webhooks against beta/prod.
For real SMTP delivery locally (not console logs):
# in .env
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST_USER=…
EMAIL_HOST_PASSWORD=…
SMTP2GO_WEBHOOK_SECRET=…
SMTP2GO_SMS_API_KEY=… # SMS sends only
PCM_API_KEY=…
PCM_API_SECRET=…
PCM_WEBHOOK_SECRETS=sec1,sec2,…
PCM_RETURN_ADDRESS={…}
Endpoints (app)
| Path | Purpose |
|---|---|
POST /portal/messaging/webhooks/smtp2go/ |
Unified SMTP2GO email + SMS (+ inbound STOP) |
POST /portal/messaging/webhooks/email/ |
Legacy alias → same as /smtp2go/ |
POST /portal/messaging/webhooks/sms/ |
Legacy alias → same as /smtp2go/ |
POST /portal/messaging/webhooks/postcard/ |
PCM order / mail tracking events |
GET /portal/messaging/campaigns/<id>/status.json |
Live stats for the campaign report UI |
GET /portal/messaging/postcard/ |
PCM designer iframe |
Code: webhooks.py, providers/postcard/pcm.py, views.py.