Context
CMP needs Datamailer-managed recipient lists for course-derived groups:
- Everyone who registered for a course campaign.
- Everyone enrolled in a course.
- Everyone who submitted a homework.
- Everyone who submitted a project.
- Later: pending peer reviewers, certificate-eligible learners, graduates.
We should not overload tags for these lists. Tags are audience-scoped and broad; recipient lists need client/audience scope, membership audit, backfills, counters, and reconciliation state.
Proposed model
RecipientList:
client
audience
key, unique with client and audience, for example homework-submitters:ml-zoomcamp-2026:homework-1
type, for example homework_submitters, project_submitters, course_registrants
name
metadata
- counters:
member_count, active_member_count
last_reconciled_at
- timestamps
RecipientListMember:
recipient_list
contact
email_snapshot
source_object_key, for example homework-submission:123
metadata
active
removed_at
- timestamps
Required uniqueness:
(client, audience, key)
(recipient_list, source_object_key)
(recipient_list, contact)
API shape
PUT /api/recipient-lists/{key}
GET /api/recipient-lists/{key}
PUT /api/recipient-lists/{key}/members/{source_object_key}
POST /api/recipient-lists/{key}/members/bulk-upsert
POST /api/recipient-lists/{key}/reconcile
The member upsert should create the parent list when needed, using list metadata supplied by the request.
Acceptance criteria
- Client API can create/update a recipient list idempotently.
- Client API can upsert one member by
source_object_key idempotently.
- Client API can bulk upsert members.
- Reconcile endpoint accepts a full source snapshot, supports
dry_run, and can soft-remove absent members with remove_absent=true.
- List responses include active/removed counts and last reconciliation status.
- Tests cover client scoping: two clients can use the same list key without conflict.
- Tests cover duplicate contact/source keys and soft removal.
Context
CMP needs Datamailer-managed recipient lists for course-derived groups:
We should not overload tags for these lists. Tags are audience-scoped and broad; recipient lists need client/audience scope, membership audit, backfills, counters, and reconciliation state.
Proposed model
RecipientList:clientaudiencekey, unique with client and audience, for examplehomework-submitters:ml-zoomcamp-2026:homework-1type, for examplehomework_submitters,project_submitters,course_registrantsnamemetadatamember_count,active_member_countlast_reconciled_atRecipientListMember:recipient_listcontactemail_snapshotsource_object_key, for examplehomework-submission:123metadataactiveremoved_atRequired uniqueness:
(client, audience, key)(recipient_list, source_object_key)(recipient_list, contact)API shape
PUT /api/recipient-lists/{key}GET /api/recipient-lists/{key}PUT /api/recipient-lists/{key}/members/{source_object_key}POST /api/recipient-lists/{key}/members/bulk-upsertPOST /api/recipient-lists/{key}/reconcileThe member upsert should create the parent list when needed, using list metadata supplied by the request.
Acceptance criteria
source_object_keyidempotently.dry_run, and can soft-remove absent members withremove_absent=true.