Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,38 @@ api
.catch((err) => console.log(err.statusCode, err.message));
```

#### WhatsApp

Create a new `SendWhatsAppRequest` object containing:

- `transactional_message_id`: the ID or trigger name of the transactional message you want to send.
- an `identifiers` object containing the `id` or `email` of your recipient. If the profile does not exist, Customer.io will create it.
- `to` and `from`: E.164-formatted phone numbers. Both are optional only when the referenced `transactional_message_id` already defines them.

Use `sendWhatsApp` referencing your request to send a transactional message. [Learn more about transactional messages](https://customer.io/docs/transactional-api).

```javascript
const { APIClient, SendWhatsAppRequest, RegionUS, RegionEU } = require("customerio-node");
const api = new APIClient("app-key", { region: RegionUS });

const request = new SendWhatsAppRequest({
to: "+15558675309",
from: "+15551234567",
transactional_message_id: "3",
message_data: {
name: "Person",
},
identifiers: {
id: "2",
},
});

api
.sendWhatsApp(request)
.then((res) => console.log(res))
.catch((err) => console.log(err.statusCode, err.message));
```

### api.triggerBroadcast(campaign_id, data, recipients)

Trigger an email broadcast using the broadcast ID. You can also optionally pass along custom data that will be merged with the liquid template, and additional conditions to filter recipients.
Expand Down
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export type {
SendEmailRequestOptions,
SendPushRequestOptions,
SendSMSRequestOptions,
SendWhatsAppRequestOptions,
SendInboxMessageRequestOptions,
SendInAppRequestOptions,
EmailMessage,
PushMessage,
SMSMessage,
WhatsAppMessage,
InboxMessage,
InAppMessage,
} from './lib/api/requests';
17 changes: 17 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SendEmailRequest,
SendPushRequest,
SendSMSRequest,
SendWhatsAppRequest,
SendInboxMessageRequest,
SendInAppRequest,
} from './api/requests';
Expand Down Expand Up @@ -391,6 +392,21 @@ export class APIClient {
return this.request.post(`${this.apiRoot}/send/sms`, req.message);
}

/**
* Send a transactional WhatsApp message.
*
* @param req A constructed {@link SendWhatsAppRequest} instance.
* @returns The parsed JSON response body.
* @throws {Error} If `req` is not a {@link SendWhatsAppRequest} instance.
*/
sendWhatsApp(req: SendWhatsAppRequest) {
if (!(req instanceof SendWhatsAppRequest)) {
throw new Error('"request" must be an instance of SendWhatsAppRequest');
}

return this.request.post(`${this.apiRoot}/send/whatsapp`, req.message);
}

/**
* Send a transactional inbox message.
*
Expand Down Expand Up @@ -2332,6 +2348,7 @@ export {
SendEmailRequest,
SendPushRequest,
SendSMSRequest,
SendWhatsAppRequest,
SendInboxMessageRequest,
SendInAppRequest,
} from './api/requests';
57 changes: 57 additions & 0 deletions lib/api/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { pickDefined } from '../utils';
const SEND_EMAIL_BRAND = Symbol.for('customerio-node.SendEmailRequest');
const SEND_PUSH_BRAND = Symbol.for('customerio-node.SendPushRequest');
const SEND_SMS_BRAND = Symbol.for('customerio-node.SendSMSRequest');
const SEND_WHATSAPP_BRAND = Symbol.for('customerio-node.SendWhatsAppRequest');
const SEND_INBOX_MESSAGE_BRAND = Symbol.for('customerio-node.SendInboxMessageRequest');
const SEND_IN_APP_BRAND = Symbol.for('customerio-node.SendInAppRequest');

Expand Down Expand Up @@ -285,6 +286,62 @@ export class SendSMSRequest {
}
}

export type WhatsAppMessage = Partial<SendWhatsAppRequestOptions>;

export type SendWhatsAppRequestRequiredOptions = {
identifiers: Identifiers;
transactional_message_id: string | number;
};

export type SendWhatsAppRequestOptionalOptions = Partial<{
to: string;
from: string;
tracked: boolean;
disable_message_retention: boolean;
send_to_unsubscribed: boolean;
queue_draft: boolean;
message_data: Record<string, any>;
send_at: number;
language: string;
}>;

export type SendWhatsAppRequestOptions = SendWhatsAppRequestRequiredOptions & SendWhatsAppRequestOptionalOptions & {};

const WHATSAPP_OPTIONAL_KEYS = [
'to',
'from',
'tracked',
'disable_message_retention',
'send_to_unsubscribed',
'queue_draft',
'message_data',
'send_at',
'language',
] as const satisfies ReadonlyArray<keyof SendWhatsAppRequestOptionalOptions>;

/**
* Builder for a transactional WhatsApp request. Pass an instance to {@link APIClient.sendWhatsApp}.
*
* `to` and `from` are phone numbers in E.164 format. Both are optional here only
* when the referenced `transactional_message_id` already defines them.
*/
export class SendWhatsAppRequest {
message: WhatsAppMessage;

static [Symbol.hasInstance](instance: unknown): instance is SendWhatsAppRequest {
return typeof instance === 'object' && instance !== null && (instance as any)[SEND_WHATSAPP_BRAND] === true;
}

constructor(opts: SendWhatsAppRequestOptions) {
Object.defineProperty(this, SEND_WHATSAPP_BRAND, { value: true });
this.message = {
identifiers: opts.identifiers,
transactional_message_id: opts.transactional_message_id,
...pickDefined(opts, WHATSAPP_OPTIONAL_KEYS),
};
}
}

export type InboxMessage = Partial<SendInboxMessageRequestOptions>;

export type SendInboxMessageRequestRequiredOptions = {
Expand Down
70 changes: 70 additions & 0 deletions test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SendEmailRequest,
SendPushRequest,
SendSMSRequest,
SendWhatsAppRequest,
SendInboxMessageRequest,
SendInAppRequest,
} from '../lib/api';
Expand Down Expand Up @@ -649,6 +650,75 @@ test('#sendSMS: error', async (t) => {
t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/sms`, req.message));
});

