Documentation

📦 Guia: Criar Community Node n8n para Corp-API

Este guia explica passo a passo como criar um n8n Community Node que expõe todos os endpoints da Corp-API como nodes visuais no n8n.


📋 Sumário

  1. Pré-requisitos
  2. Estrutura do Projeto
  3. Step-by-Step — Criação
  4. Credential (Autenticação)
  5. Nodes da API
  6. Webhook Trigger Node
  7. Build & Teste Local
  8. Publicar no npm
  9. Instalar no n8n

1. Pré-requisitos

# Node.js 18+
node --version

# n8n instalado globalmente (para teste)
npm install -g n8n

# Conta no npmjs.com (para publicar o pacote)

2. Estrutura do Projeto

Crie o projeto fora do repositório corp-api — é um pacote npm independente.

n8n-nodes-corp-api/
├── .gitignore
├── .npmignore
├── package.json
├── tsconfig.json
├── README.md
├── credentials/
│   └── CorpApi.credentials.ts    # Autenticação MASTER_TOKEN
├── nodes/
│   ├── CorpApiTrigger/
│   │   ├── CorpApiTrigger.node.ts    # Webhook Trigger (recebe eventos)
│   │   └── corp-api-logo.svg
│   ├── SendMessage/
│   │   └── SendMessage.node.ts       # Enviar texto/mídia/localização
│   ├── ManageInstance/
│   │   └── ManageInstance.node.ts    # Conectar/desconectar/status/QR
│   ├── ManageGroup/
│   │   └── ManageGroup.node.ts       # Listar/atualizar grupos
│   └── VoipCall/
│       └── VoipCall.node.ts          # Chamadas VoIP
└── utils/
    └── api.ts                        # Helper HTTP (chamadas à Corp-API)

3. Step-by-Step

3.1 Criar o projeto

mkdir n8n-nodes-corp-api
cd n8n-nodes-corp-api
npm init -y

3.2 package.json

{
  "name": "n8n-nodes-corp-api",
  "version": "1.0.0",
  "description": "n8n Community Nodes for Corp-API — WhatsApp Multi-Session + VoIP automation",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "prepublishOnly": "npm run build"
  },
  "files": [
    "dist/"
  ],
  "n8n": {
    "n8nNodesApiVersion": 1,
    "nodes": [
      "dist/nodes/CorpApiTrigger/CorpApiTrigger.node.js",
      "dist/nodes/SendMessage/SendMessage.node.js",
      "dist/nodes/ManageInstance/ManageInstance.node.js",
      "dist/nodes/ManageGroup/ManageGroup.node.js",
      "dist/nodes/VoipCall/VoipCall.node.js"
    ],
    "credentials": [
      "dist/credentials/CorpApi.credentials.js"
    ]
  },
  "dependencies": {
    "n8n-workflow": "^1.50.0"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "@types/node": "^22.0.0"
  },
  "keywords": ["n8n-community-node-package", "whatsapp", "voip", "corp-api", "zapo"],
  "license": "MIT"
}

IMPORTANTE: O campo n8n.nodes e n8n.credentials são obrigatórios — é assim que o n8n descobre seus nodes.

