Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
Campaign Submodule
recipient-lists
Named lists of recipients that campaigns target. Supports manual entry, CSV import, CRM sync, and public signup.
Source path: libraries/campaign/src/services/campaign-recipient-list.service.ts, campaign-realtime.service.ts, dto/public-signup.dto.ts
Module file: campaign.module.ts
Introduction
The Recipient Lists submodule models `campaign_recipient_list`, the audience container a campaign points at via `recipient_list_id`. Every list carries a unique, validated slug (lowercase letters, numbers, hyphens, minimum 2 characters — auto-generated from the name if not supplied), a `source` classification ("manual" | "import" | "crm" | "mixed") that reflects how its recipients were populated, and rolled-up counters (`total_recipients`, `total_active`, `total_unsubscribed`, `total_suppressed`, `total_invalid`) that CampaignRecipientListService recalculates after every mutation rather than computing on read. All routes are dual-mounted under both `/campaign/lists/*` and `/campaign/recipient-lists/*` — the controller registers each handler twice with NestJS's multi-decorator support — so both naming conventions resolve identically; this documentation uses `/campaign/recipient-lists/*` as the canonical form.
Recipients can be added to a list three ways. Inline JSON (`POST .../recipients` with an array of `{ email, name?, ... }` objects or a single `{ email }` shorthand) upserts by email within the list. CRM sync (`person_ids` in the same endpoint) resolves each CRM Person's primary EMAIL-type contact, skipping people with no valid email and reporting them back in a `skipped` array, then flips the list's `source` to "mixed" (or "crm" if every recipient came from CRM) once at least one person is added. CSV import is a two-step flow: `POST .../recipients/import/preview` parses the uploaded file (custom hand-rolled CSV parser supporting both comma and semicolon delimiters and quoted fields, capped at 10 MB / enforced again at 5000 data rows) and returns detected columns plus a 20-row sample without persisting anything; `POST .../recipients/import` takes the same file plus a JSON `mapping` of CSV columns to recipient fields and performs the actual upsert-by-email, optionally linking each imported row to an existing CRM company via `company_id`.
Public signup (`POST /campaign/lists/:slug/signup`, unauthenticated) lets a list opt into self-service subscription. It only works when `allow_public_signup` is true on the list; when `enable_captcha` is also true, the configured CAPTCHA Integration Profile (reCAPTCHA, Cloudflare Turnstile, or Altcha) is invoked to validate the submitted `captcha_token` before the email is upserted. This is the typical entry point for embeddable newsletter signup forms.
The submodule also hosts the campaign realtime cursor (`GET /campaign/realtime/cursor`, `GET /campaign/realtime/stream`): every list mutation calls `CampaignRealtimeService.publish()`, bumping an in-memory sequence counter that admin UIs poll or subscribe to over Server-Sent Events to know when to refetch list data, without the server tracking per-client state.
HTTP Endpoints
15 endpoints
Get the current realtime update cursor (sequence number and last-updated timestamp) for polling-based refresh.
Open a Server-Sent Events stream that pushes the updated cursor whenever a recipient list changes (create, update, delete, recipient mutation).
Aggregate statistics across all recipient lists: total lists and summed recipient/active/unsubscribed/suppressed/invalid counters. Also available at /campaign/lists/stats.
Paginated list of recipient lists. Also available at /campaign/lists.
- Query
- page, pageSize, search, status, source
Create a recipient list. Also available at /campaign/lists.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | — |
| slug | string | no | Auto-generated from name if omitted; lowercase letters/numbers/hyphens, min 2 chars, must be unique |
| description | string | no | — |
| source | "manual" | "import" | "crm" | "mixed" | no | Default: "manual" |
| status | "active" | "inactive" | no | Default: "active" |
| allow_public_signup | boolean | no | Default: false |
| enable_captcha | boolean | no | Default: false |
| captcha_integration_profile_id | number | no | Required when enable_captcha is true |
| metadata | object | no | — |
Bulk delete recipient lists. Also available at /campaign/lists.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| ids | number[] | yes | IDs to delete |
Get a recipient list by ID. Also available at /campaign/lists/:id.
- Params
- id (int)
Update a recipient list. Only provided fields are changed. Also available at /campaign/lists/:id.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | no | — |
| description | string | no | — |
| source | "manual" | "import" | "crm" | "mixed" | no | — |
| status | "active" | "inactive" | no | — |
| metadata | object | no | — |
| slug | string | no | Must remain unique |
| allow_public_signup | boolean | no | — |
| enable_captcha | boolean | no | — |
| captcha_integration_profile_id | number | no | — |
Delete a single recipient list. Also available at /campaign/lists/:id.
- Params
- id (int)
Paginated list of recipients in a specific list. Also available at /campaign/lists/:id/recipients.
- Params
- id (int)
- Query
- page, pageSize, search, status
Preview a CSV import: parses delimiters/quoting and returns detected columns plus a 20-row sample without persisting any data. Rejects files over 5000 data rows. Also available at /campaign/lists/:id/recipients/import/preview.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| file | file (CSV, max 10 MB) | yes | multipart/form-data upload; text/csv or application/vnd.ms-excel |
Import recipients from a CSV file. Upserts by email within the list and optionally creates linked CRM Person records. Also available at /campaign/lists/:id/recipients/import.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| file | file (CSV, max 10 MB) | yes | multipart/form-data upload |
| mapping | string (JSON) | yes | JSON object mapping CSV column names to recipient fields (e.g. {"Email":"email","Nome":"name"}) |
| company_id | number | no | CRM company ID to associate imported recipients/persons |
Add recipients to a list from CRM person IDs or inline data. Upserts by email and recalculates list counters. Also available at /campaign/lists/:id/recipients.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| person_ids | number[] | no | CRM person IDs; auto-resolves primary EMAIL contact. When provided, other fields are ignored |
| recipients | object[] | no | Array of { email, name?, first_name?, last_name?, company?, phone?, status?, source?, person_id?, metadata? } |
| string | no | Shorthand for adding a single recipient by email |
Remove specific recipients from a list. Also available at /campaign/lists/:id/recipients.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| ids | number[] | yes | Recipient IDs to remove from this list |
Public signup for a recipient list identified by slug. Only works when the list has allow_public_signup enabled. Validates CAPTCHA (reCAPTCHA, Cloudflare Turnstile, or Altcha) when enable_captcha is set, then upserts the recipient as active.
- Params
- slug (string) — public slug of the recipient list
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| string | yes | Valid email address | |
| name | string | no | — |
| first_name | string | no | — |
| last_name | string | no | — |
| company | string | no | — |
| phone | string | no | — |
| captcha_token | string | no | Required when the list has enable_captcha set |