diff --git a/README.md b/README.md index c27d8a9..e4c6324 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,11 @@ All active subscribers, or a saved segment: await senddock.broadcast({ template_id: 'tpl-id' }) await senddock.broadcast({ template_id: 'tpl-id', segment_id: 'seg-id' }) + +await senddock.broadcast({ template_id: 'tpl-id', newsletter_id: 'nl-id' }) ``` -Broadcasts require the instance to have a public URL configured (recipients need a working unsubscribe link). +Broadcasts require the instance to have a public URL configured (recipients need a working unsubscribe link). A newsletter broadcast targets that publication's active members and gives each recipient a per-newsletter unsubscribe link — leaving the rest of the list and their project status untouched. `segment_id` and `newsletter_id` are mutually exclusive. ### Import subscribers diff --git a/package.json b/package.json index 10a17cb..eb0ddb3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@senddock/sdk", - "version": "0.1.0", - "description": "Official TypeScript SDK for the SendDock API — self-hosted email marketing, isolated by project.", + "version": "0.2.0", + "description": "Official TypeScript SDK for the SendDock API \u2014 self-hosted email marketing, isolated by project.", "keywords": [ "senddock", "email", diff --git a/src/client.test.ts b/src/client.test.ts index 8e3700b..8b8742e 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -71,6 +71,16 @@ describe('SendDock client', () => { expect(JSON.parse(init.body)).toEqual({ template_id: 't-1', segment_id: 'seg-1' }) }) + it('sends a newsletter broadcast and honours the per-newsletter unsubscribe', async () => { + const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, { sent: 12, broadcast_id: 'b-9' })) + const sd = makeClient(fetchMock) + + await sd.broadcast({ template_id: 't-1', newsletter_id: 'nl-3' }) + + const [, init] = fetchMock.mock.calls[0]! + expect(JSON.parse(init.body)).toEqual({ template_id: 't-1', newsletter_id: 'nl-3' }) + }) + it('imports subscribers as a bare array with validation flags', async () => { const fetchMock = vi.fn().mockResolvedValue( jsonResponse(200, { diff --git a/src/types.test-d.ts b/src/types.test-d.ts index 956b9a6..941af42 100644 --- a/src/types.test-d.ts +++ b/src/types.test-d.ts @@ -27,6 +27,8 @@ describe('compile-time contract', () => { void sd.sendBatch({ template_id: 't', recipients: [{ data: {} }] }) // @ts-expect-error template_id is required void sd.broadcast({ segment_id: 's' }) + // newsletter targeting is part of the runtime surface + void sd.broadcast({ template_id: 't', newsletter_id: 'nl-1' }) }) it('rejects invalid import rows at compile time', () => { diff --git a/src/types.ts b/src/types.ts index 1b1e45d..636e703 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,13 +58,16 @@ export interface BroadcastRequest { subject?: string variables?: Record html_fields?: string[] + /** Target a saved segment. Mutually exclusive with `newsletter_id`. */ segment_id?: string + /** Target a newsletter — recipients get a per-newsletter unsubscribe link. Mutually exclusive with `segment_id`. */ + newsletter_id?: string } export interface BroadcastResponse { sent: number - failed: number - suppressed?: number + /** The broadcast id, so you can poll `GET /broadcasts` for the final sent/failed/suppressed tallies. */ + broadcast_id?: string } export type SubscriberStatus = 'active' | 'pending' | 'unsubscribed' @@ -116,6 +119,7 @@ export type WebhookEventType = | 'email.clicked' | 'subscriber.created' | 'subscriber.unsubscribed' + | 'subscriber.newsletter_unsubscribed' export interface WebhookEvent> { id: string