Create ZapSign n8n node with comprehensive documentation and implementation

Co-authored-by: andre <andre@zapsign.com.br>
This commit is contained in:
Cursor Agent 2025-07-31 12:11:07 +00:00
commit b76fd3d7f3
16 changed files with 6991 additions and 579 deletions

View file

@ -1,77 +0,0 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
export class ExampleNode implements INodeType {
description: INodeTypeDescription = {
displayName: 'Example Node',
name: 'exampleNode',
group: ['transform'],
version: 1,
description: 'Basic Example Node',
defaults: {
name: 'Example Node',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
usableAsTool: true,
properties: [
// Node properties which the user gets displayed and
// can change on the node.
{
displayName: 'My String',
name: 'myString',
type: 'string',
default: '',
placeholder: 'Placeholder value',
description: 'The description text',
},
],
};
// The function below is responsible for actually doing whatever this node
// is supposed to do. In this case, we're just appending the `myString` property
// with whatever the user has entered.
// You can make async calls and use `await`.
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
let item: INodeExecutionData;
let myString: string;
// Iterates over all input items and add the key "myString" with the
// value the parameter "myString" resolves to.
// (This could be a different value for each item in case it contains an expression)
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
myString = this.getNodeParameter('myString', itemIndex, '') as string;
item = items[itemIndex];
item.json.myString = myString;
} catch (error) {
// This node should never fail but we want to showcase how
// to handle errors.
if (this.continueOnFail()) {
items.push({ json: this.getInputData(itemIndex)[0].json, error, pairedItem: itemIndex });
} else {
// Adding `itemIndex` allows other workflows to handle this error
if (error.context) {
// If the error thrown already contains the context property,
// only append the itemIndex
error.context.itemIndex = itemIndex;
throw error;
}
throw new NodeOperationError(this.getNode(), error, {
itemIndex,
});
}
}
}
return [items];
}
}

View file

@ -1,18 +0,0 @@
{
"node": "n8n-nodes-base.httpbin",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "http://httpbin.org/#/Auth/get_bearer"
}
],
"primaryDocumentation": [
{
"url": "http://httpbin.org/"
}
]
}
}

View file

@ -1,63 +0,0 @@
import { INodeType, INodeTypeDescription, NodeConnectionType } from 'n8n-workflow';
import { httpVerbFields, httpVerbOperations } from './HttpVerbDescription';
export class HttpBin implements INodeType {
description: INodeTypeDescription = {
displayName: 'HttpBin',
name: 'httpBin',
icon: { light: 'file:httpbin.svg', dark: 'file:httpbin.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with HttpBin API',
defaults: {
name: 'HttpBin',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
usableAsTool: true,
credentials: [
{
name: 'httpbinApi',
required: false,
},
],
requestDefaults: {
baseURL: 'https://httpbin.org',
url: '',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
/**
* In the properties array we have two mandatory options objects required
*
* [Resource & Operation]
*
* https://docs.n8n.io/integrations/creating-nodes/code/create-first-node/#resources-and-operations
*
* In our example, the operations are separated into their own file (HTTPVerbDescription.ts)
* to keep this class easy to read.
*
*/
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'HTTP Verb',
value: 'httpVerb',
},
],
default: 'httpVerb',
},
...httpVerbOperations,
...httpVerbFields,
],
};
}

View file

