diff --git a/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts b/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts
new file mode 100644
index 000000000..5ee2344ef
--- /dev/null
+++ b/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts
@@ -0,0 +1,23 @@
+import {
+ ICredentialType,
+ NodePropertyTypes,
+} from 'n8n-workflow';
+
+export class AcuitySchedulingApi implements ICredentialType {
+ name = 'acuitySchedulingApi';
+ displayName = 'Acuity Scheduling API';
+ properties = [
+ {
+ displayName: 'User ID',
+ name: 'userId',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ {
+ displayName: 'API Key',
+ name: 'apiKey',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ ];
+}
diff --git a/packages/nodes-base/credentials/CopperApi.credentials.ts b/packages/nodes-base/credentials/CopperApi.credentials.ts
new file mode 100644
index 000000000..ea3105054
--- /dev/null
+++ b/packages/nodes-base/credentials/CopperApi.credentials.ts
@@ -0,0 +1,25 @@
+import {
+ ICredentialType,
+ NodePropertyTypes,
+} from 'n8n-workflow';
+
+export class CopperApi implements ICredentialType {
+ name = 'copperApi';
+ displayName = 'Copper API';
+ properties = [
+ {
+ displayName: 'API Key',
+ name: 'apiKey',
+ required: true,
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ {
+ displayName: 'Email',
+ name: 'email',
+ required: true,
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ },
+ ];
+}
diff --git a/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts b/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts
new file mode 100644
index 000000000..2166b5cc8
--- /dev/null
+++ b/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts
@@ -0,0 +1,163 @@
+import {
+ IHookFunctions,
+ IWebhookFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+ INodeTypeDescription,
+ INodeType,
+ IWebhookResponseData,
+} from 'n8n-workflow';
+
+import {
+ acuitySchedulingApiRequest,
+} from './GenericFunctions';
+
+export class AcuitySchedulingTrigger implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Acuity Scheduling Trigger',
+ name: 'acuitySchedulingTrigger',
+ icon: 'file:acuityScheduling.png',
+ group: ['trigger'],
+ version: 1,
+ description: 'Handle Acuity Scheduling events via webhooks',
+ defaults: {
+ name: 'Acuity Scheduling Trigger',
+ color: '#000000',
+ },
+ inputs: [],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'acuitySchedulingApi',
+ required: true,
+ }
+ ],
+ webhooks: [
+ {
+ name: 'default',
+ httpMethod: 'POST',
+ responseMode: 'onReceived',
+ path: 'webhook',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Event',
+ name: 'event',
+ type: 'options',
+ required: true,
+ default: '',
+ options: [
+ {
+ name: 'appointment.scheduled',
+ value: 'appointment.scheduled',
+ description: 'is called once when an appointment is initially booked',
+ },
+ {
+ name: 'appointment.rescheduled',
+ value: 'appointment.rescheduled',
+ description: 'is called when the appointment is rescheduled to a new time',
+ },
+ {
+ name: 'appointment.canceled',
+ value: 'appointment.canceled',
+ description: 'is called whenever an appointment is canceled',
+ },
+ {
+ name: 'appointment.changed',
+ value: 'appointment.changed',
+ description: 'is called when the appointment is changed in any way',
+ },
+ {
+ name: 'order.completed',
+ value: 'order.completed',
+ description: 'is called when an order is completed',
+ },
+ ],
+ },
+ {
+ displayName: 'Resolve Data',
+ name: 'resolveData',
+ type: 'boolean',
+ default: true,
+ description: 'By default does the webhook-data only contain the ID of the object.
If this option gets activated it will resolve the data automatically.',
+ },
+ ],
+ };
+ // @ts-ignore
+ webhookMethods = {
+ default: {
+ async checkExists(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ if (webhookData.webhookId === undefined) {
+ return false;
+ }
+ const endpoint = '/webhooks';
+ const webhooks = await acuitySchedulingApiRequest.call(this, 'GET', endpoint);
+ if (Array.isArray(webhooks)) {
+ for (const webhook of webhooks) {
+ if (webhook.id === webhookData.webhookId) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+ async create(this: IHookFunctions): Promise {
+ const webhookUrl = this.getNodeWebhookUrl('default');
+ const webhookData = this.getWorkflowStaticData('node');
+ const event = this.getNodeParameter('event') as string;
+ const endpoint = '/webhooks';
+ const body: IDataObject = {
+ target: webhookUrl,
+ event,
+ };
+ const { id } = await acuitySchedulingApiRequest.call(this, 'POST', endpoint, body);
+ webhookData.webhookId = id;
+ return true;
+ },
+ async delete(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ const endpoint = `/webhooks/${webhookData.webhookId}`;
+ try {
+ await acuitySchedulingApiRequest.call(this, 'DELETE', endpoint);
+ } catch(error) {
+ return false;
+ }
+ delete webhookData.webhookId;
+ return true;
+ },
+ },
+ };
+
+ async webhook(this: IWebhookFunctions): Promise {
+ const req = this.getRequestObject();
+
+ const resolveData = this.getNodeParameter('resolveData', false) as boolean;
+
+ if (resolveData === false) {
+ // Return the data as it got received
+ return {
+ workflowData: [
+ this.helpers.returnJsonArray(req.body),
+ ],
+ };
+ }
+
+ // Resolve the data by requesting the information via API
+ const event = this.getNodeParameter('event', false) as string;
+ const eventType = event.split('.').shift();
+ const endpoint = `/${eventType}s/${req.body.id}`;
+ const responseData = await acuitySchedulingApiRequest.call(this, 'GET', endpoint, {});
+
+ return {
+ workflowData: [
+ this.helpers.returnJsonArray(responseData),
+ ],
+ };
+
+
+ }
+}
diff --git a/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts b/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts
new file mode 100644
index 000000000..9a2d0fc59
--- /dev/null
+++ b/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts
@@ -0,0 +1,42 @@
+import { OptionsWithUri } from 'request';
+import {
+ IExecuteFunctions,
+ IExecuteSingleFunctions,
+ IHookFunctions,
+ ILoadOptionsFunctions,
+ IWebhookFunctions,
+} from 'n8n-core';
+import { IDataObject } from 'n8n-workflow';
+
+export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any
+ const credentials = this.getCredentials('acuitySchedulingApi');
+ if (credentials === undefined) {
+ throw new Error('No credentials got returned!');
+ }
+
+ const options: OptionsWithUri = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ auth: {
+ user: credentials.userId as string,
+ password: credentials.apiKey as string,
+ },
+ method,
+ qs,
+ body,
+ uri: uri ||`https://acuityscheduling.com/api/v1${resource}`,
+ json: true
+ };
+ try {
+ return await this.helpers.request!(options);
+ } catch (error) {
+
+ let errorMessage = error.message;
+ if (error.response.body && error.response.body.message) {
+ errorMessage = `[${error.response.body.status_code}] ${error.response.body.message}`;
+ }
+
+ throw new Error('Acuity Scheduling Error: ' + errorMessage);
+ }
+}
diff --git a/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png b/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png
new file mode 100644
index 000000000..df087b60e
Binary files /dev/null and b/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png differ
diff --git a/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts b/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts
new file mode 100644
index 000000000..2c95c25eb
--- /dev/null
+++ b/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts
@@ -0,0 +1,174 @@
+import {
+ IHookFunctions,
+ IWebhookFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+ INodeTypeDescription,
+ INodeType,
+ IWebhookResponseData,
+} from 'n8n-workflow';
+
+import {
+ copperApiRequest,
+ getAutomaticSecret,
+} from './GenericFunctions';
+
+export class CopperTrigger implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Copper Trigger',
+ name: 'copperTrigger',
+ icon: 'file:copper.png',
+ group: ['trigger'],
+ version: 1,
+ description: 'Handle Copper events via webhooks',
+ defaults: {
+ name: 'Copper Trigger',
+ color: '#ff2564',
+ },
+ inputs: [],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'copperApi',
+ required: true,
+ }
+ ],
+ webhooks: [
+ {
+ name: 'default',
+ httpMethod: 'POST',
+ responseMode: 'onReceived',
+ path: 'webhook',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ required: true,
+ default: '',
+ options: [
+ {
+ name: 'Company',
+ value: 'company',
+ },
+ {
+ name: 'Lead',
+ value: 'lead',
+ },
+ {
+ name: 'Opportunity',
+ value: 'opportunity',
+ },
+ {
+ name: 'Person',
+ value: 'person',
+ },
+ {
+ name: 'Project',
+ value: 'project',
+ },
+ {
+ name: 'Task',
+ value: 'task',
+ },
+ ],
+ description: 'The resource which will fire the event.',
+ },
+ {
+ displayName: 'Event',
+ name: 'event',
+ type: 'options',
+ required: true,
+ default: '',
+ options: [
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'An existing record is removed',
+ },
+ {
+ name: 'New',
+ value: 'new',
+ description: 'A new record is created',
+ },
+ {
+ name: 'Update',
+ value: 'update',
+ description: 'Any field in the existing entity record is changed',
+ },
+ ],
+ description: 'The event to listen to.',
+ },
+ ],
+ };
+ // @ts-ignore
+ webhookMethods = {
+ default: {
+ async checkExists(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ if (webhookData.webhookId === undefined) {
+ return false;
+ }
+ const endpoint = `/webhooks/${webhookData.webhookId}`;
+ try {
+ await copperApiRequest.call(this, 'GET', endpoint);
+ } catch (err) {
+ return false;
+ }
+ return true;
+ },
+ async create(this: IHookFunctions): Promise {
+ const webhookUrl = this.getNodeWebhookUrl('default');
+ const webhookData = this.getWorkflowStaticData('node');
+ const resource = this.getNodeParameter('resource') as string;
+ const event = this.getNodeParameter('event') as string;
+ const endpoint = '/webhooks';
+ const body: IDataObject = {
+ target: webhookUrl,
+ type: resource,
+ event,
+ };
+
+ const credentials = this.getCredentials('copperApi');
+ body.secret = {
+ secret: getAutomaticSecret(credentials!),
+ };
+
+ const { id } = await copperApiRequest.call(this, 'POST', endpoint, body);
+ webhookData.webhookId = id;
+ return true;
+ },
+ async delete(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ const endpoint = `/webhooks/${webhookData.webhookId}`;
+ try {
+ await copperApiRequest.call(this, 'DELETE', endpoint);
+ } catch(error) {
+ return false;
+ }
+ delete webhookData.webhookId;
+ return true;
+ },
+ },
+ };
+
+ async webhook(this: IWebhookFunctions): Promise {
+ const credentials = this.getCredentials('copperApi');
+ const req = this.getRequestObject();
+
+ // Check if the supplied secret matches. If not ignore request.
+ if (req.body.secret !== getAutomaticSecret(credentials!)) {
+ return {};
+ }
+
+ return {
+ workflowData: [
+ this.helpers.returnJsonArray(req.body),
+ ],
+ };
+ }
+}
diff --git a/packages/nodes-base/nodes/Copper/GenericFunctions.ts b/packages/nodes-base/nodes/Copper/GenericFunctions.ts
new file mode 100644
index 000000000..31bd1379c
--- /dev/null
+++ b/packages/nodes-base/nodes/Copper/GenericFunctions.ts
@@ -0,0 +1,63 @@
+import { createHash } from 'crypto';
+import { OptionsWithUri } from 'request';
+
+import {
+ IExecuteFunctions,
+ IExecuteSingleFunctions,
+ IHookFunctions,
+ ILoadOptionsFunctions,
+ IWebhookFunctions,
+} from 'n8n-core';
+import {
+ ICredentialDataDecryptedObject,
+ IDataObject,
+} from 'n8n-workflow';
+
+export async function copperApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any
+ const credentials = this.getCredentials('copperApi');
+ if (credentials === undefined) {
+ throw new Error('No credentials got returned!');
+ }
+
+ let options: OptionsWithUri = {
+ headers: {
+ 'X-PW-AccessToken': credentials.apiKey,
+ 'X-PW-Application': 'developer_api',
+ 'X-PW-UserEmail': credentials.email,
+ 'Content-Type': 'application/json',
+ },
+ method,
+ qs,
+ body,
+ uri: uri ||`https://api.prosperworks.com/developer_api/v1${resource}`,
+ json: true
+ };
+ options = Object.assign({}, options, option);
+ if (Object.keys(options.body).length === 0) {
+ delete options.body;
+ }
+
+ try {
+ return await this.helpers.request!(options);
+ } catch (error) {
+ let errorMessage = error.message;
+ if (error.response.body && error.response.body.message) {
+ errorMessage = error.response.body.message;
+ }
+
+ throw new Error('Copper Error: ' + errorMessage);
+ }
+}
+
+
+/**
+ * Creates a secret from the credentials
+ *
+ * @export
+ * @param {ICredentialDataDecryptedObject} credentials
+ * @returns
+ */
+export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) {
+ const data = `${credentials.email},${credentials.apiKey}`;
+ return createHash('md5').update(data).digest("hex");
+}
diff --git a/packages/nodes-base/nodes/Copper/copper.png b/packages/nodes-base/nodes/Copper/copper.png
new file mode 100644
index 000000000..befa65c18
Binary files /dev/null and b/packages/nodes-base/nodes/Copper/copper.png differ
diff --git a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts
index 53a0a7530..709b9858c 100644
--- a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts
+++ b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts
@@ -19,7 +19,7 @@ import {
export class WebflowTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Webflow Trigger',
- name: 'webflow',
+ name: 'webflowTrigger',
icon: 'file:webflow.png',
group: ['trigger'],
version: 1,
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index 8835a31ec..4087568d5 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -27,6 +27,7 @@
"n8n": {
"credentials": [
"dist/credentials/ActiveCampaignApi.credentials.js",
+ "dist/credentials/AcuitySchedulingApi.credentials.js",
"dist/credentials/AirtableApi.credentials.js",
"dist/credentials/Amqp.credentials.js",
"dist/credentials/AsanaApi.credentials.js",
@@ -35,6 +36,7 @@
"dist/credentials/ChargebeeApi.credentials.js",
"dist/credentials/CodaApi.credentials.js",
"dist/credentials/ClickUpApi.credentials.js",
+ "dist/credentials/CopperApi.credentials.js",
"dist/credentials/DropboxApi.credentials.js",
"dist/credentials/EventbriteApi.credentials.js",
"dist/credentials/FreshdeskApi.credentials.js",
@@ -86,6 +88,7 @@
"dist/nodes/ActiveCampaign/ActiveCampaign.node.js",
"dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js",
"dist/nodes/Airtable/Airtable.node.js",
+ "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js",
"dist/nodes/Amqp/Amqp.node.js",
"dist/nodes/Amqp/AmqpTrigger.node.js",
"dist/nodes/Asana/Asana.node.js",
@@ -98,6 +101,7 @@
"dist/nodes/Coda/Coda.node.js",
"dist/nodes/ClickUp/ClickUp.node.js",
"dist/nodes/ClickUp/ClickUpTrigger.node.js",
+ "dist/nodes/Copper/CopperTrigger.node.js",
"dist/nodes/Cron.node.js",
"dist/nodes/Discord/Discord.node.js",
"dist/nodes/Dropbox/Dropbox.node.js",