3.3 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2021",
    "module": "commonjs",
    "lib": ["ES2021"],
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "resolveJsonModule": true
  },
  "include": ["credentials/**/*.ts", "nodes/**/*.ts", "utils/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

4. Credential

Arquivo: credentials/CorpApi.credentials.ts

import {
  IAuthenticateGeneric,
  ICredentialTestRequest,
  ICredentialType,
  INodeProperties,
} from 'n8n-workflow';

export class CorpApi implements ICredentialType {
  name = 'corpApi';
  displayName = 'Corp-API (WhatsApp)';
  documentationUrl = 'https://github.com/kabaweb/corp-api';
  
  properties: INodeProperties[] = [
    {
      displayName: 'API Base URL',
      name: 'baseUrl',
      type: 'string',
      default: 'http://localhost:3000/api/v1',
      description: 'URL da sua Corp-API (ex: https://meu-dominio.com/api/v1)',
      required: true,
    },
    {
      displayName: 'Master Token',
      name: 'masterToken',
      type: 'string',
      typeOptions: { password: true },
      default: '',
      description: 'Token mestre configurado no MASTER_TOKEN do docker-compose.yml',
      required: true,
    },
  ];

  // O n8n usa isso para popular o header Authorization automaticamente
  authenticate: IAuthenticateGeneric = {
    type: 'generic',
    properties: {
      headers: {
        Authorization: '=Bearer {{$credentials.masterToken}}',
      },
    },
  };

  // Teste de conexão: chama /health
  test: ICredentialTestRequest = {
    request: {
      baseURL: '={{$credentials.baseUrl.replace(/\/api\/v1$/, "")}}',
      url: '/health',
      method: 'GET',
    },
  };
}

5. Nodes da API

5.1 Helper HTTP compartilhado

Arquivo: utils/api.ts

import { IExecuteFunctions, IHttpRequestOptions } from 'n8n-workflow';

export async function corpApiRequest(
  thisAction: IExecuteFunctions,
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
  endpoint: string,
  body?: Record<string, unknown>,
): Promise<any> {
  const credentials = await thisAction.getCredentials('corpApi');
  const baseUrl = (credentials.baseUrl as string).replace(/\/$/, '');

  const options: IHttpRequestOptions = {
    method,
    url: `${baseUrl}${endpoint}`,
    headers: {
      Authorization: `Bearer ${credentials.masterToken}`,
      'Content-Type': 'application/json',
    },
    json: true,
  };

  if (body) {
    options.body = body;
  }

  return thisAction.helpers.httpRequest(options);
}

5.2 Node: SendMessage

Arquivo: nodes/SendMessage/SendMessage.node.ts

import {
  IExecuteFunctions,
  INodeExecutionData,
  INodeType,
  INodeTypeDescription,
} from 'n8n-workflow';
import { corpApiRequest } from '../../utils/api';

export class SendMessage implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'Corp-API Send Message',
    name: 'sendMessage',
    icon: 'fa:paper-plane',
    group: ['transform'],
    version: 1,
    description: 'Envia mensagens via WhatsApp (texto, mídia, localização, reação)',
    defaults: { name: 'Send WhatsApp Message' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'corpApi', required: true }],
    properties: [
      {
        displayName: 'Instance ID',
        name: 'instanceId',
        type: 'string',
        default: '',
        required: true,
        description: 'ID da instância configurada na Corp-API',
      },
      {
        displayName: 'Message Type',
        name: 'messageType',
        type: 'options',
        options: [
          { name: 'Text', value: 'text' },
          { name: 'Media (Image/Video/Document)', value: 'media' },
          { name: 'Location', value: 'location' },
          { name: 'Reaction', value: 'reaction' },
        ],
        default: 'text',
        required: true,
      },
      {
        displayName: 'To (Phone Number)',
        name: 'to',
        type: 'string',
        default: '',
        required: true,
        description: 'Número do destinatário (ex: 5581999999999)',
        displayOptions: { show: { messageType: ['text', 'media', 'location', 'reaction'] } },
      },
      // --- Text ---
      {
        displayName: 'Text',
        name: 'text',
        type: 'string',
        typeOptions: { rows: 3 },
        default: '',
        required: true,
        displayOptions: { show: { messageType: ['text'] } },
      },
      // --- Media ---
      {
        displayName: 'Media Type',
        name: 'mediaType',
        type: 'options',
        options: [
          { name: 'Image', value: 'image' },
          { name: 'Video', value: 'video' },
          { name: 'Document', value: 'document' },
        ],
        default: 'image',
        displayOptions: { show: { messageType: ['media'] } },
      },
      {
        displayName: 'Media Base64',
        name: 'base64',
        type: 'string',
        default: '',
        required: true,
        description: 'Arquivo em Base64',
        displayOptions: { show: { messageType: ['media'] } },
      },
      // --- Location ---
      {
        displayName: 'Latitude',
        name: 'latitude',
        type: 'number',
        default: 0,
        required: true,
        displayOptions: { show: { messageType: ['location'] } },
      },
      {
        displayName: 'Longitude',
        name: 'longitude',
        type: 'number',
        default: 0,
        required: true,
        displayOptions: { show: { messageType: ['location'] } },
      },
      // --- Reaction ---
      {
        displayName: 'Message ID',
        name: 'messageId',
        type: 'string',
        default: '',
        required: true,
        description: 'ID da mensagem original',
        displayOptions: { show: { messageType: ['reaction'] } },
      },
      {
        displayName: 'Emoji',
        name: 'emoji',
        type: 'string',
        default: '👍',
        required: true,
        displayOptions: { show: { messageType: ['reaction'] } },
      },
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    for (let i = 0; i < items.length; i++) {
      const instanceId = this.getNodeParameter('instanceId', i) as string;
      const messageType = this.getNodeParameter('messageType', i) as string;
      const to = this.getNodeParameter('to', i) as string;

      let endpoint: string;
      let body: Record<string, unknown>;

      switch (messageType) {
        case 'text':
          endpoint = `/${instanceId}/message/send-text`;
          body = { to, text: this.getNodeParameter('text', i) };
          break;
        case 'media':
          endpoint = `/${instanceId}/message/send-media`;
          body = {
            to,
            type: this.getNodeParameter('mediaType', i),
            base64: this.getNodeParameter('base64', i),
          };
          break;
        case 'location':
          endpoint = `/${instanceId}/message/send-location`;
          body = {
            to,
            latitude: this.getNodeParameter('latitude', i),
            longitude: this.getNodeParameter('longitude', i),
          };
          break;
        case 'reaction':
          endpoint = `/${instanceId}/message/react`;
          body = {
            to,
            messageId: this.getNodeParameter('messageId', i),
            emoji: this.getNodeParameter('emoji', i),
          };
          break;
        default:
          throw new Error(`Tipo de mensagem inválido: ${messageType}`);
      }

      const response = await corpApiRequest.call(this, 'POST', endpoint, body);
      returnData.push({ json: response });
    }

    return [returnData];
  }
}

5.3 Node: ManageInstance

Arquivo: nodes/ManageInstance/ManageInstance.node.ts

import {
  IExecuteFunctions,
  INodeExecutionData,
  INodeType,
  INodeTypeDescription,
} from 'n8n-workflow';
import { corpApiRequest } from '../../utils/api';

export class ManageInstance implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'Corp-API Manage Instance',
    name: 'manageInstance',
    icon: 'fa:mobile-alt',
    group: ['transform'],
    version: 1,
    description: 'Gerencia instâncias WhatsApp: conectar, desconectar, status, QR code',
    defaults: { name: 'Manage Instance' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'corpApi', required: true }],
    properties: [
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        options: [
          { name: 'Create Instance', value: 'create' },
          { name: 'Connect (Generate QR)', value: 'connect' },
          { name: 'Disconnect', value: 'disconnect' },
          { name: 'Restart', value: 'restart' },
          { name: 'Get QR Code', value: 'qrcode' },
          { name: 'Get Status', value: 'status' },
          { name: 'List Instances', value: 'list' },
          { name: 'Delete Instance', value: 'delete' },
        ],
        default: 'status',
        required: true,
      },
      {
        displayName: 'Instance ID',
        name: 'instanceId',
        type: 'string',
        default: '',
        required: true,
        description: 'ID da instância',
        displayOptions: { show: { operation: ['connect', 'disconnect', 'restart', 'qrcode', 'status', 'delete'] } },
      },
      {
        displayName: 'Instance ID',
        name: 'instanceIdCreate',
        type: 'string',
        default: '',
        required: true,
        description: 'ID único da nova instância',
        displayOptions: { show: { operation: ['create'] } },
      },
      {
        displayName: 'Instance Name',
        name: 'name',
        type: 'string',
        default: '',
        required: true,
        description: 'Nome descritivo da instância',
        displayOptions: { show: { operation: ['create'] } },
      },
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    for (let i = 0; i < items.length; i++) {
      const operation = this.getNodeParameter('operation', i) as string;
      let endpoint: string;
      let method: 'GET' | 'POST' | 'DELETE' = 'POST';
      let body: Record<string, unknown> | undefined;

      switch (operation) {
        case 'create':
          endpoint = '/instances';
          body = {
            id: this.getNodeParameter('instanceIdCreate', i),
            name: this.getNodeParameter('name', i),
          };
          break;
        case 'list':
          endpoint = '/instances';
          method = 'GET';
          break;
        case 'delete':
          endpoint = `/instances/${this.getNodeParameter('instanceId', i)}`;
          method = 'DELETE';
          break;
        case 'connect':
          endpoint = `/${this.getNodeParameter('instanceId', i)}/connect`;
          break;
        case 'disconnect':
          endpoint = `/${this.getNodeParameter('instanceId', i)}/disconnect`;
          break;
        case 'restart':
          endpoint = `/${this.getNodeParameter('instanceId', i)}/restart`;
          break;
        case 'qrcode':
          endpoint = `/${this.getNodeParameter('instanceId', i)}/qrcode`;
          method = 'GET';
          break;
        case 'status':
          endpoint = `/${this.getNodeParameter('instanceId', i)}/status`;
          method = 'GET';
          break;
        default:
          throw new Error(`Operação inválida: ${operation}`);
      }

      const response = await corpApiRequest.call(this, method, endpoint, body);
      returnData.push({ json: response });
    }

    return [returnData];
  }
}