@ -1,250 +0,0 @@
import { INodeProperties } from 'n8n-workflow';
// When the resource `httpVerb` is selected, this `operation` parameter will be shown.
export const httpVerbOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['httpVerb'],
},
},
options: [
{
name: 'GET',
value: 'get',
description: 'Perform a GET request',
action: 'Perform a GET request',
routing: {
request: {
method: 'GET',
url: '/get',
},
},
},
{
name: 'DELETE',
value: 'delete',
description: 'Perform a DELETE request',
action: 'Perform a DELETE request',
routing: {
request: {
method: 'DELETE',
url: '/delete',
},
},
},
],
default: 'get',
},
];
// Here we define what to show when the `get` operation is selected.
// We do that by adding `operation: ["get"]` to `displayOptions.show`
const getOperation: INodeProperties[] = [
{
displayName: 'Type of Data',
name: 'typeofData',
default: 'queryParameter',
description: 'Select type of data to send [Query Parameters]',
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['get'],
},
},
type: 'options',
options: [
{
name: 'Query',
value: 'queryParameter',
},
],
required: true,
},
{
displayName: 'Query Parameters',
name: 'arguments',
default: {},
description: "The request's query parameters",
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['get'],
},
},
options: [
{
name: 'keyvalue',
displayName: 'Key:Value',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Key of query parameter',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '={{$parent.key}}',
type: 'query',
},
},
required: true,
description: 'Value of query parameter',
},
],
},
],
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
];
// Here we define what to show when the DELETE Operation is selected.
// We do that by adding `operation: ["delete"]` to `displayOptions.show`
const deleteOperation: INodeProperties[] = [
{
displayName: 'Type of Data',
name: 'typeofData',
default: 'queryParameter',
description: 'Select type of data to send [Query Parameter Arguments, JSON-Body]',
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['delete'],
},
},
options: [
{
name: 'Query',
value: 'queryParameter',
},
{
name: 'JSON',
value: 'jsonData',
},
],
required: true,
type: 'options',
},
{
displayName: 'Query Parameters',
name: 'arguments',
default: {},
description: "The request's query parameters",
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['delete'],
typeofData: ['queryParameter'],
},
},
options: [
{
name: 'keyvalue',
displayName: 'Key:Value',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Key of query parameter',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '={{$parent.key}}',
type: 'query',
},
},
required: true,
description: 'Value of query parameter',
},
],
},
],
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
{
displayName: 'JSON Object',
name: 'arguments',
default: {},
description: "The request's JSON properties",
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['delete'],
typeofData: ['jsonData'],
},
},
options: [
{
name: 'keyvalue',
displayName: 'Key:Value',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Key of JSON property',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '={{$parent.key}}',
type: 'body',
},
},
required: true,
description: 'Value of JSON property',
},
],
},
],
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
];
export const httpVerbFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* httpVerb:get */
/* -------------------------------------------------------------------------- */
...getOperation,
/* -------------------------------------------------------------------------- */
/* httpVerb:delete */
/* -------------------------------------------------------------------------- */
...deleteOperation,
];

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve"> <image id="image0" width="32" height="32" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElN
RQfmBg4UAC/TqOZZAAACA0lEQVRIx5XVv09TURwF8M+jFHDSyRkGFhPAEfyRdDHi5uriXyDoYgKT
MJDWzUT/Ahf/AiOEpajEgCESmpiYmDCxGowDTYE+h76+vte+15Zzk753b7733HNO772PbEw7ECba
genswtEcgl0/PHARV72066YrIDSZ6k8KBym4741r0XsB284TdUX8chn1zrzwJUmw4KFXPqjFE0Y0
u5YKEhpmfLZuy7f2wLKGI8WhDRYdaVhurdTCidmU5P44N+skaaGQH1IfFFrOYMotT932zNgQExve
OfTeT8dtBceO3TFlOyopY7UPxV+/fWyn3Y0xrFhJjZWFXhs12pKdRO9ObGSuyB8Xbd9JjMjDc6HQ
IcrKqAiVe8vyCEJPrGBWxZYqqtZt9RbmHabAvAAVdVUlJTvWshbMt0AYn40OmlchSKOePTyYIMQn
rb8yI8TsDCrRs4od7Jv3KOoPGWKboBqp2LN3FQvdO7EPshSsRSTXrSop2cSiiUGkG/bj2JqaQiHW
4nv50mFcu28j30KQarAnEPhuzvwwGYQ975vx7+JwGXTjTIAzoYlhCArR5d0KkfauqJAVY6+FG5hD
OS6veqyCuSiTAQT/jKmlQtyxIBCoZV28HQvN6LuQvJFC4xjvibfYOZUdUXd9taTWJbOubiIVXmjG
W/fs9qpZcpr6pOe1U0udSf8BR7ef4yxyOskAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjItMDYtMTRU
MTc6MDA6NDcrMDM6MDBfo1sRAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIyLTA2LTE0VDE3OjAwOjQ3
KzAzOjAwLv7jrQAAAABJRU5ErkJggg==" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,889 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IRequestOptions,
IDataObject,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
export class ZapSign implements INodeType {
description: INodeTypeDescription = {
displayName: 'ZapSign',
name: 'zapSign',
icon: 'file:zapsign.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with ZapSign API for digital signatures',
defaults: {
name: 'ZapSign',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'zapSignApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Document',
value: 'document',
},
{
name: 'Signer',
value: 'signer',
},
{
name: 'Template',
value: 'template',
},
{
name: 'Webhook',
value: 'webhook',
},
],
default: 'document',
},
// Document operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['document'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new document',
action: 'Create a document',
},
{
name: 'Get',
value: 'get',
description: 'Get a document',
action: 'Get a document',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many documents',
action: 'Get many documents',
},
{
name: 'Send',
value: 'send',
description: 'Send document for signature',
action: 'Send a document for signature',
},
{
name: 'Cancel',
value: 'cancel',
description: 'Cancel a document',
action: 'Cancel a document',
},
{
name: 'Download',
value: 'download',
description: 'Download a signed document',
action: 'Download a document',
},
],
default: 'create',
},
// Signer operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['signer'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a signer to a document',
action: 'Add a signer',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many signers of a document',
action: 'Get many signers',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a signer from a document',
action: 'Remove a signer',
},
{
name: 'Update',
value: 'update',
description: 'Update a signer',
action: 'Update a signer',
},
],
default: 'add',
},
// Template operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['template'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Get many templates',
action: 'Get many templates',
},
{
name: 'Create Document From Template',
value: 'createDocument',
action: 'Create document from template',
},
],
default: 'getAll',
},
// Webhook operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['webhook'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a webhook',
action: 'Create a webhook',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many webhooks',
action: 'Get many webhooks',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a webhook',
action: 'Delete a webhook',
},
],
default: 'create',
},
// Document fields
{
displayName: 'Document Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
description: 'Name of the document',
},
{
displayName: 'File',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
description: 'Name of the binary property containing the file data',
},
{
displayName: 'Document ID',
name: 'documentId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['document'],
operation: ['get', 'send', 'cancel', 'download'],
},
},
description: 'ID of the document',
},
{
displayName: 'Document ID',
name: 'documentId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'getAll', 'remove', 'update'],
},
},
description: 'ID of the document',
},
// Signer fields
{
displayName: 'Signer Email',
name: 'signerEmail',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'remove', 'update'],
},
},
description: 'Email of the signer',
},
{
displayName: 'Signer Name',
name: 'signerName',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'update'],
},
},
description: 'Name of the signer',
},
{
displayName: 'Authentication Method',
name: 'authMethod',
type: 'options',
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'SMS',
value: 'sms',
},
{
name: 'WhatsApp',
value: 'whatsapp',
},
],
default: 'email',
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'update'],
},
},
description: 'Authentication method for the signer',
},
{
displayName: 'Phone Number',
name: 'phoneNumber',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'update'],
authMethod: ['sms', 'whatsapp'],
},
},
description: 'Phone number for SMS or WhatsApp authentication',
},
{
displayName: 'Require Document Authentication',
name: 'requireDocAuth',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'update'],
},
},
description: 'Whether to require document authentication (ID upload)',
},
{
displayName: 'Require Facial Recognition',
name: 'requireFacialRecognition',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['signer'],
operation: ['add', 'update'],
},
},
description: 'Whether to require facial recognition',
},
// Template fields
{
displayName: 'Template ID',
name: 'templateId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['template'],
operation: ['createDocument'],
},
},
description: 'ID of the template',
},
// Webhook fields
{
displayName: 'Webhook URL',
name: 'webhookUrl',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['webhook'],
operation: ['create'],
},
},
description: 'URL to receive webhook notifications',
},
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: 'Document Created',
value: 'document.created',
},
{
name: 'Document Sent',
value: 'document.sent',
},
{
name: 'Document Signed',
value: 'document.signed',
},
{
name: 'Document Completed',
value: 'document.completed',
},
{
name: 'Document Cancelled',
value: 'document.cancelled',
},
{
name: 'Signer Signed',
value: 'signer.signed',
},
],
default: ['document.completed'],
displayOptions: {
show: {
resource: ['webhook'],
operation: ['create'],
},
},
description: 'Events to listen for',
},
{
displayName: 'Webhook ID',
name: 'webhookId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['webhook'],
operation: ['delete'],
},
},
description: 'ID of the webhook to delete',
},
// Additional options
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
options: [
{
displayName: 'Brand ID',
name: 'brandId',
type: 'string',
default: '',
description: 'Brand ID for custom branding',
},
{
displayName: 'Locale',
name: 'locale',
type: 'options',
options: [
{
name: 'English',
value: 'en',
},
{
name: 'Portuguese (Brazil)',
value: 'pt-BR',
},
{
name: 'Spanish',
value: 'es',
},
],
default: 'en',
description: 'Language for the document interface',
},
],
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 50,
displayOptions: {
show: {
resource: ['document', 'template', 'webhook'],
operation: ['getAll'],
},
},
description: 'Max number of results to return',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'document') {
if (operation === 'create') {
// Create document
const name = this.getNodeParameter('name', i) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
// For file upload, we need to use form-data
const options: IRequestOptions = {
method: 'POST',
url: '/v1/documents',
formData: {
name,
file: {
value: Buffer.from(binaryData.data, 'base64'),
options: {
filename: binaryData.fileName || 'document.pdf',
contentType: binaryData.mimeType || 'application/pdf',
},
},
...additionalFields,
},
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'get') {
// Get document
const documentId = this.getNodeParameter('documentId', i) as string;
const options: IRequestOptions = {
method: 'GET',
url: `/v1/documents/${documentId}`,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'getAll') {
// Get all documents
const limit = this.getNodeParameter('limit', i) as number;
const options: IRequestOptions = {
method: 'GET',
url: '/v1/documents',
qs: {
limit,
},
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
// Handle pagination if API returns array or paginated response
if (Array.isArray(responseData)) {
returnData.push(...responseData);
} else {
returnData.push(responseData);
}
} else if (operation === 'send') {
// Send document for signature
const documentId = this.getNodeParameter('documentId', i) as string;
const options: IRequestOptions = {
method: 'POST',
url: `/v1/documents/${documentId}/send`,
body: {},
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'cancel') {
// Cancel document
const documentId = this.getNodeParameter('documentId', i) as string;
const options: IRequestOptions = {
method: 'POST',
url: `/v1/documents/${documentId}/cancel`,
body: {},
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'download') {
// Download signed document
const documentId = this.getNodeParameter('documentId', i) as string;
const options: IRequestOptions = {
method: 'GET',
url: `/v1/documents/${documentId}/download`,
encoding: null, // Get binary data
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
// Convert to binary data
const binaryData = await this.helpers.prepareBinaryData(
responseData as Buffer,
`document-${documentId}.pdf`,
'application/pdf',
);
returnData.push({
json: { documentId, downloaded: true },
binary: {
data: binaryData,
},
});
}
} else if (resource === 'signer') {
const documentId = this.getNodeParameter('documentId', i) as string;
if (operation === 'add') {
// Add signer
const signerEmail = this.getNodeParameter('signerEmail', i) as string;
const signerName = this.getNodeParameter('signerName', i) as string;
const authMethod = this.getNodeParameter('authMethod', i) as string;
const phoneNumber = this.getNodeParameter('phoneNumber', i, '') as string;
const requireDocAuth = this.getNodeParameter('requireDocAuth', i) as boolean;
const requireFacialRecognition = this.getNodeParameter('requireFacialRecognition', i) as boolean;
const body: IDataObject = {
email: signerEmail,
name: signerName,
auth_method: authMethod,
require_doc_auth: requireDocAuth,
require_facial_recognition: requireFacialRecognition,
};
if (phoneNumber) {
body.phone_number = phoneNumber;
}
const options: IRequestOptions = {
method: 'POST',
url: `/v1/documents/${documentId}/signers`,
body,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'getAll') {
// Get all signers
const options: IRequestOptions = {
method: 'GET',
url: `/v1/documents/${documentId}/signers`,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
if (Array.isArray(responseData)) {
returnData.push(...responseData);
} else {
returnData.push(responseData);
}
} else if (operation === 'remove') {
// Remove signer
const signerEmail = this.getNodeParameter('signerEmail', i) as string;
const options: IRequestOptions = {
method: 'DELETE',
url: `/v1/documents/${documentId}/signers/${encodeURIComponent(signerEmail)}`,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'update') {
// Update signer
const signerEmail = this.getNodeParameter('signerEmail', i) as string;
const signerName = this.getNodeParameter('signerName', i) as string;
const authMethod = this.getNodeParameter('authMethod', i) as string;
const phoneNumber = this.getNodeParameter('phoneNumber', i, '') as string;
const requireDocAuth = this.getNodeParameter('requireDocAuth', i) as boolean;
const requireFacialRecognition = this.getNodeParameter('requireFacialRecognition', i) as boolean;
const body: IDataObject = {
name: signerName,
auth_method: authMethod,
require_doc_auth: requireDocAuth,
require_facial_recognition: requireFacialRecognition,
};
if (phoneNumber) {
body.phone_number = phoneNumber;
}
const options: IRequestOptions = {
method: 'PUT',
url: `/v1/documents/${documentId}/signers/${encodeURIComponent(signerEmail)}`,
body,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
}
} else if (resource === 'template') {
if (operation === 'getAll') {
// Get all templates
const limit = this.getNodeParameter('limit', i) as number;
const options: IRequestOptions = {
method: 'GET',
url: '/v1/templates',
qs: {
limit,
},
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
if (Array.isArray(responseData)) {
returnData.push(...responseData);
} else {
returnData.push(responseData);
}
} else if (operation === 'createDocument') {
// Create document from template
const templateId = this.getNodeParameter('templateId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const body: IDataObject = {
name,
template_id: templateId,
};
const options: IRequestOptions = {
method: 'POST',
url: '/v1/documents/from-template',
body,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
}
} else if (resource === 'webhook') {
if (operation === 'create') {
// Create webhook
const webhookUrl = this.getNodeParameter('webhookUrl', i) as string;
const events = this.getNodeParameter('events', i) as string[];
const body: IDataObject = {
url: webhookUrl,
events,
};
const options: IRequestOptions = {
method: 'POST',
url: '/v1/webhooks',
body,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
} else if (operation === 'getAll') {
// Get all webhooks
const limit = this.getNodeParameter('limit', i) as number;
const options: IRequestOptions = {
method: 'GET',
url: '/v1/webhooks',
qs: {
limit,
},
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
if (Array.isArray(responseData)) {
returnData.push(...responseData);
} else {
returnData.push(responseData);
}
} else if (operation === 'delete') {
// Delete webhook
const webhookId = this.getNodeParameter('webhookId', i) as string;
const options: IRequestOptions = {
method: 'DELETE',
url: `/v1/webhooks/${webhookId}`,
};
const responseData = await this.helpers.requestWithAuthentication.call(
this,
'zapSignApi',
options,
);
returnData.push(responseData);
}
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
error: error.message,
json: {},
});
continue;
}
throw new NodeOperationError(this.getNode(), error, {
itemIndex: i,
});
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

26
nodes/ZapSign/zapsign.svg Normal file
View file

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4F46E5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#7C3AED;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Document background -->
<rect x="12" y="8" width="40" height="48" rx="4" ry="4" fill="#ffffff" stroke="url(#grad)" stroke-width="2"/>
<!-- Document lines -->
<line x1="20" y1="20" x2="44" y2="20" stroke="#E5E7EB" stroke-width="1"/>
<line x1="20" y1="26" x2="44" y2="26" stroke="#E5E7EB" stroke-width="1"/>
<line x1="20" y1="32" x2="38" y2="32" stroke="#E5E7EB" stroke-width="1"/>
<!-- Signature line -->
<line x1="20" y1="42" x2="44" y2="42" stroke="#D1D5DB" stroke-width="1" stroke-dasharray="2,2"/>
<!-- Signature/pen icon -->
<path d="M26 38 L32 44 L42 34 L40 32 Z" fill="url(#grad)"/>
<circle cx="43" cy="33" r="2" fill="url(#grad)"/>
<!-- Lightning bolt for "zap" -->
<path d="M48 16 L46 20 L48 20 L46 24 L50 20 L48 20 Z" fill="#F59E0B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,307 @@
import type {
IHookFunctions,
IWebhookFunctions,
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
export class ZapSignTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'ZapSign Trigger',
name: 'zapSignTrigger',
icon: 'file:zapsign.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when ZapSign events occur',
defaults: {
name: 'ZapSign Trigger',
},
inputs: [],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'zapSignApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: 'Document Created',
value: 'document.created',
description: 'Triggered when a document is created',
},
{
name: 'Document Sent',
value: 'document.sent',
description: 'Triggered when a document is sent for signature',
},
{
name: 'Document Viewed',
value: 'document.viewed',
description: 'Triggered when a document is viewed by a signer',
},
{
name: 'Document Signed',
value: 'document.signed',
description: 'Triggered when a document is signed by any signer',
},
{
name: 'Document Completed',
value: 'document.completed',
description: 'Triggered when all signers have signed the document',
},
{
name: 'Document Cancelled',
value: 'document.cancelled',
description: 'Triggered when a document is cancelled',
},
{
name: 'Document Expired',
value: 'document.expired',
description: 'Triggered when a document expires',
},
{
name: 'Signer Added',
value: 'signer.added',
description: 'Triggered when a signer is added to a document',
},
{
name: 'Signer Signed',
value: 'signer.signed',
description: 'Triggered when a specific signer signs',
},
{
name: 'Signer Declined',
value: 'signer.declined',
description: 'Triggered when a signer declines to sign',
},
],
required: true,
default: ['document.completed'],
description: 'The events to listen for',
},
{
displayName: 'Document Filter',
name: 'documentFilter',
type: 'collection',
placeholder: 'Add Filter',
default: {},
options: [
{
displayName: 'Document ID',
name: 'documentId',
type: 'string',
default: '',
description: 'Only trigger for specific document ID',
},
{
displayName: 'Signer Email',
name: 'signerEmail',
type: 'string',
default: '',
description: 'Only trigger for specific signer email',
},
{
displayName: 'Brand ID',
name: 'brandId',
type: 'string',
default: '',
description: 'Only trigger for specific brand ID',
},
],
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const credentials = await this.getCredentials('zapSignApi');
const baseUrl = credentials.environment === 'sandbox'
? 'https://sandbox.api.zapsign.com'
: 'https://api.zapsign.com';
try {
const response = await this.helpers.request({
method: 'GET',
url: `${baseUrl}/v1/webhooks`,
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
},
});
// Check if webhook with this URL already exists
const webhooks = Array.isArray(response) ? response : response.data || [];
for (const webhook of webhooks) {
if (webhook.url === webhookUrl) {
return true;
}
}
} catch (error) {
return false;
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const events = this.getNodeParameter('events') as string[];
const credentials = await this.getCredentials('zapSignApi');
const baseUrl = credentials.environment === 'sandbox'
? 'https://sandbox.api.zapsign.com'
: 'https://api.zapsign.com';
const body = {
url: webhookUrl,
events,
name: `n8n-webhook-${Date.now()}`,
};
try {
const responseData = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/v1/webhooks`,
body,
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
},
});
if (responseData.id === undefined) {
// We did not get back the ID which we need to delete the webhook
return false;
}
webhookData.webhookId = responseData.id as string;
return true;
} catch (error) {
return false;
}
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const credentials = await this.getCredentials('zapSignApi');
const baseUrl = credentials.environment === 'sandbox'
? 'https://sandbox.api.zapsign.com'
: 'https://api.zapsign.com';
try {
await this.helpers.request({
method: 'DELETE',
url: `${baseUrl}/v1/webhooks/${webhookData.webhookId}`,
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
},
});
} catch (error) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registered anymore
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
const events = this.getNodeParameter('events') as string[];
const documentFilter = this.getNodeParameter('documentFilter') as IDataObject;
// Validate that this is a ZapSign webhook
if (!bodyData.event || !bodyData.data) {
return {
webhookResponse: {
status: 400,
body: 'Invalid webhook payload',
},
};
}
const eventType = bodyData.event as string;
// Check if we should handle this event
if (!events.includes(eventType)) {
return {
webhookResponse: {
status: 200,
body: 'Event type not configured',
},
};
}
// Apply filters if configured
const data = bodyData.data as IDataObject;
if (documentFilter.documentId && (data.document as IDataObject)?.id !== documentFilter.documentId) {
return {
webhookResponse: {
status: 200,
body: 'Document ID filter not matched',
},
};
}
if (documentFilter.signerEmail && (data.signer as IDataObject)?.email !== documentFilter.signerEmail) {
return {
webhookResponse: {
status: 200,
body: 'Signer email filter not matched',
},
};
}
if (documentFilter.brandId && (data.document as IDataObject)?.brand_id !== documentFilter.brandId) {
return {
webhookResponse: {
status: 200,
body: 'Brand ID filter not matched',
},
};
}
// Enrich the payload with additional metadata
const enrichedData = {
...bodyData,
metadata: {
event_type: eventType,
received_at: new Date().toISOString(),
webhook_source: 'zapsign',
},
};
return {
workflowData: [this.helpers.returnJsonArray([enrichedData])],
};
}
}

View file

@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4F46E5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#7C3AED;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Document background -->
<rect x="12" y="8" width="40" height="48" rx="4" ry="4" fill="#ffffff" stroke="url(#grad)" stroke-width="2"/>
<!-- Document lines -->
<line x1="20" y1="20" x2="44" y2="20" stroke="#E5E7EB" stroke-width="1"/>
<line x1="20" y1="26" x2="44" y2="26" stroke="#E5E7EB" stroke-width="1"/>
<line x1="20" y1="32" x2="38" y2="32" stroke="#E5E7EB" stroke-width="1"/>
<!-- Signature line -->
<line x1="20" y1="42" x2="44" y2="42" stroke="#D1D5DB" stroke-width="1" stroke-dasharray="2,2"/>
<!-- Signature/pen icon -->
<path d="M26 38 L32 44 L42 34 L40 32 Z" fill="url(#grad)"/>
<circle cx="43" cy="33" r="2" fill="url(#grad)"/>
<!-- Lightning bolt for "zap" -->
<path d="M48 16 L46 20 L48 20 L46 24 L50 20 L48 20 Z" fill="#F59E0B"/>
<!-- Webhook indicator (small antenna icon) -->
<circle cx="56" cy="48" r="2" fill="#10B981"/>
<path d="M54 46 L58 46 M56 44 L56 50" stroke="#10B981" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB