n8n-nodes-posthawk 0.1.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,45 @@
1
+ # n8n-nodes-posthawk
2
+
3
+ This is an [n8n](https://n8n.io/) community node for [Posthawk](https://posthawk.dev) — email infrastructure for developers.
4
+
5
+ ## Features
6
+
7
+ ### Posthawk Node (Actions)
8
+
9
+ | Resource | Operations |
10
+ |----------|-----------|
11
+ | **Email** | Send, Send Batch, Get Status |
12
+ | **Scheduled Email** | List, Get, Cancel, Reschedule |
13
+ | **Contact** | List, Get, Create, Update, Delete |
14
+ | **Suppression** | List, Add, Remove |
15
+ | **Webhook** | List, Create, Update, Delete, Test |
16
+
17
+ ### Posthawk Trigger (Webhook)
18
+
19
+ Automatically triggers your n8n workflow when email events occur:
20
+
21
+ - **Send** — Email accepted by SES
22
+ - **Delivery** — Email delivered to recipient
23
+ - **Bounce** — Email bounced
24
+ - **Complaint** — Recipient marked as spam
25
+ - **Open** — Email opened
26
+ - **Click** — Link clicked
27
+ - **Reject** — Email rejected by SES
28
+ - **Delivery Delay** — Delivery delayed
29
+
30
+ The trigger node automatically creates and manages webhook endpoints in your Posthawk account.
31
+
32
+ ## Setup
33
+
34
+ 1. Install this node in your n8n instance
35
+ 2. Add your Posthawk API key in the credentials
36
+ 3. If self-hosting Posthawk, update the Base URL in credentials
37
+
38
+ ## Credentials
39
+
40
+ - **API Key**: Your Posthawk API key (find it in Dashboard → API Keys)
41
+ - **Base URL**: `https://api.posthawk.dev` (or your self-hosted URL)
42
+
43
+ ## License
44
+
45
+ MIT
@@ -0,0 +1,9 @@
1
+ import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class PosthawkApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PosthawkApi = void 0;
4
+ class PosthawkApi {
5
+ constructor() {
6
+ this.name = 'posthawkApi';
7
+ this.displayName = 'Posthawk API';
8
+ this.documentationUrl = 'https://docs.posthawk.dev/api';
9
+ this.properties = [
10
+ {
11
+ displayName: 'API Key',
12
+ name: 'apiKey',
13
+ type: 'string',
14
+ typeOptions: { password: true },
15
+ default: '',
16
+ required: true,
17
+ description: 'Your Posthawk API key. Find it in Dashboard → API Keys.',
18
+ },
19
+ {
20
+ displayName: 'Base URL',
21
+ name: 'baseUrl',
22
+ type: 'string',
23
+ default: 'https://api.posthawk.dev',
24
+ description: 'API base URL. Change this if you are self-hosting Posthawk.',
25
+ },
26
+ ];
27
+ this.authenticate = {
28
+ type: 'generic',
29
+ properties: {
30
+ headers: {
31
+ Authorization: '=Bearer {{$credentials.apiKey}}',
32
+ },
33
+ },
34
+ };
35
+ this.test = {
36
+ request: {
37
+ baseURL: '={{$credentials.baseUrl}}',
38
+ url: '/v1/emails/queue/stats',
39
+ method: 'GET',
40
+ },
41
+ };
42
+ }
43
+ }
44
+ exports.PosthawkApi = PosthawkApi;
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class Posthawk implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,664 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Posthawk = void 0;
4
+ class Posthawk {
5
+ constructor() {
6
+ this.description = {
7
+ displayName: 'Posthawk',
8
+ name: 'posthawk',
9
+ icon: 'file:posthawk.svg',
10
+ group: ['transform'],
11
+ version: 1,
12
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
13
+ description: 'Send, schedule, and manage emails with Posthawk',
14
+ defaults: {
15
+ name: 'Posthawk',
16
+ },
17
+ inputs: ['main'],
18
+ outputs: ['main'],
19
+ credentials: [
20
+ {
21
+ name: 'posthawkApi',
22
+ required: true,
23
+ },
24
+ ],
25
+ properties: [
26
+ // ─── Resource ───
27
+ {
28
+ displayName: 'Resource',
29
+ name: 'resource',
30
+ type: 'options',
31
+ noDataExpression: true,
32
+ options: [
33
+ { name: 'Email', value: 'email' },
34
+ { name: 'Scheduled Email', value: 'scheduled' },
35
+ { name: 'Contact', value: 'contact' },
36
+ { name: 'Suppression', value: 'suppression' },
37
+ { name: 'Webhook', value: 'webhook' },
38
+ ],
39
+ default: 'email',
40
+ },
41
+ // ─── Email Operations ───
42
+ {
43
+ displayName: 'Operation',
44
+ name: 'operation',
45
+ type: 'options',
46
+ noDataExpression: true,
47
+ displayOptions: { show: { resource: ['email'] } },
48
+ options: [
49
+ { name: 'Send', value: 'send', action: 'Send an email', description: 'Send a transactional email' },
50
+ { name: 'Send Batch', value: 'batch', action: 'Send batch emails', description: 'Send multiple emails in one request' },
51
+ { name: 'Get', value: 'get', action: 'Get email status', description: 'Get the status of a sent email by job ID' },
52
+ ],
53
+ default: 'send',
54
+ },
55
+ // ─── Scheduled Operations ───
56
+ {
57
+ displayName: 'Operation',
58
+ name: 'operation',
59
+ type: 'options',
60
+ noDataExpression: true,
61
+ displayOptions: { show: { resource: ['scheduled'] } },
62
+ options: [
63
+ { name: 'List', value: 'list', action: 'List scheduled emails' },
64
+ { name: 'Get', value: 'get', action: 'Get a scheduled email' },
65
+ { name: 'Cancel', value: 'cancel', action: 'Cancel a scheduled email' },
66
+ { name: 'Reschedule', value: 'reschedule', action: 'Reschedule an email' },
67
+ ],
68
+ default: 'list',
69
+ },
70
+ // ─── Contact Operations ───
71
+ {
72
+ displayName: 'Operation',
73
+ name: 'operation',
74
+ type: 'options',
75
+ noDataExpression: true,
76
+ displayOptions: { show: { resource: ['contact'] } },
77
+ options: [
78
+ { name: 'List', value: 'list', action: 'List contacts' },
79
+ { name: 'Get', value: 'get', action: 'Get a contact' },
80
+ { name: 'Create', value: 'create', action: 'Create a contact' },
81
+ { name: 'Update', value: 'update', action: 'Update a contact' },
82
+ { name: 'Delete', value: 'delete', action: 'Delete a contact' },
83
+ ],
84
+ default: 'list',
85
+ },
86
+ // ─── Suppression Operations ───
87
+ {
88
+ displayName: 'Operation',
89
+ name: 'operation',
90
+ type: 'options',
91
+ noDataExpression: true,
92
+ displayOptions: { show: { resource: ['suppression'] } },
93
+ options: [
94
+ { name: 'List', value: 'list', action: 'List suppressions' },
95
+ { name: 'Add', value: 'add', action: 'Add a suppression' },
96
+ { name: 'Remove', value: 'remove', action: 'Remove a suppression' },
97
+ ],
98
+ default: 'list',
99
+ },
100
+ // ─── Webhook Operations ───
101
+ {
102
+ displayName: 'Operation',
103
+ name: 'operation',
104
+ type: 'options',
105
+ noDataExpression: true,
106
+ displayOptions: { show: { resource: ['webhook'] } },
107
+ options: [
108
+ { name: 'List', value: 'list', action: 'List webhooks' },
109
+ { name: 'Create', value: 'create', action: 'Create a webhook' },
110
+ { name: 'Update', value: 'update', action: 'Update a webhook' },
111
+ { name: 'Delete', value: 'delete', action: 'Delete a webhook' },
112
+ { name: 'Test', value: 'test', action: 'Send a test webhook' },
113
+ ],
114
+ default: 'list',
115
+ },
116
+ // ════════════════════════════════════════════════
117
+ // EMAIL FIELDS
118
+ // ════════════════════════════════════════════════
119
+ {
120
+ displayName: 'From',
121
+ name: 'from',
122
+ type: 'string',
123
+ default: '',
124
+ required: true,
125
+ placeholder: 'hello@yourdomain.com',
126
+ description: 'Sender email address (must be from a verified domain)',
127
+ displayOptions: { show: { resource: ['email'], operation: ['send'] } },
128
+ },
129
+ {
130
+ displayName: 'To',
131
+ name: 'to',
132
+ type: 'string',
133
+ default: '',
134
+ required: true,
135
+ placeholder: 'user@example.com',
136
+ description: 'Recipient email address. For multiple recipients, separate with commas.',
137
+ displayOptions: { show: { resource: ['email'], operation: ['send'] } },
138
+ },
139
+ {
140
+ displayName: 'Subject',
141
+ name: 'subject',
142
+ type: 'string',
143
+ default: '',
144
+ required: true,
145
+ description: 'Email subject line',
146
+ displayOptions: { show: { resource: ['email'], operation: ['send'] } },
147
+ },
148
+ {
149
+ displayName: 'HTML Body',
150
+ name: 'html',
151
+ type: 'string',
152
+ typeOptions: { rows: 6 },
153
+ default: '',
154
+ description: 'HTML content of the email',
155
+ displayOptions: { show: { resource: ['email'], operation: ['send'] } },
156
+ },
157
+ {
158
+ displayName: 'Text Body',
159
+ name: 'text',
160
+ type: 'string',
161
+ typeOptions: { rows: 4 },
162
+ default: '',
163
+ description: 'Plain text fallback',
164
+ displayOptions: { show: { resource: ['email'], operation: ['send'] } },
165
+ },
166
+ {
167
+ displayName: 'Additional Fields',
168
+ name: 'additionalFields',
169
+ type: 'collection',
170
+ placeholder: 'Add Field',
171
+ default: {},
172
+ displayOptions: { show: { resource: ['email'], operation: ['send'] } },
173
+ options: [
174
+ {
175
+ displayName: 'CC',
176
+ name: 'cc',
177
+ type: 'string',
178
+ default: '',
179
+ description: 'CC recipients (comma-separated)',
180
+ },
181
+ {
182
+ displayName: 'BCC',
183
+ name: 'bcc',
184
+ type: 'string',
185
+ default: '',
186
+ description: 'BCC recipients (comma-separated)',
187
+ },
188
+ {
189
+ displayName: 'Reply To',
190
+ name: 'replyTo',
191
+ type: 'string',
192
+ default: '',
193
+ },
194
+ {
195
+ displayName: 'Template ID',
196
+ name: 'templateId',
197
+ type: 'string',
198
+ default: '',
199
+ description: 'Use an email template by ID instead of HTML/text body',
200
+ },
201
+ {
202
+ displayName: 'Template Variables (JSON)',
203
+ name: 'variables',
204
+ type: 'json',
205
+ default: '{}',
206
+ description: 'Variables to substitute in the template',
207
+ },
208
+ {
209
+ displayName: 'Schedule For',
210
+ name: 'scheduledFor',
211
+ type: 'dateTime',
212
+ default: '',
213
+ description: 'Schedule the email for future delivery (ISO 8601)',
214
+ },
215
+ {
216
+ displayName: 'Timezone',
217
+ name: 'timezone',
218
+ type: 'string',
219
+ default: 'UTC',
220
+ description: 'Timezone for scheduled delivery (e.g. America/New_York)',
221
+ },
222
+ {
223
+ displayName: 'Tags (JSON)',
224
+ name: 'tags',
225
+ type: 'json',
226
+ default: '{}',
227
+ description: 'Key-value tags for categorization',
228
+ },
229
+ {
230
+ displayName: 'Metadata (JSON)',
231
+ name: 'metadata',
232
+ type: 'json',
233
+ default: '{}',
234
+ description: 'Custom metadata attached to the email',
235
+ },
236
+ ],
237
+ },
238
+ // Email Get
239
+ {
240
+ displayName: 'Job ID',
241
+ name: 'jobId',
242
+ type: 'string',
243
+ default: '',
244
+ required: true,
245
+ description: 'The job ID returned when the email was sent',
246
+ displayOptions: { show: { resource: ['email'], operation: ['get'] } },
247
+ },
248
+ // Email Batch
249
+ {
250
+ displayName: 'Emails (JSON)',
251
+ name: 'emails',
252
+ type: 'json',
253
+ default: '[]',
254
+ required: true,
255
+ description: 'Array of email objects to send in batch',
256
+ displayOptions: { show: { resource: ['email'], operation: ['batch'] } },
257
+ },
258
+ // ════════════════════════════════════════════════
259
+ // SCHEDULED FIELDS
260
+ // ════════════════════════════════════════════════
261
+ {
262
+ displayName: 'Scheduled Email ID',
263
+ name: 'scheduledId',
264
+ type: 'string',
265
+ default: '',
266
+ required: true,
267
+ displayOptions: { show: { resource: ['scheduled'], operation: ['get', 'cancel', 'reschedule'] } },
268
+ },
269
+ {
270
+ displayName: 'New Scheduled Time',
271
+ name: 'scheduledFor',
272
+ type: 'dateTime',
273
+ default: '',
274
+ required: true,
275
+ description: 'New delivery time (ISO 8601)',
276
+ displayOptions: { show: { resource: ['scheduled'], operation: ['reschedule'] } },
277
+ },
278
+ // ════════════════════════════════════════════════
279
+ // CONTACT FIELDS
280
+ // ════════════════════════════════════════════════
281
+ {
282
+ displayName: 'Contact ID',
283
+ name: 'contactId',
284
+ type: 'string',
285
+ default: '',
286
+ required: true,
287
+ displayOptions: { show: { resource: ['contact'], operation: ['get', 'update', 'delete'] } },
288
+ },
289
+ {
290
+ displayName: 'Email',
291
+ name: 'email',
292
+ type: 'string',
293
+ default: '',
294
+ required: true,
295
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
296
+ },
297
+ {
298
+ displayName: 'Additional Fields',
299
+ name: 'contactFields',
300
+ type: 'collection',
301
+ placeholder: 'Add Field',
302
+ default: {},
303
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
304
+ options: [
305
+ { displayName: 'Name', name: 'name', type: 'string', default: '' },
306
+ {
307
+ displayName: 'Tags',
308
+ name: 'tags',
309
+ type: 'string',
310
+ default: '',
311
+ description: 'Comma-separated tags',
312
+ },
313
+ {
314
+ displayName: 'Metadata (JSON)',
315
+ name: 'metadata',
316
+ type: 'json',
317
+ default: '{}',
318
+ },
319
+ ],
320
+ },
321
+ // ════════════════════════════════════════════════
322
+ // SUPPRESSION FIELDS
323
+ // ════════════════════════════════════════════════
324
+ {
325
+ displayName: 'Email',
326
+ name: 'email',
327
+ type: 'string',
328
+ default: '',
329
+ required: true,
330
+ displayOptions: { show: { resource: ['suppression'], operation: ['add'] } },
331
+ },
332
+ {
333
+ displayName: 'Reason',
334
+ name: 'reason',
335
+ type: 'options',
336
+ options: [
337
+ { name: 'Bounce', value: 'bounce' },
338
+ { name: 'Complaint', value: 'complaint' },
339
+ { name: 'Manual', value: 'manual' },
340
+ ],
341
+ default: 'manual',
342
+ displayOptions: { show: { resource: ['suppression'], operation: ['add'] } },
343
+ },
344
+ {
345
+ displayName: 'Suppression ID',
346
+ name: 'suppressionId',
347
+ type: 'string',
348
+ default: '',
349
+ required: true,
350
+ displayOptions: { show: { resource: ['suppression'], operation: ['remove'] } },
351
+ },
352
+ // ════════════════════════════════════════════════
353
+ // WEBHOOK FIELDS
354
+ // ════════════════════════════════════════════════
355
+ {
356
+ displayName: 'Webhook ID',
357
+ name: 'webhookId',
358
+ type: 'string',
359
+ default: '',
360
+ required: true,
361
+ displayOptions: { show: { resource: ['webhook'], operation: ['update', 'delete', 'test'] } },
362
+ },
363
+ {
364
+ displayName: 'URL',
365
+ name: 'url',
366
+ type: 'string',
367
+ default: '',
368
+ required: true,
369
+ placeholder: 'https://example.com/webhook',
370
+ displayOptions: { show: { resource: ['webhook'], operation: ['create'] } },
371
+ },
372
+ {
373
+ displayName: 'Events',
374
+ name: 'events',
375
+ type: 'multiOptions',
376
+ options: [
377
+ { name: 'Send', value: 'send' },
378
+ { name: 'Delivery', value: 'delivery' },
379
+ { name: 'Bounce', value: 'bounce' },
380
+ { name: 'Complaint', value: 'complaint' },
381
+ { name: 'Open', value: 'open' },
382
+ { name: 'Click', value: 'click' },
383
+ { name: 'Reject', value: 'reject' },
384
+ { name: 'Delivery Delay', value: 'delivery_delay' },
385
+ ],
386
+ default: ['delivery', 'bounce', 'complaint'],
387
+ displayOptions: { show: { resource: ['webhook'], operation: ['create', 'update'] } },
388
+ },
389
+ {
390
+ displayName: 'Webhook URL (for Update)',
391
+ name: 'url',
392
+ type: 'string',
393
+ default: '',
394
+ displayOptions: { show: { resource: ['webhook'], operation: ['update'] } },
395
+ },
396
+ ],
397
+ };
398
+ }
399
+ async execute() {
400
+ const items = this.getInputData();
401
+ const returnData = [];
402
+ const resource = this.getNodeParameter('resource', 0);
403
+ const operation = this.getNodeParameter('operation', 0);
404
+ const credentials = await this.getCredentials('posthawkApi');
405
+ const baseUrl = credentials.baseUrl;
406
+ for (let i = 0; i < items.length; i++) {
407
+ try {
408
+ let responseData;
409
+ // ─── Email ───
410
+ if (resource === 'email') {
411
+ if (operation === 'send') {
412
+ const to = this.getNodeParameter('to', i).split(',').map(s => s.trim());
413
+ const additional = this.getNodeParameter('additionalFields', i, {});
414
+ const body = {
415
+ from: this.getNodeParameter('from', i),
416
+ to: to.length === 1 ? to[0] : to,
417
+ subject: this.getNodeParameter('subject', i),
418
+ };
419
+ const html = this.getNodeParameter('html', i, '');
420
+ const text = this.getNodeParameter('text', i, '');
421
+ if (html)
422
+ body.html = html;
423
+ if (text)
424
+ body.text = text;
425
+ if (additional.cc)
426
+ body.cc = additional.cc.split(',').map(s => s.trim());
427
+ if (additional.bcc)
428
+ body.bcc = additional.bcc.split(',').map(s => s.trim());
429
+ if (additional.replyTo)
430
+ body.replyTo = additional.replyTo;
431
+ if (additional.templateId)
432
+ body.templateId = additional.templateId;
433
+ if (additional.variables)
434
+ body.variables = JSON.parse(additional.variables);
435
+ if (additional.scheduledFor)
436
+ body.scheduledFor = additional.scheduledFor;
437
+ if (additional.timezone)
438
+ body.timezone = additional.timezone;
439
+ if (additional.tags)
440
+ body.tags = JSON.parse(additional.tags);
441
+ if (additional.metadata)
442
+ body.metadata = JSON.parse(additional.metadata);
443
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
444
+ method: 'POST',
445
+ url: `${baseUrl}/v1/emails/send`,
446
+ body,
447
+ json: true,
448
+ });
449
+ }
450
+ else if (operation === 'batch') {
451
+ const emails = JSON.parse(this.getNodeParameter('emails', i));
452
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
453
+ method: 'POST',
454
+ url: `${baseUrl}/v1/emails/batch`,
455
+ body: { emails },
456
+ json: true,
457
+ });
458
+ }
459
+ else if (operation === 'get') {
460
+ const jobId = this.getNodeParameter('jobId', i);
461
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
462
+ method: 'GET',
463
+ url: `${baseUrl}/v1/emails/send/${jobId}`,
464
+ json: true,
465
+ });
466
+ }
467
+ }
468
+ // ─── Scheduled ───
469
+ else if (resource === 'scheduled') {
470
+ if (operation === 'list') {
471
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
472
+ method: 'GET',
473
+ url: `${baseUrl}/v1/scheduled`,
474
+ json: true,
475
+ });
476
+ }
477
+ else if (operation === 'get') {
478
+ const id = this.getNodeParameter('scheduledId', i);
479
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
480
+ method: 'GET',
481
+ url: `${baseUrl}/v1/scheduled/${id}`,
482
+ json: true,
483
+ });
484
+ }
485
+ else if (operation === 'cancel') {
486
+ const id = this.getNodeParameter('scheduledId', i);
487
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
488
+ method: 'DELETE',
489
+ url: `${baseUrl}/v1/scheduled/${id}`,
490
+ json: true,
491
+ });
492
+ }
493
+ else if (operation === 'reschedule') {
494
+ const id = this.getNodeParameter('scheduledId', i);
495
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
496
+ method: 'PATCH',
497
+ url: `${baseUrl}/v1/scheduled/${id}/reschedule`,
498
+ body: { scheduledFor: this.getNodeParameter('scheduledFor', i) },
499
+ json: true,
500
+ });
501
+ }
502
+ }
503
+ // ─── Contact ───
504
+ else if (resource === 'contact') {
505
+ if (operation === 'list') {
506
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
507
+ method: 'GET',
508
+ url: `${baseUrl}/v1/emails/contacts`,
509
+ json: true,
510
+ });
511
+ }
512
+ else if (operation === 'get') {
513
+ const id = this.getNodeParameter('contactId', i);
514
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
515
+ method: 'GET',
516
+ url: `${baseUrl}/v1/emails/contacts/${id}`,
517
+ json: true,
518
+ });
519
+ }
520
+ else if (operation === 'create') {
521
+ const fields = this.getNodeParameter('contactFields', i, {});
522
+ const body = {
523
+ email: this.getNodeParameter('email', i),
524
+ };
525
+ if (fields.name)
526
+ body.name = fields.name;
527
+ if (fields.tags)
528
+ body.tags = fields.tags.split(',').map(s => s.trim());
529
+ if (fields.metadata)
530
+ body.metadata = JSON.parse(fields.metadata);
531
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
532
+ method: 'POST',
533
+ url: `${baseUrl}/v1/emails/contacts`,
534
+ body,
535
+ json: true,
536
+ });
537
+ }
538
+ else if (operation === 'update') {
539
+ const id = this.getNodeParameter('contactId', i);
540
+ const fields = this.getNodeParameter('contactFields', i, {});
541
+ const body = {};
542
+ if (fields.name)
543
+ body.name = fields.name;
544
+ if (fields.tags)
545
+ body.tags = fields.tags.split(',').map(s => s.trim());
546
+ if (fields.metadata)
547
+ body.metadata = JSON.parse(fields.metadata);
548
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
549
+ method: 'PATCH',
550
+ url: `${baseUrl}/v1/emails/contacts/${id}`,
551
+ body,
552
+ json: true,
553
+ });
554
+ }
555
+ else if (operation === 'delete') {
556
+ const id = this.getNodeParameter('contactId', i);
557
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
558
+ method: 'DELETE',
559
+ url: `${baseUrl}/v1/emails/contacts/${id}`,
560
+ json: true,
561
+ });
562
+ }
563
+ }
564
+ // ─── Suppression ───
565
+ else if (resource === 'suppression') {
566
+ if (operation === 'list') {
567
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
568
+ method: 'GET',
569
+ url: `${baseUrl}/v1/emails/suppressions`,
570
+ json: true,
571
+ });
572
+ }
573
+ else if (operation === 'add') {
574
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
575
+ method: 'POST',
576
+ url: `${baseUrl}/v1/emails/suppressions`,
577
+ body: {
578
+ email: this.getNodeParameter('email', i),
579
+ reason: this.getNodeParameter('reason', i),
580
+ },
581
+ json: true,
582
+ });
583
+ }
584
+ else if (operation === 'remove') {
585
+ const id = this.getNodeParameter('suppressionId', i);
586
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
587
+ method: 'DELETE',
588
+ url: `${baseUrl}/v1/emails/suppressions/${id}`,
589
+ json: true,
590
+ });
591
+ }
592
+ }
593
+ // ─── Webhook ───
594
+ else if (resource === 'webhook') {
595
+ if (operation === 'list') {
596
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
597
+ method: 'GET',
598
+ url: `${baseUrl}/v1/webhooks`,
599
+ json: true,
600
+ });
601
+ }
602
+ else if (operation === 'create') {
603
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
604
+ method: 'POST',
605
+ url: `${baseUrl}/v1/webhooks`,
606
+ body: {
607
+ url: this.getNodeParameter('url', i),
608
+ events: this.getNodeParameter('events', i),
609
+ },
610
+ json: true,
611
+ });
612
+ }
613
+ else if (operation === 'update') {
614
+ const id = this.getNodeParameter('webhookId', i);
615
+ const body = {};
616
+ const url = this.getNodeParameter('url', i, '');
617
+ const events = this.getNodeParameter('events', i, []);
618
+ if (url)
619
+ body.url = url;
620
+ if (events.length)
621
+ body.events = events;
622
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
623
+ method: 'PATCH',
624
+ url: `${baseUrl}/v1/webhooks/${id}`,
625
+ body,
626
+ json: true,
627
+ });
628
+ }
629
+ else if (operation === 'delete') {
630
+ const id = this.getNodeParameter('webhookId', i);
631
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
632
+ method: 'DELETE',
633
+ url: `${baseUrl}/v1/webhooks/${id}`,
634
+ json: true,
635
+ });
636
+ }
637
+ else if (operation === 'test') {
638
+ const id = this.getNodeParameter('webhookId', i);
639
+ responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
640
+ method: 'POST',
641
+ url: `${baseUrl}/v1/webhooks/${id}/test`,
642
+ json: true,
643
+ });
644
+ }
645
+ }
646
+ if (Array.isArray(responseData)) {
647
+ returnData.push(...responseData.map(d => ({ json: d })));
648
+ }
649
+ else {
650
+ returnData.push({ json: responseData ?? { success: true } });
651
+ }
652
+ }
653
+ catch (error) {
654
+ if (this.continueOnFail()) {
655
+ returnData.push({ json: { error: error.message } });
656
+ continue;
657
+ }
658
+ throw error;
659
+ }
660
+ }
661
+ return [returnData];
662
+ }
663
+ }
664
+ exports.Posthawk = Posthawk;
@@ -0,0 +1,12 @@
1
+ import { IHookFunctions, IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
2
+ export declare class PosthawkTrigger implements INodeType {
3
+ description: INodeTypeDescription;
4
+ webhookMethods: {
5
+ default: {
6
+ checkExists(this: IHookFunctions): Promise<boolean>;
7
+ create(this: IHookFunctions): Promise<boolean>;
8
+ delete(this: IHookFunctions): Promise<boolean>;
9
+ };
10
+ };
11
+ webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
12
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PosthawkTrigger = void 0;
4
+ class PosthawkTrigger {
5
+ constructor() {
6
+ this.description = {
7
+ displayName: 'Posthawk Trigger',
8
+ name: 'posthawkTrigger',
9
+ icon: 'file:posthawk.svg',
10
+ group: ['trigger'],
11
+ version: 1,
12
+ subtitle: '={{$parameter["events"].join(", ")}}',
13
+ description: 'Triggers workflow on Posthawk email events (delivery, bounce, open, etc.)',
14
+ defaults: {
15
+ name: 'Posthawk Trigger',
16
+ },
17
+ inputs: [],
18
+ outputs: ['main'],
19
+ credentials: [
20
+ {
21
+ name: 'posthawkApi',
22
+ required: true,
23
+ },
24
+ ],
25
+ webhooks: [
26
+ {
27
+ name: 'default',
28
+ httpMethod: 'POST',
29
+ responseMode: 'onReceived',
30
+ path: 'webhook',
31
+ },
32
+ ],
33
+ properties: [
34
+ {
35
+ displayName: 'Events',
36
+ name: 'events',
37
+ type: 'multiOptions',
38
+ required: true,
39
+ options: [
40
+ { name: 'Send', value: 'send', description: 'Email accepted by SES' },
41
+ { name: 'Delivery', value: 'delivery', description: 'Email delivered to recipient' },
42
+ { name: 'Bounce', value: 'bounce', description: 'Email bounced' },
43
+ { name: 'Complaint', value: 'complaint', description: 'Recipient marked as spam' },
44
+ { name: 'Open', value: 'open', description: 'Email opened by recipient' },
45
+ { name: 'Click', value: 'click', description: 'Link clicked in email' },
46
+ { name: 'Reject', value: 'reject', description: 'Email rejected by SES' },
47
+ { name: 'Delivery Delay', value: 'delivery_delay', description: 'Delivery delayed' },
48
+ ],
49
+ default: ['delivery', 'bounce', 'complaint'],
50
+ description: 'Which email events should trigger this workflow',
51
+ },
52
+ ],
53
+ };
54
+ this.webhookMethods = {
55
+ default: {
56
+ async checkExists() {
57
+ const webhookUrl = this.getNodeWebhookUrl('default');
58
+ const credentials = await this.getCredentials('posthawkApi');
59
+ const baseUrl = credentials.baseUrl;
60
+ const response = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
61
+ method: 'GET',
62
+ url: `${baseUrl}/v1/webhooks`,
63
+ json: true,
64
+ });
65
+ const webhooks = Array.isArray(response) ? response : response?.webhooks ?? [];
66
+ const existing = webhooks.find((w) => w.url === webhookUrl);
67
+ if (existing) {
68
+ const webhookData = this.getWorkflowStaticData('node');
69
+ webhookData.webhookId = existing.id;
70
+ return true;
71
+ }
72
+ return false;
73
+ },
74
+ async create() {
75
+ const webhookUrl = this.getNodeWebhookUrl('default');
76
+ const events = this.getNodeParameter('events');
77
+ const credentials = await this.getCredentials('posthawkApi');
78
+ const baseUrl = credentials.baseUrl;
79
+ const response = await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
80
+ method: 'POST',
81
+ url: `${baseUrl}/v1/webhooks`,
82
+ body: {
83
+ url: webhookUrl,
84
+ events,
85
+ },
86
+ json: true,
87
+ });
88
+ if (!response?.id && !response?.webhook?.id) {
89
+ return false;
90
+ }
91
+ const webhookData = this.getWorkflowStaticData('node');
92
+ webhookData.webhookId = response.id || response.webhook.id;
93
+ return true;
94
+ },
95
+ async delete() {
96
+ const webhookData = this.getWorkflowStaticData('node');
97
+ const webhookId = webhookData.webhookId;
98
+ if (!webhookId)
99
+ return true;
100
+ const credentials = await this.getCredentials('posthawkApi');
101
+ const baseUrl = credentials.baseUrl;
102
+ try {
103
+ await this.helpers.httpRequestWithAuthentication.call(this, 'posthawkApi', {
104
+ method: 'DELETE',
105
+ url: `${baseUrl}/v1/webhooks/${webhookId}`,
106
+ json: true,
107
+ });
108
+ }
109
+ catch {
110
+ // Webhook may have been deleted already
111
+ }
112
+ delete webhookData.webhookId;
113
+ return true;
114
+ },
115
+ },
116
+ };
117
+ }
118
+ async webhook() {
119
+ const body = this.getBodyData();
120
+ // Posthawk sends webhook payloads with signature in X-Posthawk-Signature header
121
+ // The event type is in the body
122
+ return {
123
+ workflowData: [
124
+ this.helpers.returnJsonArray([body]),
125
+ ],
126
+ };
127
+ }
128
+ }
129
+ exports.PosthawkTrigger = PosthawkTrigger;
@@ -0,0 +1,4 @@
1
+ <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="60" height="60" rx="8" fill="#8262ff"/>
3
+ <path d="M30 14L18 26h8v8l-8 8h24l-8-8v-8h8L30 14z" fill="white" fill-opacity="0.9"/>
4
+ </svg>
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "n8n-nodes-posthawk",
3
+ "version": "0.1.0",
4
+ "description": "n8n node for Posthawk — send, schedule, and manage emails via the Posthawk API",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "n8n",
8
+ "posthawk",
9
+ "email",
10
+ "transactional-email",
11
+ "email-api"
12
+ ],
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Posthawk",
16
+ "email": "support@posthawk.dev"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/endibuka/posthawk"
21
+ },
22
+ "main": "dist/index.js",
23
+ "scripts": {
24
+ "build": "tsc && gulp build:icons",
25
+ "dev": "tsc --watch",
26
+ "lint": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "n8n": {
33
+ "n8nNodesApiVersion": 1,
34
+ "credentials": [
35
+ "dist/credentials/PosthawkApi.credentials.js"
36
+ ],
37
+ "nodes": [
38
+ "dist/nodes/Posthawk/Posthawk.node.js",
39
+ "dist/nodes/Posthawk/PosthawkTrigger.node.js"
40
+ ]
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "gulp": "^5.0.0",
45
+ "n8n-workflow": "^1.0.0",
46
+ "typescript": "^5.5.0"
47
+ },
48
+ "peerDependencies": {
49
+ "n8n-workflow": "*"
50
+ }
51
+ }