5.4 Node: VoipCall

Arquivo: nodes/VoipCall/VoipCall.node.ts

import {
  IExecuteFunctions,
  INodeExecutionData,
  INodeType,
  INodeTypeDescription,
} from 'n8n-workflow';
import { corpApiRequest } from '../../utils/api';

export class VoipCall implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'Corp-API VoIP Call',
    name: 'voipCall',
    icon: 'fa:phone',
    group: ['transform'],
    version: 1,
    description: 'Gerencia chamadas VoIP via WhatsApp',
    defaults: { name: 'VoIP Call' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'corpApi', required: true }],
    properties: [
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        options: [
          { name: 'Make Call', value: 'call' },
          { name: 'Hang Up', value: 'hangup' },
          { name: 'Accept Call', value: 'accept' },
          { name: 'Reject Call', value: 'reject' },
          { name: 'Mute', value: 'mute' },
          { name: 'Unmute', value: 'unmute' },
          { name: 'Hold', value: 'hold' },
          { name: 'Resume', value: 'resume' },
          { name: 'List Active Calls', value: 'listCalls' },
          { name: 'Get Call Details', value: 'getCall' },
          { name: 'List Recordings', value: 'listRecordings' },
          { name: 'Get Recording', value: 'getRecording' },
        ],
        default: 'call',
        required: true,
      },
      {
        displayName: 'Instance ID',
        name: 'instanceId',
        type: 'string',
        default: '',
        required: true,
      },
      {
        displayName: 'Phone Number',
        name: 'to',
        type: 'string',
        default: '',
        required: true,
        description: 'Número para ligar (ex: 5581999999999)',
        displayOptions: { show: { operation: ['call'] } },
      },
      {
        displayName: 'Audio (Base64 WAV)',
        name: 'audioBase64',
        type: 'string',
        default: '',
        description: 'Áudio WAV/PCM em Base64 para tocar na chamada (opcional)',
        displayOptions: { show: { operation: ['call'] } },
      },
      {
        displayName: 'Call ID',
        name: 'callId',
        type: 'string',
        default: '',
        required: true,
        description: 'ID da chamada',
        displayOptions: { show: { operation: ['hangup', 'accept', 'reject', 'mute', 'unmute', 'hold', 'resume', 'getCall', 'getRecording'] } },
      },
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    for (let i = 0; i < items.length; i++) {
      const instanceId = this.getNodeParameter('instanceId', i) as string;
      const operation = this.getNodeParameter('operation', i) as string;
      let endpoint: string;
      let method: 'GET' | 'POST' = 'POST';
      let body: Record<string, unknown> | undefined;

      switch (operation) {
        case 'call':
          endpoint = `/${instanceId}/voip/call`;
          body = { to: this.getNodeParameter('to', i) };
          const audio = this.getNodeParameter('audioBase64', i, '') as string;
          if (audio) body.audioBase64 = audio;
          break;
        case 'hangup':
          endpoint = `/${instanceId}/voip/hangup`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'accept':
          endpoint = `/${instanceId}/voip/accept`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'reject':
          endpoint = `/${instanceId}/voip/reject`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'mute':
          endpoint = `/${instanceId}/voip/mute`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'unmute':
          endpoint = `/${instanceId}/voip/unmute`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'hold':
          endpoint = `/${instanceId}/voip/hold`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'resume':
          endpoint = `/${instanceId}/voip/resume`;
          body = { callId: this.getNodeParameter('callId', i) };
          break;
        case 'listCalls':
          endpoint = `/${instanceId}/voip/calls`;
          method = 'GET';
          break;
        case 'getCall':
          endpoint = `/${instanceId}/voip/calls/${this.getNodeParameter('callId', i)}`;
          method = 'GET';
          break;
        case 'listRecordings':
          endpoint = `/${instanceId}/voip/recordings`;
          method = 'GET';
          break;
        case 'getRecording':
          endpoint = `/${instanceId}/voip/recordings/${this.getNodeParameter('callId', i)}`;
          method = 'GET';
          break;
        default:
          throw new Error(`Operação inválida: ${operation}`);
      }

      const response = await corpApiRequest.call(this, method, endpoint, body);
      returnData.push({ json: response });
    }

    return [returnData];
  }
}

