@messagebird/bird-plugin-medusa 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.invalid = invalid;
4
+ exports.regionalOrigin = regionalOrigin;
5
+ exports.validateOptions = validateOptions;
6
+ exports.prepareMessage = prepareMessage;
7
+ const utils_1 = require("@medusajs/framework/utils");
8
+ const operations_gen_1 = require("./generated/operations.gen");
9
+ function invalid(message) {
10
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, message);
11
+ }
12
+ function nonBlank(value) {
13
+ return typeof value === "string" && value.trim().length > 0;
14
+ }
15
+ function regionalOrigin(key) {
16
+ if (typeof key !== "string")
17
+ invalid("Set api_key to a current Bird API key.");
18
+ const match = /^bk_([a-z]{2}\d+)_[A-Za-z0-9_-]+$/.exec(key);
19
+ if (!match)
20
+ invalid("Set api_key to a current Bird API key beginning with bk_<region>_.");
21
+ return `https://${match[1]}.platform.bird.com`;
22
+ }
23
+ function validateOptions(options) {
24
+ regionalOrigin(options.api_key);
25
+ if (!Array.isArray(options.channels) ||
26
+ !options.channels.length ||
27
+ options.channels.some((c) => c !== "email" && c !== "sms" && c !== "whatsapp")) {
28
+ invalid("Set channels to email, sms, whatsapp, or a combination of them.");
29
+ }
30
+ for (const field of ["email_from", "sms_from", "whatsapp_from"]) {
31
+ if (options[field] !== undefined && !nonBlank(options[field])) {
32
+ invalid(`Set ${field} to a non-empty sender, or omit it and supply notification.from.`);
33
+ }
34
+ }
35
+ }
36
+ function prepareMessage(notification, options) {
37
+ const channel = notification.channel;
38
+ if ((channel !== "email" && channel !== "sms" && channel !== "whatsapp") ||
39
+ !options.channels.includes(channel)) {
40
+ invalid("This Bird provider is not configured for the notification channel. Enable email, sms, or whatsapp in channels.");
41
+ }
42
+ if (!nonBlank(notification.to))
43
+ invalid("Set notification.to to a recipient.");
44
+ const extra = notification.provider_data ?? {};
45
+ const allowed = new Set([
46
+ "language",
47
+ "idempotency_key",
48
+ ...(channel === "whatsapp" ? ["preview_url"] : ["category"]),
49
+ ...(channel === "email" ? ["cc", "bcc", "reply_to"] : []),
50
+ ]);
51
+ if (Object.keys(extra).some((key) => !allowed.has(key))) {
52
+ invalid("Unsupported provider_data field. See the Bird provider's documented options.");
53
+ }
54
+ const idempotencyKey = extra.idempotency_key;
55
+ if (idempotencyKey !== undefined &&
56
+ (!nonBlank(idempotencyKey) ||
57
+ idempotencyKey.length > 255 ||
58
+ /[\r\n]/.test(idempotencyKey))) {
59
+ invalid("Set provider_data.idempotency_key to a non-empty string of at most 255 characters without line breaks.");
60
+ }
61
+ const language = extra.language;
62
+ if (language !== undefined && !nonBlank(language))
63
+ invalid("Set provider_data.language to a language tag, or omit it.");
64
+ if (notification.data != null &&
65
+ (typeof notification.data !== "object" || Array.isArray(notification.data))) {
66
+ invalid("Set notification.data to an object containing template parameters.");
67
+ }
68
+ const connection = { idempotencyKey: idempotencyKey };
69
+ switch (channel) {
70
+ case "email":
71
+ return {
72
+ ...operations_gen_1.birdOperations.email,
73
+ ...connection,
74
+ body: prepareEmailMessage(notification, options, language),
75
+ };
76
+ case "sms":
77
+ return {
78
+ ...operations_gen_1.birdOperations.sms,
79
+ ...connection,
80
+ body: prepareSmsMessage(notification, options, language),
81
+ };
82
+ case "whatsapp":
83
+ return {
84
+ ...operations_gen_1.birdOperations.whatsapp,
85
+ ...connection,
86
+ body: prepareWhatsAppMessage(notification, options, language),
87
+ };
88
+ }
89
+ }
90
+ function category(value, categories, channel) {
91
+ for (const candidate of categories) {
92
+ if (value === candidate)
93
+ return candidate;
94
+ }
95
+ return invalid(`Unsupported ${channel} category.`);
96
+ }
97
+ function templateSelector(template, prefix) {
98
+ if (!nonBlank(template)) {
99
+ invalid("Set notification.template to a Bird template ID or slug, or supply inline content.");
100
+ }
101
+ const value = template.trim();
102
+ return value.startsWith(prefix) ? { id: value } : { slug: value };
103
+ }
104
+ function prepareEmailMessage(notification, options, language) {
105
+ const from = notification.from?.trim() || options.email_from;
106
+ if (!from) {
107
+ invalid("Set email_from or notification.from to an address on your verified Bird domain.");
108
+ }
109
+ const extra = notification.provider_data ?? {};
110
+ const body = { from, to: [notification.to] };
111
+ const content = notification.content;
112
+ if (content != null) {
113
+ if (extra.language !== undefined) {
114
+ invalid("provider_data.language applies to stored templates. Omit it for inline content.");
115
+ }
116
+ if (!nonBlank(content.subject) ||
117
+ (!nonBlank(content.html) && !nonBlank(content.text))) {
118
+ invalid("Inline email requires content.subject and content.html or content.text.");
119
+ }
120
+ body.subject = content.subject;
121
+ if (content.html !== undefined)
122
+ body.html = content.html;
123
+ if (content.text !== undefined)
124
+ body.text = content.text;
125
+ if (notification.data != null)
126
+ body.parameters = notification.data;
127
+ body.category = category(extra.category ?? "transactional", operations_gen_1.birdOperations.email.categories, "email");
128
+ }
129
+ else {
130
+ body.template = {
131
+ ...templateSelector(notification.template, "emt_"),
132
+ ...(language === undefined ? {} : { language }),
133
+ ...(notification.data == null ? {} : { parameters: notification.data }),
134
+ };
135
+ if (extra.category !== undefined) {
136
+ body.category = category(extra.category, operations_gen_1.birdOperations.email.categories, "email");
137
+ }
138
+ }
139
+ for (const field of ["cc", "bcc", "reply_to"]) {
140
+ const addresses = extra[field];
141
+ if (addresses !== undefined) {
142
+ if (!Array.isArray(addresses) ||
143
+ !addresses.length ||
144
+ addresses.some((address) => !nonBlank(address))) {
145
+ invalid(`Set provider_data.${field} to a non-empty array of email addresses.`);
146
+ }
147
+ body[field] = addresses;
148
+ }
149
+ }
150
+ if (notification.attachments?.length) {
151
+ body.attachments = notification.attachments.map((attachment) => {
152
+ if (attachment.disposition &&
153
+ !["attachment", "inline"].includes(attachment.disposition)) {
154
+ invalid("Email attachment disposition must be attachment or inline.");
155
+ }
156
+ if (attachment.disposition === "inline" && !nonBlank(attachment.id)) {
157
+ invalid("An inline attachment requires an id referenced by cid: in the HTML.");
158
+ }
159
+ if (attachment.id && attachment.disposition === "attachment") {
160
+ invalid("Omit attachment.id for a downloadable attachment, or set disposition to inline.");
161
+ }
162
+ return {
163
+ filename: attachment.filename,
164
+ content: attachment.content,
165
+ ...(attachment.content_type
166
+ ? { content_type: attachment.content_type }
167
+ : {}),
168
+ ...(attachment.id ? { content_id: attachment.id } : {}),
169
+ };
170
+ });
171
+ }
172
+ return body;
173
+ }
174
+ function messagingSender(notification, defaultSender) {
175
+ if (notification.content == null &&
176
+ notification.template?.trim().startsWith("bird_")) {
177
+ if (nonBlank(notification.from)) {
178
+ invalid("Omit notification.from for built-in SMS and WhatsApp templates; Bird selects their sender.");
179
+ }
180
+ return undefined;
181
+ }
182
+ return notification.from?.trim() || defaultSender;
183
+ }
184
+ function prepareSmsMessage(notification, options, language) {
185
+ if (notification.attachments?.length)
186
+ invalid("SMS notifications do not support attachments.");
187
+ const from = messagingSender(notification, options.sms_from);
188
+ const extra = notification.provider_data ?? {};
189
+ const content = notification.content;
190
+ if (content != null) {
191
+ if (extra.language !== undefined) {
192
+ invalid("provider_data.language applies to stored templates. Omit it for inline content.");
193
+ }
194
+ if (!from)
195
+ invalid("Set sms_from or notification.from before sending inline sms content.");
196
+ if (!nonBlank(content.text) ||
197
+ content.html !== undefined ||
198
+ content.subject !== undefined) {
199
+ invalid("Inline SMS requires content.text and does not support content.html or content.subject.");
200
+ }
201
+ if (notification.data && Object.keys(notification.data).length) {
202
+ invalid("Inline SMS does not interpolate notification.data. Render content.text before sending, or use a stored template.");
203
+ }
204
+ return {
205
+ from,
206
+ to: notification.to,
207
+ text: content.text,
208
+ category: category(extra.category ?? "transactional", operations_gen_1.birdOperations.sms.categories, "sms"),
209
+ };
210
+ }
211
+ if (extra.category !== undefined) {
212
+ invalid("Omit provider_data.category for SMS templates; the template supplies it.");
213
+ }
214
+ return {
215
+ to: notification.to,
216
+ ...(from ? { from } : {}),
217
+ template: {
218
+ ...templateSelector(notification.template, "smt_"),
219
+ ...(language === undefined ? {} : { language }),
220
+ ...(notification.data == null ? {} : { parameters: notification.data }),
221
+ },
222
+ };
223
+ }
224
+ function prepareWhatsAppMessage(notification, options, language) {
225
+ const from = messagingSender(notification, options.whatsapp_from);
226
+ const body = {
227
+ to: notification.to,
228
+ ...(from ? { from } : {}),
229
+ };
230
+ const extra = notification.provider_data ?? {};
231
+ if (notification.attachments?.length) {
232
+ invalid("WhatsApp notifications do not support attachments. Use media in an approved template's components.");
233
+ }
234
+ if (notification.content != null) {
235
+ if (extra.language !== undefined) {
236
+ invalid("provider_data.language applies to stored templates. Omit it for inline content.");
237
+ }
238
+ if (!from) {
239
+ invalid("Set whatsapp_from or notification.from before sending inline WhatsApp content.");
240
+ }
241
+ const content = notification.content;
242
+ if (!nonBlank(content.text) ||
243
+ content.html !== undefined ||
244
+ content.subject !== undefined) {
245
+ invalid("Inline WhatsApp requires content.text and does not support content.html or content.subject.");
246
+ }
247
+ if ([...content.text].length > 4096) {
248
+ invalid("Inline WhatsApp content.text must be at most 4096 characters.");
249
+ }
250
+ if (notification.data && Object.keys(notification.data).length) {
251
+ invalid("Inline WhatsApp does not interpolate notification.data. Render content.text before sending, or use a stored template.");
252
+ }
253
+ if (extra.preview_url !== undefined &&
254
+ typeof extra.preview_url !== "boolean") {
255
+ invalid("Set provider_data.preview_url to a boolean, or omit it.");
256
+ }
257
+ body.text = {
258
+ body: content.text,
259
+ ...(extra.preview_url === undefined
260
+ ? {}
261
+ : { preview_url: extra.preview_url }),
262
+ };
263
+ }
264
+ else {
265
+ if (!nonBlank(notification.template)) {
266
+ invalid("Set notification.template to a Bird WhatsApp template ID or slug, or supply inline content.");
267
+ }
268
+ if (extra.preview_url !== undefined) {
269
+ invalid("provider_data.preview_url applies to inline WhatsApp text. Omit it for stored templates.");
270
+ }
271
+ const data = notification.data ?? {};
272
+ if (Object.keys(data).some((key) => key !== "components") ||
273
+ (data.components !== undefined &&
274
+ (!Array.isArray(data.components) ||
275
+ data.components.some((component) => !component ||
276
+ typeof component !== "object" ||
277
+ Array.isArray(component) ||
278
+ !nonBlank(component.type))))) {
279
+ invalid("Set WhatsApp notification.data.components to an array of template component objects with a type, or omit data for a template without parameters.");
280
+ }
281
+ const template = notification.template.trim();
282
+ body.template = {
283
+ ...templateSelector(template, "wat_"),
284
+ ...(language === undefined ? {} : { language }),
285
+ ...(data.components === undefined ? {} : { components: data.components }),
286
+ };
287
+ }
288
+ return body;
289
+ }
@@ -0,0 +1,4 @@
1
+ import { BirdNotificationProviderService } from "./service";
2
+ export { BirdNotificationProviderService };
3
+ declare const _default: import("@medusajs/types").ModuleProviderExports;
4
+ export default _default;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BirdNotificationProviderService = void 0;
4
+ const utils_1 = require("@medusajs/framework/utils");
5
+ const service_1 = require("./service");
6
+ Object.defineProperty(exports, "BirdNotificationProviderService", { enumerable: true, get: function () { return service_1.BirdNotificationProviderService; } });
7
+ exports.default = (0, utils_1.ModuleProvider)(utils_1.Modules.NOTIFICATION, {
8
+ services: [service_1.BirdNotificationProviderService],
9
+ });
@@ -0,0 +1,11 @@
1
+ import type { ProviderSendNotificationDTO, ProviderSendNotificationResultsDTO } from "@medusajs/framework/types";
2
+ import { AbstractNotificationProviderService } from "@medusajs/framework/utils";
3
+ import type { BirdNotificationProviderOptions } from "../../types";
4
+ export declare class BirdNotificationProviderService extends AbstractNotificationProviderService {
5
+ static identifier: string;
6
+ protected readonly options: BirdNotificationProviderOptions;
7
+ protected readonly origin: string;
8
+ static validateOptions(options: BirdNotificationProviderOptions): void;
9
+ constructor(_container: Record<string, unknown>, options: BirdNotificationProviderOptions);
10
+ send(notification: ProviderSendNotificationDTO): Promise<ProviderSendNotificationResultsDTO>;
11
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BirdNotificationProviderService = void 0;
4
+ const utils_1 = require("@medusajs/framework/utils");
5
+ const message_1 = require("../../message");
6
+ class BirdNotificationProviderService extends utils_1.AbstractNotificationProviderService {
7
+ static identifier = "bird";
8
+ options;
9
+ origin;
10
+ static validateOptions(options) {
11
+ (0, message_1.validateOptions)(options);
12
+ }
13
+ constructor(_container, options) {
14
+ super();
15
+ (0, message_1.validateOptions)(options);
16
+ this.options = { ...options, channels: [...options.channels] };
17
+ this.origin = (0, message_1.regionalOrigin)(options.api_key);
18
+ }
19
+ async send(notification) {
20
+ const { method, path, body, idempotencyKey } = (0, message_1.prepareMessage)(notification, this.options);
21
+ const headers = {
22
+ Authorization: `Bearer ${this.options.api_key}`,
23
+ "Content-Type": "application/json",
24
+ Accept: "application/json",
25
+ };
26
+ if (idempotencyKey)
27
+ headers["Idempotency-Key"] = idempotencyKey;
28
+ let serialized;
29
+ try {
30
+ serialized = JSON.stringify(body);
31
+ }
32
+ catch {
33
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Notification content and parameters must be JSON-serializable.");
34
+ }
35
+ let response;
36
+ let result;
37
+ try {
38
+ response = await fetch(`${this.origin}${path}`, {
39
+ method,
40
+ headers,
41
+ body: serialized,
42
+ redirect: "error",
43
+ signal: AbortSignal.timeout(30_000),
44
+ });
45
+ result = await response.json().catch(() => undefined);
46
+ }
47
+ catch {
48
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "Bird could not confirm acceptance of the notification. Check connectivity and Bird message history before retrying; reuse provider_data.idempotency_key when supplied.");
49
+ }
50
+ const data = result && typeof result === "object"
51
+ ? result
52
+ : {};
53
+ if (!response.ok) {
54
+ const error = data.error && typeof data.error === "object"
55
+ ? data.error
56
+ : {};
57
+ const details = [formatErrorDetail(error), error.remediation].filter((value) => typeof value === "string" && value.length > 0);
58
+ if (Array.isArray(error.details)) {
59
+ for (const detail of error.details) {
60
+ if (detail && typeof detail === "object") {
61
+ const description = formatErrorDetail(detail);
62
+ if (description)
63
+ details.push(description);
64
+ }
65
+ }
66
+ }
67
+ const message = details.length
68
+ ? details.join(" ")
69
+ : "Check the API key, its channel permissions, and your Bird sender configuration.";
70
+ const safe = message
71
+ .replaceAll(this.options.api_key, "[redacted]")
72
+ .replace(/\b(?:bk|bt)_[a-z]{2}\d+_[A-Za-z0-9_-]+/g, "[redacted]")
73
+ .slice(0, 2000);
74
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, `Bird rejected the notification (HTTP ${response.status}): ${safe}`);
75
+ }
76
+ if (typeof data.id !== "string" || !data.id.length) {
77
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "Bird returned no message ID. Check Bird message history before retrying; acceptance could not be confirmed.");
78
+ }
79
+ return { id: data.id };
80
+ }
81
+ }
82
+ exports.BirdNotificationProviderService = BirdNotificationProviderService;
83
+ function formatErrorDetail(error) {
84
+ const message = typeof error.message === "string" ? error.message : "";
85
+ const param = typeof error.param === "string" ? error.param : "";
86
+ return [param, message].filter(Boolean).join(": ") || undefined;
87
+ }
@@ -0,0 +1,17 @@
1
+ import type { birdOperations, EmailMessageRequest, SmsMessageRequest } from "./generated/operations.gen";
2
+ export type BirdNotificationProviderOptions = {
3
+ api_key: string;
4
+ channels: (keyof typeof birdOperations)[];
5
+ email_from?: string;
6
+ sms_from?: string;
7
+ whatsapp_from?: string;
8
+ };
9
+ export type BirdNotificationProviderData = {
10
+ language?: string;
11
+ category?: EmailMessageRequest["category"] | SmsMessageRequest["category"];
12
+ idempotency_key?: string;
13
+ preview_url?: boolean;
14
+ cc?: string[];
15
+ bcc?: string[];
16
+ reply_to?: string[];
17
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@messagebird/bird-plugin-medusa",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Bird email, SMS, and WhatsApp notifications for Medusa v2.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Bird",
8
+ "email": "connectors@bird.com"
9
+ },
10
+ "homepage": "https://bird.com",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/messagebird/bird-plugin-medusa.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/messagebird/bird-plugin-medusa/issues"
17
+ },
18
+ "keywords": [
19
+ "medusa-v2",
20
+ "medusa-plugin-integration",
21
+ "medusa-plugin-notification",
22
+ "medusa-provider",
23
+ "bird",
24
+ "messagebird",
25
+ "email",
26
+ "sms",
27
+ "whatsapp",
28
+ "notifications"
29
+ ],
30
+ "engines": {
31
+ "node": "^20.19.0 || >=22.12.0"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./providers/bird": {
41
+ "types": "./dist/providers/bird/index.d.ts",
42
+ "default": "./dist/providers/bird/index.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "whatsapp.md",
50
+ "LICENSE"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org"
55
+ },
56
+ "scripts": {
57
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc6",
58
+ "typecheck": "tsc6 -p tsconfig.typecheck.json",
59
+ "test": "npm run typecheck && vitest run src",
60
+ "lint": "eslint src && npm run typecheck",
61
+ "prepack": "npm run build",
62
+ "test:integration": "npm run build && node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config jest.integration.config.cjs --runInBand"
63
+ },
64
+ "peerDependencies": {
65
+ "@medusajs/framework": "^2.21.2"
66
+ },
67
+ "devDependencies": {
68
+ "@medusajs/eslint-plugin": "2.21.2",
69
+ "@medusajs/framework": "2.21.2",
70
+ "@medusajs/notification": "2.21.2",
71
+ "@medusajs/notification-local": "2.21.2",
72
+ "@medusajs/test-utils": "2.21.2",
73
+ "@testcontainers/postgresql": "11.12.0",
74
+ "@types/node": "22.20.5",
75
+ "eslint": "9.39.5",
76
+ "jest": "29.7.0",
77
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
78
+ "vitest": "5.0.5"
79
+ }
80
+ }
package/whatsapp.md ADDED
@@ -0,0 +1,54 @@
1
+ # WhatsApp notifications
2
+
3
+ Enable `whatsapp` in the Bird provider's `channels` and give your API key `whatsapp:write`. The provider uses the same regional API host, error handling, and idempotency behavior as email and SMS.
4
+
5
+ ## Send an approved template
6
+
7
+ Set `template` to a WhatsApp template ID (`wat_…`) or slug. Pass the approved language's components in `data.components`, using Bird's [template component format](https://bird.com/docs/api/reference/create-whatsapp-message). Email and SMS parameter maps do not apply to WhatsApp.
8
+
9
+ This built-in delivery template selects its own sender. The provider skips `whatsapp_from` for `bird_…` slugs; omit `from` in the notification:
10
+
11
+ ```ts
12
+ await notificationModuleService.createNotifications({
13
+ to: customer.phone,
14
+ channel: "whatsapp",
15
+ template: "bird_delivery_update",
16
+ data: {
17
+ components: [
18
+ {
19
+ type: "body",
20
+ parameters: [
21
+ { type: "text", name: "ref", text: String(order.display_id) },
22
+ { type: "text", name: "date", text: deliveryDate },
23
+ ],
24
+ },
25
+ ],
26
+ },
27
+ idempotency_key: `delivery-update/${order.id}`,
28
+ provider_data: {
29
+ language: "en",
30
+ idempotency_key: `delivery-update/${order.id}`,
31
+ },
32
+ });
33
+ ```
34
+
35
+ For your workspace's approved templates, set `whatsapp_from` to a connected number belonging to the template's WhatsApp Business Account, or supply `notification.from`. Use the parameter names or positions required by the approved language. Header media, buttons, and carousel parameters stay inside their template components; Bird validates their shape against the template. Omit `data` for a template without parameters. Template categories come from the template; `provider_data.category` is unsupported.
36
+
37
+ ## Send text in an open conversation
38
+
39
+ Freeform text requires an open 24-hour customer service window for the recipient and sender. Use a connected sender and render the final text in your application:
40
+
41
+ ```ts
42
+ await notificationModuleService.createNotifications({
43
+ to: customer.phone,
44
+ from: process.env.BIRD_WHATSAPP_FROM,
45
+ channel: "whatsapp",
46
+ template: "delivery-reply",
47
+ content: { text: `Your tracking link: ${trackingUrl}` },
48
+ provider_data: { preview_url: true },
49
+ });
50
+ ```
51
+
52
+ Medusa requires a `template` label even when `content` supplies the text. Inline WhatsApp accepts 1–4096 characters in `content.text`; it rejects HTML, subjects, attachments, template `data`, and `provider_data.language`. Link previews are disabled unless `provider_data.preview_url` is `true`.
53
+
54
+ Bird enforces the service window. A closed window returns an error with instructions to use an approved template. A message accepted just before the window closes can still fail asynchronously. Inspect the Bird message or its webhooks for delivery; Medusa's success status records API acceptance.