Email campaign endpoints
Campaigns
GET /api/email-campaigns
Email campaigns (type = "email") with statistics from the v_email_campaign_stats materialized
view: total_count, valid_count, pending_count, queued_count, requested_count,
sent_count, delivered_count, opened_count, clicked_count, bounced_count,
suppressed_count, unsubscribed_count, blocked_count. opened_count includes unsubscribed
recipients, and delivered_count excludes bounced and suppressed sends. See
Campaign statistics for each definition.
POST /api/email-campaigns
Creates the campaign with type = "email". Accepts the email configuration columns —
schedulerIntervalSeconds, emailsPerHour, maxEmailsPerDay, emailsPerVariationPerHour,
emailWindowStart, emailWindowEnd, emailAllowWeekends, resendTopicId, templateId,
segmentId, description, goal. Optional segment_id (validated before insert) is stored
on the campaign and queues email-campaign/import-segment.requested after the draft exists.
PATCH /api/email-campaigns/{id}
Partial update, including status — setting paused stops the next pg_cron tick from sending.
completed is accepted only when the campaign is currently paused; any other current status returns 400.
Completed campaigns are not broadcast.
Optional segment_id (null to clear) updates the assigned segment only; it does not re-import members.
POST /api/email-campaigns/{id}/duplicate
Synchronously creates a draft copy named "<name> (copy)" with the source’s email configuration
(description, goal, schedulerIntervalSeconds, emailsPerHour, maxEmailsPerDay,
emailsPerVariationPerHour, emailWindowStart, emailWindowEnd, emailAllowWeekends), creates a
fresh Resend topic for it (best-effort), then emits email-campaign/duplicate.requested and returns
201 with the new campaign.
Recipients are copied asynchronously by the emailCampaignDuplicate Inngest function, which pages
through the source’s valid targets in chunks. A large recipient list would time out a single request, so
only the campaign shell is created inline. The copy starts in draft, so it cannot begin sending before
its recipients have arrived. See Email workflows.
POST /api/email-campaigns/{id}/send
Sends to a specific recipient or a small set — the manual path used for testing a variation, distinct from the scheduled broadcast.
POST /api/email-campaigns/{id}/broadcast
Internal only — Authorization: Bearer <INTERNAL_API_SECRET>. Called by pg_cron on the campaign’s
schedulerIntervalSeconds.
Runs one tick of EmailCampaignService.processBroadcast and returns:
{
"emailsEnqueued": 25,
"throttled": false,
"throttleReason": null,
"variationGenerationRequested": false,
"message": "…"
}
throttled: true means the condition will clear on its own (a cap window resetting).
throttled: false with zero enqueued means an operator has to act — no active variations, or no
active sending accounts. See Email campaigns.
Recipients
GET /api/email-campaigns/{id}/targets
page (default 1), page_size… — note this endpoint uses pageSize (default 25, max 500).
| Parameter | Filter |
|---|---|
|
Address or name substring |
|
|
|
|
|
Repeatable |
POST /api/email-campaigns/{id}/targets
Imports recipients. Contacts are upserted first, so EmailCampaignTarget.contactId and emailId are
always populated. Validated by ImportEmailTargetsSchema or ImportContactIdsSchema depending on the
payload shape.
PATCH /api/email-campaigns/{id}/targets/{tid}
Updates one recipient — status corrections and category overrides.
POST /api/email-campaigns/{id}/targets/from-registry
Imports from the TargetEmails registry by filter, rather than from an uploaded file.
GET /api/email-campaigns/{id}/targets/from-registry/resolve
Previews how many registry rows a filter would import, so an operator can check the blast radius first.
POST /api/email-campaigns/{id}/targets/from-segment
Queues a one-shot copy of a segment’s current active members. Body { "segment_id": "<uuid>" }.
Returns 202 { queued, campaign_id, segment_id }. Also stores segmentId on the campaign
(assignment only; members are still copied asynchronously). The campaign must be type = email
and owned; the segment must be active and in the same subscription. See Email workflows.
Campaign variations
POST /api/email-campaigns/{id}/variations
Creates a variation and its Resend template. This is also the path used when copying a campaign-less template into a campaign.
DELETE /api/email-campaigns/{id}/variations/{vid}
Removes a variation. Prefer setting status = archived when the variation has already been sent, so
historical recipients keep a resolvable variationId.
The reusable template library is at Email content and domain endpoints.