test('sendWhatsApp: passing in a plain object throws an error', (t) => {
sinon.stub(t.context.client.request, 'post');

let req = { identifiers: { id: '2' }, transactional_message_id: 1 };

t.throws(() => t.context.client.sendWhatsApp(req as any), {
message: /"request" must be an instance of SendWhatsAppRequest/,
});
t.falsy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/whatsapp`));
});

test('#sendWhatsApp: with template: success', (t) => {
sinon.stub(t.context.client.request, 'post');
let req = new SendWhatsAppRequest({
to: '+1234567890',
identifiers: { id: '2' },
transactional_message_id: 1,
});
t.context.client.sendWhatsApp(req);
t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/whatsapp`, req.message));
t.is(req.message.transactional_message_id, 1);
t.is(req.message.to, '+1234567890');
});

test('#sendWhatsApp: with optional parameters: success', (t) => {
sinon.stub(t.context.client.request, 'post');
let req = new SendWhatsAppRequest({
to: '+1234567890',
from: '+1987654321',
identifiers: { id: '2' },
transactional_message_id: 1,
message_data: { key: 'value' },
tracked: true,
disable_message_retention: true,
send_to_unsubscribed: true,
queue_draft: true,
send_at: 1234567890,
language: 'en',
});
t.context.client.sendWhatsApp(req);
t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/whatsapp`, req.message));
t.is(req.message.transactional_message_id, 1);
t.is(req.message.to, '+1234567890');
t.is(req.message.from, '+1987654321');
t.deepEqual(req.message.message_data, { key: 'value' });
t.true(req.message.tracked);
t.true(req.message.disable_message_retention);
t.true(req.message.send_to_unsubscribed);
t.true(req.message.queue_draft);
t.is(req.message.send_at, 1234567890);
t.is(req.message.language, 'en');
});

test('#sendWhatsApp: error', async (t) => {
sinon.stub(t.context.client.request, 'post').rejects({ message: 'sample error', statusCode: 400 });

let req = new SendWhatsAppRequest({
to: '+1234567890',
identifiers: { id: '2' },
transactional_message_id: 1,
});
t.context.client.sendWhatsApp(req).catch((err) => {
t.is(err.message, 'sample error');
t.is(err.statusCode, 400);
});

t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/whatsapp`, req.message));
});

test('sendInboxMessage: passing in a plain object throws an error', (t) => {
sinon.stub(t.context.client.request, 'post');

Expand Down
2 changes: 2 additions & 0 deletions test/integration/live.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ CIO_TEST_EMAIL_RECIPIENT=
CIO_TEST_TRANSACTIONAL_ID=
CIO_TEST_SMS_RECIPIENT=
CIO_TEST_SMS_TRANSACTIONAL_ID=
CIO_TEST_WHATSAPP_RECIPIENT=
CIO_TEST_WHATSAPP_TRANSACTIONAL_ID=
CIO_TEST_PUSH_TRANSACTIONAL_ID=
CIO_TEST_PUSH_DEVICE_ID=
CIO_TEST_INAPP_TRANSACTIONAL_ID=
Expand Down
18 changes: 18 additions & 0 deletions test/integration/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* CIO_TEST_NEWSLETTER_ID newsletter id for createDeliveriesExport
* CIO_TEST_SMS_TRANSACTIONAL_ID transactional_message_id for sendSMS
* CIO_TEST_SMS_RECIPIENT E.164 phone number for sendSMS
* CIO_TEST_WHATSAPP_TRANSACTIONAL_ID transactional_message_id for sendWhatsApp
* CIO_TEST_WHATSAPP_RECIPIENT E.164 phone number for sendWhatsApp
* CIO_TEST_PUSH_TRANSACTIONAL_ID transactional_message_id for sendPush
* CIO_TEST_PUSH_DEVICE_ID device id for sendPush + addDevice
* CIO_TEST_MANUAL_SEGMENT_ID manual segment id for add/removeCustomersFromSegment
Expand All @@ -37,6 +39,7 @@ import { APIClient } from '../../lib/api';
import {
SendEmailRequest,
SendSMSRequest,
SendWhatsAppRequest,
SendPushRequest,
SendInAppRequest,
SendInboxMessageRequest,
Expand Down Expand Up @@ -381,6 +384,21 @@ needs('CIO_TEST_SMS_TRANSACTIONAL_ID')('sendSMS delivers a transactional SMS', a
t.truthy(result.delivery_id);
});

needs('CIO_TEST_WHATSAPP_TRANSACTIONAL_ID')('sendWhatsApp delivers a transactional WhatsApp message', async (t) => {
const recipient = process.env.CIO_TEST_WHATSAPP_RECIPIENT;
if (!recipient) {
t.pass('CIO_TEST_WHATSAPP_RECIPIENT not set; skipping send');
return;
}
const req = new SendWhatsAppRequest({
to: recipient,
identifiers: { id: customerId },
transactional_message_id: process.env.CIO_TEST_WHATSAPP_TRANSACTIONAL_ID!,
});
const result = (await api!.sendWhatsApp(req)) as { delivery_id?: string };
t.truthy(result.delivery_id);
});

needs('CIO_TEST_PUSH_TRANSACTIONAL_ID')('sendPush delivers a transactional push', async (t) => {
const req = new SendPushRequest({
identifiers: { id: customerId },
Expand Down