5.5 Node: ManageGroup

Arquivo: nodes/ManageGroup/ManageGroup.node.ts

import {
  IExecuteFunctions,
  INodeExecutionData,
  INodeType,
  INodeTypeDescription,
} from 'n8n-workflow';
import { corpApiRequest } from '../../utils/api';

export class ManageGroup implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'Corp-API Manage Groups',
    name: 'manageGroup',
    icon: 'fa:users',
    group: ['transform'],
    version: 1,
    description: 'Lista e atualiza grupos do WhatsApp',
    defaults: { name: 'Manage Groups' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'corpApi', required: true }],
    properties: [
      {
        displayName: 'Instance ID',
        name: 'instanceId',
        type: 'string',
        default: '',
        required: true,
      },
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        options: [
          { name: 'List Groups', value: 'list' },
          { name: 'Refresh Groups', value: 'refresh' },
        ],
        default: 'list',
        required: true,
      },
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    for (let i = 0; i < items.length; i++) {
      const instanceId = this.getNodeParameter('instanceId', i) as string;
      const operation = this.getNodeParameter('operation', i) as string;

      let endpoint: string;
      let method: 'GET' | 'POST' = 'GET';

      if (operation === 'refresh') {
        endpoint = `/${instanceId}/groups/refresh`;
        method = 'POST';
      } else {
        endpoint = `/${instanceId}/groups`;
      }

      const response = await corpApiRequest.call(this, method, endpoint);
      returnData.push({ json: response });
    }

    return [returnData];
  }
}

6. Webhook Trigger Node

Arquivo: nodes/CorpApiTrigger/CorpApiTrigger.node.ts

Este é o node que recebe eventos da Corp-API (funciona como trigger no n8n).

import {
  IWebhookFunctions,
  IWebhookResponseData,
  INodeType,
  INodeTypeDescription,
  IWebhookDescription,
} from 'n8n-workflow';

export class CorpApiTrigger implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'Corp-API Trigger',
    name: 'corpApiTrigger',
    icon: 'fa:bolt',
    group: ['trigger'],
    version: 1,
    description: 'Recebe eventos de webhook da Corp-API (mensagens, chamadas, conexões etc.)',
    defaults: { name: 'Corp-API Trigger' },
    inputs: [],
    outputs: ['main'],
    credentials: [{ name: 'corpApi', required: true }],
    webhooks: [
      {
        name: 'default',
        httpMethod: 'POST',
        responseMode: 'onReceived',
        path: 'webhook', // URL: https://<n8n-host>/webhook/<path>
      },
    ],
    properties: [],
  };

  async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
    // O body enviado pela Corp-API:
    // { event: "message", instanceId: "inst_01", timestamp: "...", data: {...} }
    const bodyData = this.getBodyData();
    
    return {
      workflowData: [[{ json: bodyData }]],
    };
  }
}

