B2B SaaS integration pattern
Contacts sync, campaigns API, and bidirectional webhooks for embedded newsletter.
This page describes a reference architecture for a B2B SaaS product that embeds MailingCore newsletter capabilities: your app remains the source of truth for consent, while MailingCore holds an operational replica for sending, hosted unsubscribe, and deliverability.
The pattern was validated in production-style integrations (multi-tenant family apps, CRM add-ons, vertical SaaS). Adapt names and IDs to your domain.
Architecture overview
API key scopes (minimum)
| Scope | Use |
|---|---|
contacts:read | Reconciliation (updatedSince) |
contacts:write | Upsert, bulk import, erasure |
campaigns:read | Poll status and stats |
campaigns:write | Create, test, schedule, send |
webhooks:manage | Register your receiver URL |
Optional: suppressions:write if you push unsubscribes from your admin panel in addition to webhooks.
1. Contact synchronization
Goal: Keep opted-in audience in MailingCore without passing recipient lists on every send.
| Aspect | Recommendation |
|---|---|
| Identity | externalId = your stable user/account ID (opaque, no extra PII) |
| Upsert | PUT /contacts when consent is granted |
| Initial load | POST /contacts/bulk in chunks (≤ 1000 per request) |
| Incremental | Periodic job + on-change hooks when email, locale, or consent changes |
| Fields | email, locale, optIn: true, consentVersion, source tag |
| Revoke in your app | DELETE /contacts/:externalId or PATCH with optIn: false |
| GDPR erasure | DELETE /contacts/:externalId — MailingCore propagates suppression |
Reconciliation: GET /contacts?updatedSince=<ISO>&cursor= to align hosted unsubscribes back to your consent store.
Suggested cadence: every 15–60 minutes plus real-time hooks on consent events.
2. Webhook receiver (your server)
Register with Create endpoint:
{
"url": "https://your-saas.com/webhooks/mailingcore",
"events": [
"contact.unsubscribed",
"email.bounced",
"email.complained"
],
"secret": "<your-env-secret>"
}
| Event | Typical action in your SaaS |
|---|---|
contact.unsubscribed | Revoke newsletter consent by externalId or email; audit log |
email.bounced (hard) | Mark email invalid; optional suppression sync |
email.complained | Revoke consent; operator alert |
Verify X-MailingCore-Signature (HMAC SHA-256, ±5 min replay tolerance). Respond 200 immediately; process async. See Deliveries and retries.
3. Campaign workflow (replaces batch relay)
Stop fan-out POST /emails/batch with explicit recipient arrays. Use the campaigns API instead:
Pre-sync audience
Run contact sync so
optIn: truecontacts are current.Create draft
POST /campaignswithtemplateVersionId,subject,audienceFilter: { optIn: true }.Check locale coverage
GET /campaigns/:id/locale-coverage— ensure template variants exist per Locale coverage.Test send
POST /campaigns/:id/testto 1–5 internal QA addresses.Schedule or send
POST /campaigns/:id/scheduleorPOST /campaigns/:id/send(async fan-out).Poll status
GET /campaigns/:iduntilSENT,FAILED, orCANCELLED. PersistmailingcoreCampaignIdin your campaign entity.
Full step-by-step: Create and send a campaign.
4. Feature flag and rollback
Keep a configuration switch (e.g. NEWSLETTER_MODE=legacy_relay | mailingcore_api) so you can revert to your previous send path without redeploying. Run smoke tests on staging before flipping production traffic.
5. Smoke test checklist
| # | Scenario | Pass criteria |
|---|---|---|
| 1 | Upsert test contacts with distinct locales | Listed in GET /contacts; optIn: true |
| 2 | Campaign test send | Email received; {{unsubscribeUrl}} present |
| 3 | Multi-locale audience | locale-coverage warnings coherent |
| 4 | Small production send | status: SENT; stats sent > 0 |
| 5 | Click hosted unsubscribe | contact.unsubscribed received; consent revoked in your app |
| 6 | Revoke consent in your app | Contact removed or opted out in MailingCore |
| 7 | Idempotent sync retry | No duplicate externalId rows |