n8n-nodes-warecover-v1 2.8.0
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.
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Warecover — WhatsApp Node for n8n
|
|
2
|
+
|
|
3
|
+
Send WhatsApp messages from your n8n workflows using your Warecover account.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
1. Open n8n → **Settings → Community Nodes**
|
|
10
|
+
2. Click **Install**
|
|
11
|
+
3. Enter: `n8n-nodes-warecover-v2`
|
|
12
|
+
4. Restart n8n
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Connect Your Account
|
|
17
|
+
|
|
18
|
+
1. Go to **Credentials → New → Warecover API**
|
|
19
|
+
2. Enter your:
|
|
20
|
+
- **Access Token** — from Warecover → Channel → API Details
|
|
21
|
+
- **Phone Number ID** — from same page
|
|
22
|
+
3. Save
|
|
23
|
+
|
|
24
|
+
> Your Access Token is shown **only once** in Warecover. Save it immediately!
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Available Actions
|
|
29
|
+
|
|
30
|
+
| Action | Notes |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Send Text Message | ⚠️ 24-hour session required |
|
|
33
|
+
| Send Template Message | ✅ Works anytime |
|
|
34
|
+
| Send Image | ⚠️ 24-hour session required |
|
|
35
|
+
| Send Document | ⚠️ 24-hour session required |
|
|
36
|
+
| Send Audio | ⚠️ 24-hour session required |
|
|
37
|
+
| Send Video | ⚠️ 24-hour session required |
|
|
38
|
+
| Send Location | ⚠️ 24-hour session required |
|
|
39
|
+
| Send Buttons | ⚠️ 24-hour session required |
|
|
40
|
+
| Send List Menu | ⚠️ 24-hour session required |
|
|
41
|
+
| Mark as Read | — |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 24-Hour Session Rule
|
|
46
|
+
|
|
47
|
+
WhatsApp only allows sending messages **if the recipient messaged you in the last 24 hours** — except for Template Messages which work anytime.
|
|
48
|
+
|
|
49
|
+
**Tip:** Start with a Template Message. Once they reply, you can send any message type.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Phone Number Format
|
|
54
|
+
|
|
55
|
+
Use international format without `+`:
|
|
56
|
+
|
|
57
|
+
| Country | Example |
|
|
58
|
+
|---|---|
|
|
59
|
+
| India | `919209576337` |
|
|
60
|
+
| USA | `14155552671` |
|
|
61
|
+
| UAE | `971501234567` |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
Visit **warecover.com** for more info.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WarecoverApi = void 0;
|
|
4
|
+
|
|
5
|
+
class WarecoverApi {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.name = 'warecoverApi';
|
|
8
|
+
this.displayName = 'Warecover';
|
|
9
|
+
this.documentationUrl = 'https://webhook.warecover.com/docs';
|
|
10
|
+
this.properties = [
|
|
11
|
+
// ── Welcome notice ──────────────────────────────────
|
|
12
|
+
{
|
|
13
|
+
displayName: 'New to Warecover? Create your account at webhook.warecover.com',
|
|
14
|
+
name: 'warecoverNotice',
|
|
15
|
+
type: 'notice',
|
|
16
|
+
default: '',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// ── API Key ─────────────────────────────────────────
|
|
20
|
+
{
|
|
21
|
+
displayName: 'API Key',
|
|
22
|
+
name: 'apiKey',
|
|
23
|
+
type: 'string',
|
|
24
|
+
typeOptions: { password: true },
|
|
25
|
+
default: '',
|
|
26
|
+
required: true,
|
|
27
|
+
placeholder: 'wc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
|
28
|
+
description: 'Your Warecover API Key. Go to <a href="https://webhook.warecover.com/api-keys" target="_blank">webhook.warecover.com → API Keys</a> → click <strong>Generate Key</strong>. The key is shown only once — copy it immediately.',
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// ── Access Token ─────────────────────────────────────
|
|
32
|
+
{
|
|
33
|
+
displayName: 'Access Token',
|
|
34
|
+
name: 'accessToken',
|
|
35
|
+
type: 'string',
|
|
36
|
+
typeOptions: { password: true },
|
|
37
|
+
default: '',
|
|
38
|
+
required: true,
|
|
39
|
+
placeholder: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
|
40
|
+
description: 'Your WhatsApp channel access token. Go to <a href="https://webhook.warecover.com" target="_blank">Warecover Dashboard</a> → Channel → API Details → copy <strong>Access Token</strong>.',
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// ── Phone Number ID ──────────────────────────────────
|
|
44
|
+
{
|
|
45
|
+
displayName: 'Phone Number ID',
|
|
46
|
+
name: 'phoneNumberId',
|
|
47
|
+
type: 'string',
|
|
48
|
+
default: '',
|
|
49
|
+
required: true,
|
|
50
|
+
placeholder: '935752596280211',
|
|
51
|
+
description: 'Your WhatsApp Phone Number ID. Go to <a href="https://webhook.warecover.com" target="_blank">Warecover Dashboard</a> → Channel → API Details → copy <strong>Phone Number ID</strong>.',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
this.authenticate = {
|
|
56
|
+
type: 'generic',
|
|
57
|
+
properties: {
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: '=Bearer {{$credentials.accessToken}}',
|
|
60
|
+
'X-Warecover-Key': '={{$credentials.apiKey}}',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.test = {
|
|
66
|
+
request: {
|
|
67
|
+
baseURL: 'https://crm.warecover.com/api/meta/v19.0',
|
|
68
|
+
url: '/{{$credentials.phoneNumberId}}',
|
|
69
|
+
method: 'GET',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.WarecoverApi = WarecoverApi;
|
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Warecover = void 0;
|
|
4
|
+
|
|
5
|
+
class Warecover {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'Warecover',
|
|
9
|
+
name: 'warecover',
|
|
10
|
+
icon: 'file:warecover.svg',
|
|
11
|
+
group: ['output'],
|
|
12
|
+
version: 1,
|
|
13
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
14
|
+
description: 'Send WhatsApp messages via Warecover',
|
|
15
|
+
defaults: { name: 'Warecover' },
|
|
16
|
+
inputs: ['main'],
|
|
17
|
+
outputs: ['main'],
|
|
18
|
+
credentials: [{ name: 'warecoverApi', required: true }],
|
|
19
|
+
properties: [
|
|
20
|
+
{
|
|
21
|
+
displayName: 'Operation',
|
|
22
|
+
name: 'operation',
|
|
23
|
+
type: 'options',
|
|
24
|
+
noDataExpression: true,
|
|
25
|
+
options: [
|
|
26
|
+
// ─────────────────────────────────────────
|
|
27
|
+
// n8n renders section headers when:
|
|
28
|
+
// action is omitted OR empty string
|
|
29
|
+
// name is the header text
|
|
30
|
+
// value starts with _ (makes it non-selectable)
|
|
31
|
+
// ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
// MESSAGING
|
|
34
|
+
{
|
|
35
|
+
name: 'Messaging',
|
|
36
|
+
value: '_sep_messaging',
|
|
37
|
+
action: '',
|
|
38
|
+
description: '',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Send Text Message',
|
|
42
|
+
value: 'sendText',
|
|
43
|
+
action: 'Send a text message',
|
|
44
|
+
description: 'Send a plain text message. Requires active 24-hour session.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Send Template Message',
|
|
48
|
+
value: 'sendTemplate',
|
|
49
|
+
action: 'Send a pre-approved template message',
|
|
50
|
+
description: 'Works outside 24-hour window. No active session needed.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Send Image',
|
|
54
|
+
value: 'sendImage',
|
|
55
|
+
action: 'Send an image',
|
|
56
|
+
description: 'Send a JPG, PNG or WebP image with optional caption.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'Send Document / PDF',
|
|
60
|
+
value: 'sendDocument',
|
|
61
|
+
action: 'Send a file or PDF',
|
|
62
|
+
description: 'Send any document — PDF, DOCX, XLSX, etc.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'Send Audio',
|
|
66
|
+
value: 'sendAudio',
|
|
67
|
+
action: 'Send an audio file',
|
|
68
|
+
description: 'Send an MP3 or OGG audio message.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'Send Video',
|
|
72
|
+
value: 'sendVideo',
|
|
73
|
+
action: 'Send a video',
|
|
74
|
+
description: 'Send an MP4 video with optional caption.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Send Reply Buttons',
|
|
78
|
+
value: 'sendButtons',
|
|
79
|
+
action: 'Send up to 3 interactive reply buttons',
|
|
80
|
+
description: 'Customer taps a button — use in Switch node to route the flow.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'Send CTA URL Button',
|
|
84
|
+
value: 'sendCtaButton',
|
|
85
|
+
action: 'Send a message with a URL button',
|
|
86
|
+
description: 'Button opens a URL — ideal for payment links, forms, or websites.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Send List Menu',
|
|
90
|
+
value: 'sendList',
|
|
91
|
+
action: 'Send a scrollable list of options',
|
|
92
|
+
description: 'Scrollable menu — up to 10 options. Good for product menus or service selection.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Mark Message as Read',
|
|
96
|
+
value: 'markRead',
|
|
97
|
+
action: 'Mark an incoming message as read',
|
|
98
|
+
description: 'Sends a read receipt — customer sees double blue ticks.',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// ORDER HANDLING
|
|
102
|
+
{
|
|
103
|
+
name: 'Order Handling',
|
|
104
|
+
value: '_sep_order',
|
|
105
|
+
action: '',
|
|
106
|
+
description: '',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'Read Incoming Order',
|
|
110
|
+
value: 'parseOrder',
|
|
111
|
+
action: 'Extract products, totals and customer from a catalog order',
|
|
112
|
+
description: 'Parses a WhatsApp catalog order webhook — outputs product list, totals, and customer info.',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'Send Order Confirmation',
|
|
116
|
+
value: 'sendOrderConfirmation',
|
|
117
|
+
action: 'Send an order confirmation message to the customer',
|
|
118
|
+
description: 'Send a customizable confirmation message after receiving an order.',
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ADDRESS COLLECTION
|
|
122
|
+
{
|
|
123
|
+
name: 'Address Collection',
|
|
124
|
+
value: '_sep_address',
|
|
125
|
+
action: '',
|
|
126
|
+
description: '',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'Ask for Address (Native Form)',
|
|
130
|
+
value: 'requestAddressForm',
|
|
131
|
+
action: 'Send a native WhatsApp address form',
|
|
132
|
+
description: 'Customer sees a "Provide address" button — fills structured form. India & Singapore only.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'Read Address Form Reply',
|
|
136
|
+
value: 'parseAddressForm',
|
|
137
|
+
action: 'Extract address from a native form submission',
|
|
138
|
+
description: 'Parses name, phone, address, city, state, pincode from the form webhook.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'Ask for Address (Text)',
|
|
142
|
+
value: 'requestAddressText',
|
|
143
|
+
action: 'Ask customer to type their address',
|
|
144
|
+
description: 'Send a message asking the customer to type their address manually.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Read Address from Chat Reply',
|
|
148
|
+
value: 'parseAddressText',
|
|
149
|
+
action: 'Extract address from a plain text reply',
|
|
150
|
+
description: 'Reads the address the customer typed in chat.',
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// LOCATION
|
|
154
|
+
{
|
|
155
|
+
name: 'Location',
|
|
156
|
+
value: '_sep_location',
|
|
157
|
+
action: '',
|
|
158
|
+
description: '',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'Ask for Live Location',
|
|
162
|
+
value: 'requestLocation',
|
|
163
|
+
action: 'Ask customer to share their live location',
|
|
164
|
+
description: 'Sends a "Share Location" prompt — customer taps to share GPS coordinates.',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Send Location Pin',
|
|
168
|
+
value: 'sendLocation',
|
|
169
|
+
action: 'Send a map location pin to the customer',
|
|
170
|
+
description: 'Send latitude/longitude — customer sees it as a map pin in WhatsApp.',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'Read Incoming Location',
|
|
174
|
+
value: 'parseLocation',
|
|
175
|
+
action: 'Extract location coordinates from a location message',
|
|
176
|
+
description: 'Parses latitude, longitude, and address from a location webhook.',
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// CHAT MANAGEMENT
|
|
180
|
+
{
|
|
181
|
+
name: 'Chat Management',
|
|
182
|
+
value: '_sep_chat',
|
|
183
|
+
action: '',
|
|
184
|
+
description: '',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'Close Chat (Mark as Done)',
|
|
188
|
+
value: 'closeChatDone',
|
|
189
|
+
action: 'Mark the chat as done to allow the next flow to trigger',
|
|
190
|
+
description: 'Use after completing an order, booking, or after no-response timeout.',
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// DETECT & ROUTE
|
|
194
|
+
{
|
|
195
|
+
name: 'Detect & Route',
|
|
196
|
+
value: '_sep_route',
|
|
197
|
+
action: '',
|
|
198
|
+
description: '',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'Detect Button or List Click',
|
|
202
|
+
value: 'detectButton',
|
|
203
|
+
action: 'Detect which button or list item the customer clicked',
|
|
204
|
+
description: 'Returns button_id — connect to a Switch node to route the flow based on customer choice.',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'Read Incoming Message',
|
|
208
|
+
value: 'parseContact',
|
|
209
|
+
action: 'Extract phone, name and message type from any incoming webhook',
|
|
210
|
+
description: 'Outputs customer_phone, customer_name, message_type, and routing flags (is_order, is_button_reply, is_address_form, etc.).',
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
default: 'sendText',
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Section selected notice
|
|
217
|
+
{
|
|
218
|
+
displayName: 'Please select an operation from the list above.',
|
|
219
|
+
name: 'sepNotice',
|
|
220
|
+
type: 'notice',
|
|
221
|
+
default: '',
|
|
222
|
+
displayOptions: {
|
|
223
|
+
show: {
|
|
224
|
+
operation: [
|
|
225
|
+
'_sep_messaging','_sep_order','_sep_address',
|
|
226
|
+
'_sep_location','_sep_chat','_sep_route'
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// 24-HOUR NOTICE
|
|
233
|
+
{
|
|
234
|
+
displayName: '⚠️ 24-Hour Session Required',
|
|
235
|
+
name: 'sessionWindowNotice',
|
|
236
|
+
type: 'notice',
|
|
237
|
+
default: 'This message can only be sent within 24 hours after the recipient last messaged you. Use "Send Template Message" to reach users outside this window.',
|
|
238
|
+
displayOptions: { show: { operation: ['sendText','sendImage','sendDocument','sendAudio','sendVideo','sendButtons','sendCtaButton','sendList','requestLocation','requestAddressForm','requestAddressText'] } },
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// CLOSE CHAT
|
|
242
|
+
{
|
|
243
|
+
displayName: 'Customer Phone',
|
|
244
|
+
name: 'closeChatPhone',
|
|
245
|
+
type: 'string',
|
|
246
|
+
required: true,
|
|
247
|
+
default: '',
|
|
248
|
+
placeholder: '={{$json.customer_phone}}',
|
|
249
|
+
description: 'Phone number of the customer whose chat should be marked as done',
|
|
250
|
+
displayOptions: { show: { operation: ['closeChatDone'] } },
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
// TO
|
|
254
|
+
{
|
|
255
|
+
displayName: 'To (Phone Number)',
|
|
256
|
+
name: 'to',
|
|
257
|
+
type: 'string',
|
|
258
|
+
required: true,
|
|
259
|
+
default: '',
|
|
260
|
+
placeholder: '919209576337',
|
|
261
|
+
description: 'Phone in international format without + sign. India example: 919209576337',
|
|
262
|
+
displayOptions: { show: { operation: ['sendText','sendTemplate','sendImage','sendDocument','sendAudio','sendVideo','sendButtons','sendCtaButton','sendList','requestLocation','sendLocation','requestAddressForm','requestAddressText','sendOrderConfirmation'] } },
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// SEND TEXT
|
|
266
|
+
{ displayName: 'Message', name: 'text', type: 'string', typeOptions: { rows: 5 }, required: true, default: '', description: 'Supports WhatsApp formatting: *bold* _italic_ ~strikethrough~', displayOptions: { show: { operation: ['sendText'] } } },
|
|
267
|
+
{ displayName: 'Show URL Preview', name: 'previewUrl', type: 'boolean', default: false, displayOptions: { show: { operation: ['sendText'] } } },
|
|
268
|
+
|
|
269
|
+
// SEND TEMPLATE
|
|
270
|
+
{ displayName: 'Template Name', name: 'templateName', type: 'string', required: true, default: '', placeholder: 'order_confirmation', description: 'Exact name of the approved template in Meta Business Manager', displayOptions: { show: { operation: ['sendTemplate'] } } },
|
|
271
|
+
{
|
|
272
|
+
displayName: 'Language',
|
|
273
|
+
name: 'languageCode',
|
|
274
|
+
type: 'options',
|
|
275
|
+
options: [
|
|
276
|
+
{ name: 'English (en)', value: 'en' },
|
|
277
|
+
{ name: 'English US (en_US)', value: 'en_US' },
|
|
278
|
+
{ name: 'Hindi (hi)', value: 'hi' },
|
|
279
|
+
{ name: 'Marathi (mr)', value: 'mr' },
|
|
280
|
+
{ name: 'Gujarati (gu)', value: 'gu' },
|
|
281
|
+
{ name: 'Tamil (ta)', value: 'ta' },
|
|
282
|
+
{ name: 'Telugu (te)', value: 'te' },
|
|
283
|
+
{ name: 'Kannada (kn)', value: 'kn' },
|
|
284
|
+
],
|
|
285
|
+
default: 'en',
|
|
286
|
+
displayOptions: { show: { operation: ['sendTemplate'] } },
|
|
287
|
+
},
|
|
288
|
+
{ displayName: 'Template Variables (JSON)', name: 'templateComponents', type: 'json', default: '[]', description: 'Leave [] for templates with no variables.', displayOptions: { show: { operation: ['sendTemplate'] } } },
|
|
289
|
+
|
|
290
|
+
// SEND IMAGE
|
|
291
|
+
{ displayName: 'Image URL', name: 'imageUrl', type: 'string', required: true, default: '', placeholder: 'https://example.com/photo.jpg', displayOptions: { show: { operation: ['sendImage'] } } },
|
|
292
|
+
{ displayName: 'Caption', name: 'imageCaption', type: 'string', default: '', displayOptions: { show: { operation: ['sendImage'] } } },
|
|
293
|
+
|
|
294
|
+
// SEND DOCUMENT
|
|
295
|
+
{ displayName: 'Document URL', name: 'documentUrl', type: 'string', required: true, default: '', placeholder: 'https://example.com/file.pdf', displayOptions: { show: { operation: ['sendDocument'] } } },
|
|
296
|
+
{ displayName: 'File Name (shown to user)', name: 'documentFilename', type: 'string', default: 'document.pdf', displayOptions: { show: { operation: ['sendDocument'] } } },
|
|
297
|
+
{ displayName: 'Caption', name: 'documentCaption', type: 'string', default: '', displayOptions: { show: { operation: ['sendDocument'] } } },
|
|
298
|
+
|
|
299
|
+
// SEND AUDIO
|
|
300
|
+
{ displayName: 'Audio URL', name: 'audioUrl', type: 'string', required: true, default: '', placeholder: 'https://example.com/audio.mp3', displayOptions: { show: { operation: ['sendAudio'] } } },
|
|
301
|
+
|
|
302
|
+
// SEND VIDEO
|
|
303
|
+
{ displayName: 'Video URL', name: 'videoUrl', type: 'string', required: true, default: '', placeholder: 'https://example.com/video.mp4', displayOptions: { show: { operation: ['sendVideo'] } } },
|
|
304
|
+
{ displayName: 'Caption', name: 'videoCaption', type: 'string', default: '', displayOptions: { show: { operation: ['sendVideo'] } } },
|
|
305
|
+
|
|
306
|
+
// SEND BUTTONS
|
|
307
|
+
{ displayName: 'Message Body', name: 'btnBodyText', type: 'string', typeOptions: { rows: 4 }, required: true, default: '', description: 'Main message text shown above the buttons', displayOptions: { show: { operation: ['sendButtons'] } } },
|
|
308
|
+
{ displayName: 'Header Text', name: 'btnHeaderText', type: 'string', default: '', displayOptions: { show: { operation: ['sendButtons'] } } },
|
|
309
|
+
{ displayName: 'Footer Text', name: 'btnFooterText', type: 'string', default: '', displayOptions: { show: { operation: ['sendButtons'] } } },
|
|
310
|
+
{
|
|
311
|
+
displayName: 'Buttons',
|
|
312
|
+
name: 'buttons',
|
|
313
|
+
type: 'fixedCollection',
|
|
314
|
+
typeOptions: { multipleValues: true, maxValue: 3 },
|
|
315
|
+
default: { button: [{ id: 'btn_1', title: 'Option 1' }] },
|
|
316
|
+
description: 'Max 3 buttons. Button ID is returned when user clicks — use in Switch node to route.',
|
|
317
|
+
options: [{
|
|
318
|
+
name: 'button', displayName: 'Button',
|
|
319
|
+
values: [
|
|
320
|
+
{ displayName: 'Button ID', name: 'id', type: 'string', default: '', placeholder: 'btn_cod', description: 'Unique ID returned in webhook when user clicks' },
|
|
321
|
+
{ displayName: 'Button Label', name: 'title', type: 'string', default: '', placeholder: 'Cash on Delivery', description: 'Max 20 characters' },
|
|
322
|
+
],
|
|
323
|
+
}],
|
|
324
|
+
displayOptions: { show: { operation: ['sendButtons'] } },
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// SEND CTA URL BUTTON
|
|
328
|
+
{ displayName: 'Header Text', name: 'ctaHeaderText', type: 'string', default: '', description: 'Bold title above the message body', displayOptions: { show: { operation: ['sendCtaButton'] } } },
|
|
329
|
+
{ displayName: 'Message Body', name: 'ctaBodyText', type: 'string', typeOptions: { rows: 5 }, required: true, default: '', description: 'Main message content', displayOptions: { show: { operation: ['sendCtaButton'] } } },
|
|
330
|
+
{ displayName: 'Footer Text', name: 'ctaFooterText', type: 'string', default: '', displayOptions: { show: { operation: ['sendCtaButton'] } } },
|
|
331
|
+
{ displayName: 'Button Label', name: 'ctaDisplayText', type: 'string', required: true, default: '', placeholder: 'Pay Now', description: 'Text shown on the URL button (max 20 chars)', displayOptions: { show: { operation: ['sendCtaButton'] } } },
|
|
332
|
+
{ displayName: 'Button URL', name: 'ctaUrl', type: 'string', required: true, default: '', placeholder: 'https://rzp.io/l/abc123', description: 'URL that opens when user taps the button', displayOptions: { show: { operation: ['sendCtaButton'] } } },
|
|
333
|
+
|
|
334
|
+
// SEND LIST
|
|
335
|
+
{ displayName: 'Message Body', name: 'listBodyText', type: 'string', typeOptions: { rows: 4 }, required: true, default: '', displayOptions: { show: { operation: ['sendList'] } } },
|
|
336
|
+
{ displayName: 'List Button Label', name: 'listButtonLabel', type: 'string', required: true, default: 'Select Option', displayOptions: { show: { operation: ['sendList'] } } },
|
|
337
|
+
{ displayName: 'Header Text', name: 'listHeaderText', type: 'string', default: '', displayOptions: { show: { operation: ['sendList'] } } },
|
|
338
|
+
{ displayName: 'Footer Text', name: 'listFooterText', type: 'string', default: '', displayOptions: { show: { operation: ['sendList'] } } },
|
|
339
|
+
{ displayName: 'List Sections (JSON)', name: 'listSections', type: 'json', required: true, default: '[{"title":"Options","rows":[{"id":"opt_1","title":"Option 1","description":"Description here"}]}]', displayOptions: { show: { operation: ['sendList'] } } },
|
|
340
|
+
|
|
341
|
+
// MARK AS READ
|
|
342
|
+
{ displayName: 'Message ID', name: 'messageId', type: 'string', required: true, default: '', placeholder: 'wamid.HBgM...', displayOptions: { show: { operation: ['markRead'] } } },
|
|
343
|
+
|
|
344
|
+
// READ INCOMING ORDER
|
|
345
|
+
{ displayName: 'Webhook Body', name: 'webhookBody', type: 'json', required: true, default: '={{$json.body}}', description: 'Full webhook body. Outputs: customer_phone, customer_name, products[], total_items, total_amount, order_summary.', displayOptions: { show: { operation: ['parseOrder'] } } },
|
|
346
|
+
|
|
347
|
+
// SEND ORDER CONFIRMATION
|
|
348
|
+
{ displayName: 'Customer Phone', name: 'confirmTo', type: 'string', required: true, default: '', placeholder: '={{$json.customer_phone}}', displayOptions: { show: { operation: ['sendOrderConfirmation'] } } },
|
|
349
|
+
{ displayName: 'Confirmation Message', name: 'orderConfirmText', type: 'string', typeOptions: { rows: 6 }, required: true, default: 'Order received. Our team will confirm it shortly.', displayOptions: { show: { operation: ['sendOrderConfirmation'] } } },
|
|
350
|
+
|
|
351
|
+
// ASK FOR ADDRESS — NATIVE FORM
|
|
352
|
+
{
|
|
353
|
+
displayName: '✅ India & Singapore Only',
|
|
354
|
+
name: 'addressFormNotice',
|
|
355
|
+
type: 'notice',
|
|
356
|
+
default: 'Sends WhatsApp native address form — customer sees a "Provide address" button. Only works for Indian & Singapore phone numbers.',
|
|
357
|
+
displayOptions: { show: { operation: ['requestAddressForm'] } },
|
|
358
|
+
},
|
|
359
|
+
{ displayName: 'Message Body', name: 'addressFormBody', type: 'string', typeOptions: { rows: 3 }, required: true, default: 'Please provide your delivery address to complete your order.', displayOptions: { show: { operation: ['requestAddressForm'] } } },
|
|
360
|
+
{
|
|
361
|
+
displayName: 'Country',
|
|
362
|
+
name: 'addressCountry',
|
|
363
|
+
type: 'options',
|
|
364
|
+
options: [
|
|
365
|
+
{ name: 'India (IN)', value: 'IN' },
|
|
366
|
+
{ name: 'Singapore (SG)', value: 'SG' },
|
|
367
|
+
],
|
|
368
|
+
default: 'IN',
|
|
369
|
+
required: true,
|
|
370
|
+
displayOptions: { show: { operation: ['requestAddressForm'] } },
|
|
371
|
+
},
|
|
372
|
+
{ displayName: 'Pre-fill Customer Name', name: 'prefillName', type: 'string', default: '', placeholder: '={{$json.customer_name}}', displayOptions: { show: { operation: ['requestAddressForm'] } } },
|
|
373
|
+
{ displayName: 'Pre-fill Phone Number', name: 'prefillPhone', type: 'string', default: '', placeholder: '+917558510593', displayOptions: { show: { operation: ['requestAddressForm'] } } },
|
|
374
|
+
|
|
375
|
+
// READ ADDRESS FORM SUBMISSION
|
|
376
|
+
{ displayName: 'Webhook Body', name: 'addressFormWebhookBody', type: 'json', required: true, default: '={{$json.body}}', description: 'Outputs: name, phone, address, city, state, pincode, floor, landmark, full_address.', displayOptions: { show: { operation: ['parseAddressForm'] } } },
|
|
377
|
+
|
|
378
|
+
// ASK FOR ADDRESS — TEXT
|
|
379
|
+
{ displayName: 'Message Body', name: 'addressTextBody', type: 'string', typeOptions: { rows: 5 }, required: true, default: 'Please type your complete delivery address including House No, Street, City and Pincode.', displayOptions: { show: { operation: ['requestAddressText'] } } },
|
|
380
|
+
|
|
381
|
+
// READ ADDRESS FROM CHAT REPLY
|
|
382
|
+
{ displayName: 'Webhook Body', name: 'addressTextWebhookBody', type: 'json', required: true, default: '={{$json.body}}', displayOptions: { show: { operation: ['parseAddressText'] } } },
|
|
383
|
+
|
|
384
|
+
// ASK FOR LOCATION
|
|
385
|
+
{ displayName: 'Message Body', name: 'locationRequestBody', type: 'string', typeOptions: { rows: 3 }, required: true, default: 'Please share your live location so we can deliver to you.', displayOptions: { show: { operation: ['requestLocation'] } } },
|
|
386
|
+
|
|
387
|
+
// SEND LOCATION PIN
|
|
388
|
+
{ displayName: 'Latitude', name: 'latitude', type: 'number', required: true, default: 0, displayOptions: { show: { operation: ['sendLocation'] } } },
|
|
389
|
+
{ displayName: 'Longitude', name: 'longitude', type: 'number', required: true, default: 0, displayOptions: { show: { operation: ['sendLocation'] } } },
|
|
390
|
+
{ displayName: 'Location Name', name: 'locationName', type: 'string', default: '', displayOptions: { show: { operation: ['sendLocation'] } } },
|
|
391
|
+
{ displayName: 'Address', name: 'locationAddress', type: 'string', default: '', displayOptions: { show: { operation: ['sendLocation'] } } },
|
|
392
|
+
|
|
393
|
+
// READ INCOMING LOCATION
|
|
394
|
+
{ displayName: 'Webhook Body', name: 'locationWebhookBody', type: 'json', required: true, default: '={{$json.body}}', displayOptions: { show: { operation: ['parseLocation'] } } },
|
|
395
|
+
|
|
396
|
+
// DETECT BUTTON / LIST CLICK
|
|
397
|
+
{ displayName: 'Webhook Body', name: 'buttonWebhookBody', type: 'json', required: true, default: '={{$json.body}}', description: 'Outputs: button_id, button_text, customer_phone. Use button_id in Switch node to route.', displayOptions: { show: { operation: ['detectButton'] } } },
|
|
398
|
+
|
|
399
|
+
// READ INCOMING MESSAGE INFO
|
|
400
|
+
{ displayName: 'Webhook Body', name: 'contactWebhookBody', type: 'json', required: true, default: '={{$json.body}}', description: 'Outputs: customer_phone, customer_name, message_type, is_order, is_button_reply, is_address_form, is_location, is_text.', displayOptions: { show: { operation: ['parseContact'] } } },
|
|
401
|
+
],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async execute() {
|
|
406
|
+
const items = this.getInputData();
|
|
407
|
+
const returnData = [];
|
|
408
|
+
const credentials = await this.getCredentials('warecoverApi');
|
|
409
|
+
const accessToken = credentials.accessToken;
|
|
410
|
+
const phoneNumberId = credentials.phoneNumberId;
|
|
411
|
+
const apiKey = credentials.apiKey;
|
|
412
|
+
const baseURL = `https://crm.warecover.com/api/meta/v19.0`;
|
|
413
|
+
|
|
414
|
+
// ── API Key Validation ──────────────────────────────────────────
|
|
415
|
+
// Validate key against Warecover before any API call
|
|
416
|
+
try {
|
|
417
|
+
const keyCheck = await this.helpers.httpRequest({
|
|
418
|
+
method: 'POST',
|
|
419
|
+
url: 'https://webhook.warecover.com/api/n8n/verify-key',
|
|
420
|
+
headers: { 'Content-Type': 'application/json' },
|
|
421
|
+
body: { api_key: apiKey, phone_number_id: phoneNumberId },
|
|
422
|
+
json: true,
|
|
423
|
+
});
|
|
424
|
+
if (!keyCheck || keyCheck.valid === false) {
|
|
425
|
+
throw new Error('Invalid Warecover API Key. Please go to Warecover Dashboard → API Keys and generate a valid key.');
|
|
426
|
+
}
|
|
427
|
+
} catch (keyError) {
|
|
428
|
+
// If endpoint not yet deployed, check key format at minimum
|
|
429
|
+
if (!apiKey || String(apiKey).length < 10) {
|
|
430
|
+
throw new Error('Warecover API Key is missing or invalid. Go to Warecover Dashboard → API Keys → Generate Key.');
|
|
431
|
+
}
|
|
432
|
+
// If server returned explicit invalid, re-throw
|
|
433
|
+
if (keyError.message && keyError.message.includes('Invalid Warecover')) {
|
|
434
|
+
throw keyError;
|
|
435
|
+
}
|
|
436
|
+
// Network/server error — allow through (key format passed)
|
|
437
|
+
}
|
|
438
|
+
// ───────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
const sendWA = async (body) => {
|
|
441
|
+
return await this.helpers.httpRequest({
|
|
442
|
+
method: 'POST',
|
|
443
|
+
url: `${baseURL}/${phoneNumberId}/messages`,
|
|
444
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'X-Warecover-Key': String(apiKey), 'Content-Type': 'application/json' },
|
|
445
|
+
body, json: true,
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const parseBody = (raw) => {
|
|
450
|
+
try { return typeof raw === 'string' ? JSON.parse(raw) : (raw || {}); } catch (_) { return {}; }
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const extractWebhook = (body) => {
|
|
454
|
+
const entry = body.entry?.[0];
|
|
455
|
+
const changes = entry?.changes?.[0];
|
|
456
|
+
const value = changes?.value;
|
|
457
|
+
return {
|
|
458
|
+
value,
|
|
459
|
+
messages: value?.messages?.[0],
|
|
460
|
+
contacts: value?.contacts?.[0],
|
|
461
|
+
metadata: value?.metadata,
|
|
462
|
+
statuses: value?.statuses?.[0],
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
for (let i = 0; i < items.length; i++) {
|
|
467
|
+
try {
|
|
468
|
+
const operation = this.getNodeParameter('operation', i);
|
|
469
|
+
let result = {};
|
|
470
|
+
|
|
471
|
+
if (operation.startsWith('_sep')) {
|
|
472
|
+
returnData.push({ json: { info: 'Select a real operation.' }, pairedItem: { item: i } });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (operation === 'sendText') {
|
|
477
|
+
const to = this.getNodeParameter('to', i);
|
|
478
|
+
const text = this.getNodeParameter('text', i);
|
|
479
|
+
const previewUrl = this.getNodeParameter('previewUrl', i);
|
|
480
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'text', text: { body: text, preview_url: previewUrl } });
|
|
481
|
+
|
|
482
|
+
} else if (operation === 'sendTemplate') {
|
|
483
|
+
const to = this.getNodeParameter('to', i);
|
|
484
|
+
const templateName = this.getNodeParameter('templateName', i);
|
|
485
|
+
const languageCode = this.getNodeParameter('languageCode', i);
|
|
486
|
+
const componentsRaw = this.getNodeParameter('templateComponents', i);
|
|
487
|
+
let components = [];
|
|
488
|
+
try { components = typeof componentsRaw === 'string' ? JSON.parse(componentsRaw) : componentsRaw; } catch (_) { components = []; }
|
|
489
|
+
result = await sendWA({ messaging_product: 'whatsapp', to, type: 'template', template: { name: templateName, language: { code: languageCode }, ...(components.length > 0 ? { components } : {}) } });
|
|
490
|
+
|
|
491
|
+
} else if (operation === 'sendImage') {
|
|
492
|
+
const to = this.getNodeParameter('to', i);
|
|
493
|
+
const link = this.getNodeParameter('imageUrl', i);
|
|
494
|
+
const caption = this.getNodeParameter('imageCaption', i);
|
|
495
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'image', image: { link, ...(caption ? { caption } : {}) } });
|
|
496
|
+
|
|
497
|
+
} else if (operation === 'sendDocument') {
|
|
498
|
+
const to = this.getNodeParameter('to', i);
|
|
499
|
+
const link = this.getNodeParameter('documentUrl', i);
|
|
500
|
+
const filename = this.getNodeParameter('documentFilename', i);
|
|
501
|
+
const caption = this.getNodeParameter('documentCaption', i);
|
|
502
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'document', document: { link, filename, ...(caption ? { caption } : {}) } });
|
|
503
|
+
|
|
504
|
+
} else if (operation === 'sendAudio') {
|
|
505
|
+
const to = this.getNodeParameter('to', i);
|
|
506
|
+
const link = this.getNodeParameter('audioUrl', i);
|
|
507
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'audio', audio: { link } });
|
|
508
|
+
|
|
509
|
+
} else if (operation === 'sendVideo') {
|
|
510
|
+
const to = this.getNodeParameter('to', i);
|
|
511
|
+
const link = this.getNodeParameter('videoUrl', i);
|
|
512
|
+
const caption = this.getNodeParameter('videoCaption', i);
|
|
513
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'video', video: { link, ...(caption ? { caption } : {}) } });
|
|
514
|
+
|
|
515
|
+
} else if (operation === 'sendButtons') {
|
|
516
|
+
const to = this.getNodeParameter('to', i);
|
|
517
|
+
const bodyText = this.getNodeParameter('btnBodyText', i);
|
|
518
|
+
const headerText = this.getNodeParameter('btnHeaderText', i);
|
|
519
|
+
const footerText = this.getNodeParameter('btnFooterText', i);
|
|
520
|
+
const buttonsData = this.getNodeParameter('buttons', i);
|
|
521
|
+
const buttons = (buttonsData.button || []).map((btn) => ({ type: 'reply', reply: { id: btn.id, title: btn.title } }));
|
|
522
|
+
result = await sendWA({
|
|
523
|
+
messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'interactive',
|
|
524
|
+
interactive: {
|
|
525
|
+
type: 'button',
|
|
526
|
+
...(headerText ? { header: { type: 'text', text: headerText } } : {}),
|
|
527
|
+
body: { text: bodyText },
|
|
528
|
+
...(footerText ? { footer: { text: footerText } } : {}),
|
|
529
|
+
action: { buttons },
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
result.buttons_sent = buttons.map(b => b.reply);
|
|
533
|
+
|
|
534
|
+
} else if (operation === 'sendCtaButton') {
|
|
535
|
+
const to = this.getNodeParameter('to', i);
|
|
536
|
+
const headerText = this.getNodeParameter('ctaHeaderText', i);
|
|
537
|
+
const bodyText = this.getNodeParameter('ctaBodyText', i);
|
|
538
|
+
const footerText = this.getNodeParameter('ctaFooterText', i);
|
|
539
|
+
const displayText = this.getNodeParameter('ctaDisplayText', i);
|
|
540
|
+
const url = this.getNodeParameter('ctaUrl', i);
|
|
541
|
+
result = await sendWA({
|
|
542
|
+
messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'interactive',
|
|
543
|
+
interactive: {
|
|
544
|
+
type: 'cta_url',
|
|
545
|
+
...(headerText ? { header: { type: 'text', text: headerText } } : {}),
|
|
546
|
+
body: { text: bodyText },
|
|
547
|
+
...(footerText ? { footer: { text: footerText } } : {}),
|
|
548
|
+
action: { name: 'cta_url', parameters: { display_text: displayText, url } },
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
} else if (operation === 'sendList') {
|
|
553
|
+
const to = this.getNodeParameter('to', i);
|
|
554
|
+
const bodyText = this.getNodeParameter('listBodyText', i);
|
|
555
|
+
const buttonLabel = this.getNodeParameter('listButtonLabel', i);
|
|
556
|
+
const headerText = this.getNodeParameter('listHeaderText', i);
|
|
557
|
+
const footerText = this.getNodeParameter('listFooterText', i);
|
|
558
|
+
const sectionsRaw = this.getNodeParameter('listSections', i);
|
|
559
|
+
let sections = [];
|
|
560
|
+
try { sections = typeof sectionsRaw === 'string' ? JSON.parse(sectionsRaw) : sectionsRaw; } catch (_) { sections = []; }
|
|
561
|
+
result = await sendWA({
|
|
562
|
+
messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'interactive',
|
|
563
|
+
interactive: {
|
|
564
|
+
type: 'list',
|
|
565
|
+
...(headerText ? { header: { type: 'text', text: headerText } } : {}),
|
|
566
|
+
body: { text: bodyText },
|
|
567
|
+
...(footerText ? { footer: { text: footerText } } : {}),
|
|
568
|
+
action: { button: buttonLabel, sections },
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
} else if (operation === 'markRead') {
|
|
573
|
+
const messageId = this.getNodeParameter('messageId', i);
|
|
574
|
+
result = await sendWA({ messaging_product: 'whatsapp', status: 'read', message_id: messageId });
|
|
575
|
+
|
|
576
|
+
} else if (operation === 'parseOrder') {
|
|
577
|
+
const body = parseBody(this.getNodeParameter('webhookBody', i));
|
|
578
|
+
const { messages, contacts, metadata } = extractWebhook(body);
|
|
579
|
+
if (!messages || messages.type !== 'order') {
|
|
580
|
+
result = { is_order: false, message_type: messages?.type || 'unknown' };
|
|
581
|
+
} else {
|
|
582
|
+
const order = messages.order;
|
|
583
|
+
const productItems = order.product_items || [];
|
|
584
|
+
let totalAmount = 0; let totalItems = 0;
|
|
585
|
+
const products = productItems.map((item) => {
|
|
586
|
+
const qty = Number(item.quantity) || 0;
|
|
587
|
+
const price = Number(item.item_price) || 0;
|
|
588
|
+
totalAmount += qty * price; totalItems += qty;
|
|
589
|
+
return { product_id: item.product_retailer_id, quantity: qty, price, currency: item.currency || 'INR', subtotal: qty * price };
|
|
590
|
+
});
|
|
591
|
+
result = {
|
|
592
|
+
is_order: true,
|
|
593
|
+
customer_phone: contacts?.wa_id || messages.from,
|
|
594
|
+
customer_name: contacts?.profile?.name || '',
|
|
595
|
+
message_id: messages.id,
|
|
596
|
+
timestamp: messages.timestamp,
|
|
597
|
+
catalog_id: order.catalog_id,
|
|
598
|
+
order_text: order.text || '',
|
|
599
|
+
products,
|
|
600
|
+
total_items: totalItems,
|
|
601
|
+
total_amount: totalAmount,
|
|
602
|
+
currency: productItems?.[0]?.currency || 'INR',
|
|
603
|
+
phone_number_id: metadata?.phone_number_id || '',
|
|
604
|
+
display_phone_number: metadata?.display_phone_number || '',
|
|
605
|
+
order_summary: `Order Summary\n--------------------------\n${products.map((p, i) => `${i+1}. Item: ${p.product_id}\n Qty: ${p.quantity} Price: Rs. ${p.price} Subtotal: Rs. ${p.subtotal}`).join('\n\n')}\n\n--------------------------\nTotal Items : ${totalItems}\nTotal Amount: Rs. ${totalAmount}`,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
} else if (operation === 'sendOrderConfirmation') {
|
|
610
|
+
const to = this.getNodeParameter('to', i);
|
|
611
|
+
const confirmText = this.getNodeParameter('orderConfirmText', i);
|
|
612
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'text', text: { body: confirmText } });
|
|
613
|
+
|
|
614
|
+
} else if (operation === 'requestAddressForm') {
|
|
615
|
+
const to = this.getNodeParameter('to', i);
|
|
616
|
+
const bodyText = this.getNodeParameter('addressFormBody', i);
|
|
617
|
+
const country = this.getNodeParameter('addressCountry', i);
|
|
618
|
+
const prefillName = this.getNodeParameter('prefillName', i);
|
|
619
|
+
const prefillPhone = this.getNodeParameter('prefillPhone', i);
|
|
620
|
+
const params = { country };
|
|
621
|
+
if (prefillName || prefillPhone) {
|
|
622
|
+
params.values = {};
|
|
623
|
+
if (prefillName) params.values.name = prefillName;
|
|
624
|
+
if (prefillPhone) params.values.phone_number = prefillPhone;
|
|
625
|
+
}
|
|
626
|
+
result = await sendWA({
|
|
627
|
+
messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'interactive',
|
|
628
|
+
interactive: { type: 'address_message', body: { text: bodyText }, action: { name: 'address_message', parameters: JSON.stringify(params) } },
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
} else if (operation === 'parseAddressForm') {
|
|
632
|
+
const body = parseBody(this.getNodeParameter('addressFormWebhookBody', i));
|
|
633
|
+
const { messages, contacts } = extractWebhook(body);
|
|
634
|
+
if (messages?.type === 'interactive' && messages?.interactive?.type === 'nfm_reply' && messages?.interactive?.nfm_reply?.name === 'address_message') {
|
|
635
|
+
const nfmReply = messages.interactive.nfm_reply;
|
|
636
|
+
let addressValues = {};
|
|
637
|
+
try { const parsed = JSON.parse(nfmReply.response_json || '{}'); addressValues = parsed.values || parsed; } catch (_) { addressValues = {}; }
|
|
638
|
+
result = {
|
|
639
|
+
is_address_form: true,
|
|
640
|
+
customer_phone: messages.from,
|
|
641
|
+
customer_name: addressValues.name || contacts?.profile?.name || '',
|
|
642
|
+
message_id: messages.id,
|
|
643
|
+
timestamp: messages.timestamp,
|
|
644
|
+
name: addressValues.name || '',
|
|
645
|
+
phone: addressValues.phone_number || '',
|
|
646
|
+
address: addressValues.address || '',
|
|
647
|
+
city: addressValues.city || '',
|
|
648
|
+
state: addressValues.state || '',
|
|
649
|
+
pincode: addressValues.in_pin_code || '',
|
|
650
|
+
floor: addressValues.floor_number || '',
|
|
651
|
+
landmark: addressValues.landmark_area || '',
|
|
652
|
+
building: addressValues.building_name || '',
|
|
653
|
+
sg_post_code: addressValues.sg_post_code || '',
|
|
654
|
+
full_address: nfmReply.body || [addressValues.name, addressValues.phone_number, addressValues.address, addressValues.floor_number, addressValues.landmark_area, addressValues.city, addressValues.state, addressValues.in_pin_code].filter(Boolean).join(', '),
|
|
655
|
+
raw_values: addressValues,
|
|
656
|
+
};
|
|
657
|
+
} else {
|
|
658
|
+
result = { is_address_form: false, message_type: messages?.type || 'unknown', customer_phone: messages?.from || '' };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
} else if (operation === 'requestAddressText') {
|
|
662
|
+
const to = this.getNodeParameter('to', i);
|
|
663
|
+
const bodyText = this.getNodeParameter('addressTextBody', i);
|
|
664
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'text', text: { body: bodyText } });
|
|
665
|
+
|
|
666
|
+
} else if (operation === 'parseAddressText') {
|
|
667
|
+
const body = parseBody(this.getNodeParameter('addressTextWebhookBody', i));
|
|
668
|
+
const { messages, contacts } = extractWebhook(body);
|
|
669
|
+
if (messages?.type === 'text') {
|
|
670
|
+
const textBody = messages.text?.body || '';
|
|
671
|
+
result = { is_address: true, address_text: textBody, address_lines: textBody.split('\n').map(l => l.trim()).filter(Boolean), customer_phone: messages.from, customer_name: contacts?.profile?.name || '', message_id: messages.id, timestamp: messages.timestamp };
|
|
672
|
+
} else {
|
|
673
|
+
result = { is_address: false, message_type: messages?.type || 'unknown', customer_phone: messages?.from || '' };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
} else if (operation === 'requestLocation') {
|
|
677
|
+
const to = this.getNodeParameter('to', i);
|
|
678
|
+
const bodyText = this.getNodeParameter('locationRequestBody', i);
|
|
679
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'interactive', interactive: { type: 'location_request_message', body: { text: bodyText }, action: { name: 'send_location' } } });
|
|
680
|
+
|
|
681
|
+
} else if (operation === 'sendLocation') {
|
|
682
|
+
const to = this.getNodeParameter('to', i);
|
|
683
|
+
const latitude = this.getNodeParameter('latitude', i);
|
|
684
|
+
const longitude = this.getNodeParameter('longitude', i);
|
|
685
|
+
const name = this.getNodeParameter('locationName', i);
|
|
686
|
+
const address = this.getNodeParameter('locationAddress', i);
|
|
687
|
+
result = await sendWA({ messaging_product: 'whatsapp', recipient_type: 'individual', to, type: 'location', location: { latitude, longitude, name, address } });
|
|
688
|
+
|
|
689
|
+
} else if (operation === 'parseLocation') {
|
|
690
|
+
const body = parseBody(this.getNodeParameter('locationWebhookBody', i));
|
|
691
|
+
const { messages, contacts } = extractWebhook(body);
|
|
692
|
+
if (messages?.type === 'location') {
|
|
693
|
+
const loc = messages.location;
|
|
694
|
+
result = { is_location: true, customer_phone: messages.from, customer_name: contacts?.profile?.name || '', message_id: messages.id, timestamp: messages.timestamp, latitude: loc.latitude, longitude: loc.longitude, name: loc.name || '', address: loc.address || '', google_maps_url: `https://www.google.com/maps?q=${loc.latitude},${loc.longitude}` };
|
|
695
|
+
} else {
|
|
696
|
+
result = { is_location: false, message_type: messages?.type || 'unknown', customer_phone: messages?.from || '' };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
} else if (operation === 'detectButton') {
|
|
700
|
+
const body = parseBody(this.getNodeParameter('buttonWebhookBody', i));
|
|
701
|
+
const { messages, contacts } = extractWebhook(body);
|
|
702
|
+
if (messages?.type === 'interactive') {
|
|
703
|
+
const interactive = messages.interactive;
|
|
704
|
+
const btnReply = interactive.button_reply;
|
|
705
|
+
const listReply = interactive.list_reply;
|
|
706
|
+
result = { is_button_click: true, interaction_type: interactive.type, button_id: btnReply?.id || listReply?.id || '', button_text: btnReply?.title || listReply?.title || '', list_description: listReply?.description || '', customer_phone: messages.from, customer_name: contacts?.profile?.name || '', message_id: messages.id, timestamp: messages.timestamp };
|
|
707
|
+
} else if (messages?.type === 'text') {
|
|
708
|
+
result = { is_button_click: false, is_text_reply: true, button_id: '', text: messages.text?.body || '', customer_phone: messages.from, customer_name: contacts?.profile?.name || '', message_id: messages.id, timestamp: messages.timestamp };
|
|
709
|
+
} else {
|
|
710
|
+
result = { is_button_click: false, is_text_reply: false, button_id: '', message_type: messages?.type || 'unknown', customer_phone: messages?.from || '' };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
} else if (operation === 'closeChatDone') {
|
|
714
|
+
const phone = this.getNodeParameter('closeChatPhone', i);
|
|
715
|
+
// Warecover API: mark chat as done
|
|
716
|
+
result = await this.helpers.httpRequest({
|
|
717
|
+
method: 'POST',
|
|
718
|
+
url: `https://crm.warecover.com/api/chat/done`,
|
|
719
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'X-Warecover-Key': String(apiKey), 'Content-Type': 'application/json' },
|
|
720
|
+
body: { phone_number_id: phoneNumberId, customer_phone: phone },
|
|
721
|
+
json: true,
|
|
722
|
+
});
|
|
723
|
+
result.chat_closed = true;
|
|
724
|
+
result.customer_phone = phone;
|
|
725
|
+
|
|
726
|
+
} else if (operation === 'parseContact') {
|
|
727
|
+
const body = parseBody(this.getNodeParameter('contactWebhookBody', i));
|
|
728
|
+
const { messages, contacts, metadata, statuses } = extractWebhook(body);
|
|
729
|
+
const isAddressForm = messages?.type === 'interactive' && messages?.interactive?.type === 'nfm_reply' && messages?.interactive?.nfm_reply?.name === 'address_message';
|
|
730
|
+
result = {
|
|
731
|
+
customer_phone: messages?.from || statuses?.recipient_id || '',
|
|
732
|
+
customer_name: contacts?.profile?.name || '',
|
|
733
|
+
customer_wa_id: contacts?.wa_id || '',
|
|
734
|
+
message_id: messages?.id || '',
|
|
735
|
+
message_type: messages?.type || (statuses ? 'status_update' : 'unknown'),
|
|
736
|
+
timestamp: messages?.timestamp || statuses?.timestamp || '',
|
|
737
|
+
text_body: messages?.text?.body || '',
|
|
738
|
+
phone_number_id: metadata?.phone_number_id || '',
|
|
739
|
+
display_phone_number: metadata?.display_phone_number || '',
|
|
740
|
+
is_status_update: !!statuses,
|
|
741
|
+
status: statuses?.status || '',
|
|
742
|
+
is_order: messages?.type === 'order',
|
|
743
|
+
is_button_reply: messages?.type === 'interactive' && !isAddressForm,
|
|
744
|
+
is_address_form: isAddressForm,
|
|
745
|
+
is_location: messages?.type === 'location',
|
|
746
|
+
is_text: messages?.type === 'text',
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
returnData.push({ json: result, pairedItem: { item: i } });
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (this.continueOnFail()) {
|
|
753
|
+
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return [returnData];
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
exports.Warecover = Warecover;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
|
2
|
+
<rect width="60" height="60" rx="12" fill="#25D366"/>
|
|
3
|
+
<circle cx="30" cy="28" r="14" fill="#ffffff"/>
|
|
4
|
+
<circle cx="30" cy="28" r="11" fill="#25D366"/>
|
|
5
|
+
<rect x="18" y="38" width="8" height="8" rx="4" fill="#25D366"/>
|
|
6
|
+
<rect x="22" y="22" width="3" height="12" rx="1.5" fill="#ffffff"/>
|
|
7
|
+
<rect x="28" y="22" width="3" height="12" rx="1.5" fill="#ffffff"/>
|
|
8
|
+
<rect x="34" y="22" width="3" height="12" rx="1.5" fill="#ffffff"/>
|
|
9
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-warecover-v1",
|
|
3
|
+
"version": "2.8.0",
|
|
4
|
+
"description": "WhatsApp automation for n8n \u2014 send messages, buttons, CTA links, orders, address forms, location via Warecover",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"warecover",
|
|
8
|
+
"whatsapp",
|
|
9
|
+
"meta",
|
|
10
|
+
"whatsapp-api",
|
|
11
|
+
"whatsapp-business",
|
|
12
|
+
"whatsapp-automation",
|
|
13
|
+
"whatsapp-bot",
|
|
14
|
+
"whatsapp-messaging",
|
|
15
|
+
"whatsapp-crm",
|
|
16
|
+
"messaging",
|
|
17
|
+
"sms",
|
|
18
|
+
"notification",
|
|
19
|
+
"automation",
|
|
20
|
+
"ai-agent",
|
|
21
|
+
"chatbot",
|
|
22
|
+
"crm",
|
|
23
|
+
"customer-support",
|
|
24
|
+
"business-messaging",
|
|
25
|
+
"meta-api",
|
|
26
|
+
"cloud-api",
|
|
27
|
+
"send-message",
|
|
28
|
+
"template-message",
|
|
29
|
+
"webhook",
|
|
30
|
+
"workflow",
|
|
31
|
+
"n8n-node",
|
|
32
|
+
"n8n-integration",
|
|
33
|
+
"india",
|
|
34
|
+
"saas"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"author": {
|
|
38
|
+
"name": "Warecover"
|
|
39
|
+
},
|
|
40
|
+
"main": "index.js",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"dev": "tsc --watch"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"n8n": {
|
|
49
|
+
"n8nNodesApiVersion": 1,
|
|
50
|
+
"credentials": [
|
|
51
|
+
"dist/credentials/WarecoverApi.credentials.js"
|
|
52
|
+
],
|
|
53
|
+
"nodes": [
|
|
54
|
+
"dist/nodes/Warecover/Warecover.node.js"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"typescript": "~5.1.0",
|
|
59
|
+
"n8n-workflow": "*"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"n8n-workflow": "*"
|
|
63
|
+
}
|
|
64
|
+
}
|