Como usar no fluxo n8n:

  1. Adicione o node "Corp-API Trigger" ao workflow
  2. Ele gera uma URL de webhook (ex: https://n8n.meu-dominio.com/webhook/abc123)
  3. Use o endpoint POST /api/v1/webhooks da Corp-API para registrar essa URL
  4. Todo evento do WhatsApp será recebido pelo n8n automaticamente

7. Build & Teste Local

7.1 Compilar

cd n8n-nodes-corp-api
npm install
npm run build

Isso gera os arquivos compilados em dist/.

7.2 Testar no n8n local

Opção A — Instalar direto do diretório local:

# Instalar o pacote local no n8n global
npm install -g /caminho/absoluto/n8n-nodes-corp-api

# Rodar n8n
n8n start

Opção B — Link simbólico (desenvolvimento):

cd ~/.n8n
mkdir -p custom
cd custom
npm init -y
npm install /caminho/absoluto/n8n-nodes-corp-api

# Configurar n8n para carregar nodes customizados:
export N8N_CUSTOM_EXTENSIONS=~/.n8n/custom

n8n start

Opção C — Docker (recomendado para produção):

docker run -d \
  --name n8n \
  -p 5678:5678 \
  -v ~/.n8n:/home/node/.n8n \
  -e N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true \
  n8nio/n8n

Depois, no n8n, vá em Settings → Community Nodes → Install e digite n8n-nodes-corp-api.


8. Publicar no npm

cd n8n-nodes-corp-api

# 1. Fazer login no npm
npm login

# 2. Verificar o que será publicado (simulação)
npm pack --dry-run

# 3. Publicar
npm publish --access public

Após a publicação, o pacote estará disponível em:

https://www.npmjs.com/package/n8n-nodes-corp-api

9. Instalar no n8n

9.1 Via Community Nodes (interface do n8n)

  1. Acesse Settings → Community Nodes
  2. Clique em Install a Community Node
  3. Digite o nome do pacote: n8n-nodes-corp-api
  4. Marque I understand the risks... e clique Install
  5. Reinicie o n8n

9.2 Via Docker Compose (produção)

# docker-compose.yml do n8n
services:
  n8n:
    image: n8nio/n8n
    ports:
      - "5678:5678"
    environment:
      - N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
      # Pré-instala o pacote no startup
      - N8N_COMMUNITY_PACKAGES_ENABLED=true
    volumes:
      - n8n_data:/home/node/.n8n
    # Instala o pacote no entrypoint
    entrypoint: ["/bin/sh", "-c", "npm install -g n8n-nodes-corp-api && n8n start"]

9.3 Via Kubernetes / Helm

helm upgrade --install n8n n8n/n8n \
  --set env.N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true \
  --set extraPackages[0]="n8n-nodes-corp-api"

🔧 Troubleshooting

Problema Solução
"Node not found" no n8n Verifique se o campo n8n.nodes no package.json tem caminhos corretos (relativos ao dist/)
"Credential not found" Verifique n8n.credentials no package.json
Erro de CORS ao chamar a API Ajuste CORS_ORIGINS no docker-compose da Corp-API para incluir o domínio do n8n
n8n não puxa nova versão Clear cache: rm -rf ~/.n8n/cache e reinicie
Webhook não recebe eventos Verifique se o endpoint da Corp-API está acessível do n8n (rede/firewall)

📚 Referências


📦 Resumo Rápido

# 1. Criar projeto
mkdir n8n-nodes-corp-api && cd n8n-nodes-corp-api
npm init -y

# 2. Copie os arquivos deste guia para:
#    credentials/CorpApi.credentials.ts
#    utils/api.ts
#    nodes/SendMessage/SendMessage.node.ts
#    nodes/ManageInstance/ManageInstance.node.ts
#    nodes/VoipCall/VoipCall.node.ts
#    nodes/ManageGroup/ManageGroup.node.ts
#    nodes/CorpApiTrigger/CorpApiTrigger.node.ts

# 3. Build & Publicar
npm install
npm run build
npm login
npm publish --access public

# 4. No n8n:
#    Settings → Community Nodes → Install "n8n-nodes-corp-api"

Discussion