From d326b06f1af9499d338e870001e2d49686f0eb2c Mon Sep 17 00:00:00 2001 From: thethp Date: Wed, 10 Jun 2026 16:23:34 -0500 Subject: [PATCH 1/3] CDP-6131: make triggerBroadcast data and recipients optional The README documents both data and recipients as optional, but the signature required them: calling with only a broadcastId crashed with a TypeError on recipients[field], and passing {} for recipients sent an empty recipients filter that the API rejects with a 422. Empty or omitted data/recipients are now left out of the payload, so triggering a broadcast with just its id sends to the broadcast's configured recipients as documented. Co-Authored-By: Claude Fable 5 --- lib/api.ts | 33 ++++++++++++++++++++------------- test/api.ts | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/lib/api.ts b/lib/api.ts index d41c19c..6cf0ded 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -221,24 +221,31 @@ export class APIClient { * Otherwise the entire `recipients` object is forwarded verbatim alongside * `data` (use this for segment-based recipients). * + * Both `data` and `recipients` are optional; omitting `recipients` sends the + * broadcast to its configured recipients. + * * @param broadcastId The broadcast (campaign) id. * @param data Liquid `data` payload made available to the broadcast template. * @param recipients Recipient selector. See above. * @returns The parsed JSON response body. */ - triggerBroadcast(broadcastId: string | number, data: RequestData, recipients: Recipients) { - let payload = {}; - let customRecipientField = ( - Object.keys(BROADCASTS_ALLOWED_RECIPIENT_FIELDS) as BroadcastsAllowedRecipientFieldsKeys[] - ).find((field) => recipients[field]); - - if (customRecipientField) { - payload = Object.assign({ data }, filterRecipientsDataForField(recipients, customRecipientField)); - } else { - payload = { - data, - recipients, - }; + triggerBroadcast(broadcastId: string | number, data?: RequestData, recipients?: Recipients) { + let payload: Record = {}; + + if (data && Object.keys(data).length > 0) { + payload.data = data; + } + + if (recipients && Object.keys(recipients).length > 0) { + let customRecipientField = ( + Object.keys(BROADCASTS_ALLOWED_RECIPIENT_FIELDS) as BroadcastsAllowedRecipientFieldsKeys[] + ).find((field) => recipients[field]); + + if (customRecipientField) { + payload = Object.assign(payload, filterRecipientsDataForField(recipients, customRecipientField)); + } else { + payload.recipients = recipients; + } } return this.request.post(`${this.apiRoot}/campaigns/${encodeURIComponent(broadcastId)}/triggers`, payload); diff --git a/test/api.ts b/test/api.ts index 9127a41..6a18f6c 100644 --- a/test/api.ts +++ b/test/api.ts @@ -390,6 +390,28 @@ test('#triggerBroadcast discards extraneous fields', (t) => { ); }); +test('#triggerBroadcast works with broadcastId only', (t) => { + sinon.stub(t.context.client.request, 'post'); + t.context.client.triggerBroadcast(1); + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/campaigns/1/triggers`, {})); +}); + +test('#triggerBroadcast works with data and no recipients', (t) => { + sinon.stub(t.context.client.request, 'post'); + t.context.client.triggerBroadcast(1, { type: 'data' }); + t.truthy( + (t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/campaigns/1/triggers`, { + data: { type: 'data' }, + }), + ); +}); + +test('#triggerBroadcast omits empty data and recipients objects', (t) => { + sinon.stub(t.context.client.request, 'post'); + t.context.client.triggerBroadcast(1, {}, {}); + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/campaigns/1/triggers`, {})); +}); + test('#listExports: success', (t) => { sinon.stub(t.context.client.request, 'get'); t.context.client.listExports(); From 17c6112a15ab71fcc19a282447e7951d163391c1 Mon Sep 17 00:00:00 2001 From: thethp Date: Wed, 10 Jun 2026 16:47:24 -0500 Subject: [PATCH 2/3] CDP-6131: document positional recipients-without-data call shape triggerBroadcast(1, { emails: [...] }) type-checks but sends the selector as liquid data, triggering the broadcast's configured recipients. Document the undefined placeholder pattern in the JSDoc and README, and pin triggerBroadcast(1, undefined, recipients) with a test. Co-Authored-By: Claude Fable 5 --- README.md | 7 +++++++ lib/api.ts | 6 ++++++ test/api.ts | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/README.md b/README.md index 2a112b7..1e4d7d3 100644 --- a/README.md +++ b/README.md @@ -450,6 +450,13 @@ api.triggerBroadcast(1, { name: "foo" }, { emails: ["example@emails.com"], email [You can learn more about the available recipient fields here](https://customer.io/docs/api/#operation/triggerBroadcast). +Both `data` and `recipients` are optional. Omitting `recipients` sends the broadcast to its configured recipients. Note that the parameters are positional: to pass `recipients` without `data`, pass `undefined` for `data` — passing the recipient selector as the second argument would send it as liquid data instead. + +```javascript +api.triggerBroadcast(1); // broadcast's configured recipients +api.triggerBroadcast(1, undefined, { emails: ["example@emails.com"], email_ignore_missing: true }); +``` + #### Options - **id**: String or number (required) diff --git a/lib/api.ts b/lib/api.ts index 6cf0ded..9147994 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -224,6 +224,12 @@ export class APIClient { * Both `data` and `recipients` are optional; omitting `recipients` sends the * broadcast to its configured recipients. * + * Note that the parameters are positional: to pass `recipients` without + * `data`, pass `undefined` for `data` — e.g. + * `triggerBroadcast(1, undefined, { emails: ['user@example.com'] })`. + * Passing the recipient selector as the second argument would send it as + * liquid `data` and trigger the broadcast's configured recipients instead. + * * @param broadcastId The broadcast (campaign) id. * @param data Liquid `data` payload made available to the broadcast template. * @param recipients Recipient selector. See above. diff --git a/test/api.ts b/test/api.ts index 6a18f6c..8cac3e3 100644 --- a/test/api.ts +++ b/test/api.ts @@ -406,6 +406,17 @@ test('#triggerBroadcast works with data and no recipients', (t) => { ); }); +test('#triggerBroadcast works with recipients and no data', (t) => { + sinon.stub(t.context.client.request, 'post'); + t.context.client.triggerBroadcast(1, undefined, { emails: ['test@email.com'], email_ignore_missing: true }); + t.truthy( + (t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/campaigns/1/triggers`, { + emails: ['test@email.com'], + email_ignore_missing: true, + }), + ); +}); + test('#triggerBroadcast omits empty data and recipients objects', (t) => { sinon.stub(t.context.client.request, 'post'); t.context.client.triggerBroadcast(1, {}, {}); From 6546597b549ca381ac8bdaab9fc6e6eb38708be6 Mon Sep 17 00:00:00 2001 From: thethp Date: Thu, 11 Jun 2026 10:03:13 -0500 Subject: [PATCH 3/3] CDP-6131: use explicit null checks per review Co-Authored-By: Claude Fable 5 --- lib/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api.ts b/lib/api.ts index 9147994..ee6887a 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -238,11 +238,11 @@ export class APIClient { triggerBroadcast(broadcastId: string | number, data?: RequestData, recipients?: Recipients) { let payload: Record = {}; - if (data && Object.keys(data).length > 0) { + if (data != null && Object.keys(data).length > 0) { payload.data = data; } - if (recipients && Object.keys(recipients).length > 0) { + if (recipients != null && Object.keys(recipients).length > 0) { let customRecipientField = ( Object.keys(BROADCASTS_ALLOWED_RECIPIENT_FIELDS) as BroadcastsAllowedRecipientFieldsKeys[] ).find((field) => recipients[field]);