Documentation

Marketing campaigns

Multi-channel marketing campaign CRUD, scheduling, audience targeting, and analytics.

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

POST/crm/marketing/campaignsJWT
GET/crm/marketing/campaignsJWT
GET/crm/marketing/campaigns/:idJWT
PUT/crm/marketing/campaigns/:idJWT
DELETE/crm/marketing/campaigns/:idJWT

A 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

GET/crm/marketing/platformsJWT
GET/crm/marketing/campaign-typesJWT
GET/crm/marketing/social-profiles/:platform?JWT

Use 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

GET/crm/marketing/campaigns/:id/analyticsJWT
POST/crm/marketing/campaigns/aggregated-metricsJWT
GET/crm/marketing/dashboardJWT

The 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

GET/crm/marketing/healthJWT

Returns 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 in draft. 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.