The CRM marketing module orchestrates outbound campaigns across email, SMS, social posts, and ads from a single resource. Each campaign references an audience (built in Audiences and segmentation), a content template, and a schedule. The module keeps social profile state and rolls up cross-platform metrics so a campaign that hits Facebook, LinkedIn, and email reports as one record.
Campaign CRUD
/crm/marketing/campaignsJWT/crm/marketing/campaignsJWT/crm/marketing/campaigns/:idJWT/crm/marketing/campaigns/:idJWT/crm/marketing/campaigns/:idJWTA campaign body looks like:
{
"name": "Spring 2026 Launch",
"type": "multi-channel",
"channels": ["email", "facebook", "linkedin"],
"audienceId": "aud-spring-prospects",
"templateId": "tpl-spring-launch",
"schedule": {
"startAt": "2026-05-01T09:00:00Z",
"endAt": "2026-05-15T23:59:59Z",
"timezone": "America/New_York"
},
"status": "draft"
}
The type enum is exposed via the discovery endpoints below. channels may include any platform connected via the integrations module. When a channel is unconnected, the campaign saves but cannot be launched on that channel.
Discovery and metadata
/crm/marketing/platformsJWT/crm/marketing/campaign-typesJWT/crm/marketing/social-profiles/:platform?JWTUse platforms to render channel pickers that reflect which vendors the org has connected. campaign-types returns the supported strategies (one-shot blast, drip, recurring, milestone). social-profiles returns the connected social accounts for a given platform — needed to pick which account a post is published from.
Scheduling
A campaign moves through draft → scheduled → running → completed. Set schedule.startAt to a future ISO timestamp and the scheduler picks it up. Set recurring: { cron: "0 9 * * 1" } to repeat. To launch immediately, omit schedule and post the campaign with status: "running".
The scheduler runs in the Sync module. A scheduled campaign creates a schedule queue job; deleting the campaign cancels the job.
Audience targeting
Set audienceId to a saved audience or pass an inline filter:
{
"audience": {
"type": "dynamic",
"filter": {
"datatype": "contact",
"where": { "tags": { "$in": ["prospect", "vip"] } }
}
}
}
Inline filters use the DynamicQuery syntax. The reach is computed at launch, not at save, so audiences that grow over time pick up new contacts on the next send.
Analytics
/crm/marketing/campaigns/:id/analyticsJWT/crm/marketing/campaigns/aggregated-metricsJWT/crm/marketing/dashboardJWTThe per-campaign endpoint returns sends, opens, clicks, replies, bounces, conversions, revenue, and per-channel breakdowns. aggregated-metrics accepts an array of campaign IDs plus a date range and rolls them up — this is what the marketing dashboard uses for "last 30 days" tiles.
const metrics = await fetch('/api/crm/marketing/campaigns/aggregated-metrics', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
campaignIds: ['camp-1', 'camp-2', 'camp-3'],
range: { start: '2026-04-01', end: '2026-04-30' },
}),
}).then(r => r.json());
Engagement events
Every email open, click, and unsubscribe fires through the Connect automation webhook. The relevant pixels point to /connect/automation/:id/:stepId/email/open/ and /connect/automation/:id/:stepId/link/click/. Events flow into the contact's activity log and can trigger automations (see Activities and audit).
Health and status
/crm/marketing/healthJWTReturns scheduler heartbeat, queue depth, and last successful send time per channel. Useful for an operations dashboard checking that scheduled sends are firing.
Common pitfalls
- A campaign with
channels: ["email"]but no email vendor connected will save and stay indraft. Connect SendGrid or Mailgun first. - Audiences are evaluated at launch. If you add a contact after a one-shot campaign starts, they won't get the message.
- Campaign deletion is soft — the record is marked
state: "deleted"so historical analytics still work. To permanently purge, run the org-level purge job.