@seranking/n8n-nodes-seranking 1.5.9 → 1.5.12

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 CHANGED
@@ -89,10 +89,9 @@ Open `http://localhost:5678` and add the SE Ranking node to your workflow.
89
89
  ### Step 4: Configure Credentials
90
90
 
91
91
  1. Add SE Ranking node
92
- 2. Click "Create New Credential"
93
- 3. Enter your **Data API Token** (required)
94
- 4. Enter your **Project API Token** (optional — only if using Project API resources)
95
- 5. Save
92
+ 2. Click "Create New Credential" for **SE Ranking API** (Data API) — enter your Data API token
93
+ 3. (Optional) Click "Create New Credential" for **SE Ranking Project API** — enter your Project API token
94
+ 4. Save
96
95
 
97
96
  ### Docker Installation
98
97
 
@@ -163,14 +162,13 @@ To use this node, you need:
163
162
  ### Setting up credentials in n8n
164
163
 
165
164
  1. Open any workflow and add the **SE Ranking** node
166
- 2. Click on **Create New Credential**
167
- 3. Enter your **API Token (Data API)** — required
168
- 4. Enter your **API Token (Project API)** — optional, only needed for Project API resources
169
- 5. Click **Save**
165
+ 2. In the **SE Ranking API** credential slot, click "Create New Credential" and enter your **Data API token** — required for all Data API resources
166
+ 3. In the **SE Ranking Project API** credential slot, click "Create New Credential" and enter your **Project API token** — only needed for Project API resources
167
+ 4. Click **Save**
170
168
 
171
- The node will automatically test your Data API credentials by making a test request to the SE Ranking API.
169
+ Each credential is tested independently when saved. The node automatically routes requests to the correct API and credential based on the resource you select.
172
170
 
173
- > **Note:** Data API and Project API use different tokens. Data API tokens are UUID format; Project API tokens are 40-character hex format. Get both from your [SE Ranking API Dashboard](https://online.seranking.com/admin.api.dashboard.html). The node automatically routes to the correct API based on the resource you select.
171
+ > **Note:** Data API and Project API use different tokens. Data API tokens are UUID format; Project API tokens are 40-character hex format. Get both from your [SE Ranking API Dashboard](https://online.seranking.com/admin.api.dashboard.html).
174
172
 
175
173
  ---
176
174
 
@@ -880,7 +878,7 @@ For more details, see [n8n's rate limiting documentation](https://docs.n8n.io/in
880
878
  **Solution**:
881
879
 
882
880
  1. Verify API token is correct (copy from SE Ranking dashboard)
883
- 2. For Project API resources, ensure the Project API Token field is filled
881
+ 2. For Project API resources, ensure the SE Ranking Project API credential is configured
884
882
  3. Check token hasn't expired
885
883
  4. Regenerate token in SE Ranking dashboard if needed
886
884
  5. Test credentials using the "Test" button in n8n
@@ -1,8 +1,9 @@
1
- import { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
1
+ import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
2
  export declare class SeRankingApi implements ICredentialType {
3
3
  name: string;
4
4
  displayName: string;
5
5
  documentationUrl: string;
6
6
  properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
7
8
  test: ICredentialTestRequest;
8
9
  }
@@ -8,7 +8,7 @@ class SeRankingApi {
8
8
  this.documentationUrl = 'https://seranking.com/api-google-organic.html';
9
9
  this.properties = [
10
10
  {
11
- displayName: 'API Token (Data API)',
11
+ displayName: 'API Token',
12
12
  name: 'apiToken',
13
13
  type: 'string',
14
14
  typeOptions: {
@@ -16,27 +16,22 @@ class SeRankingApi {
16
16
  },
17
17
  default: '',
18
18
  required: true,
19
- description: 'Token for Data API domain analysis, keywords, backlinks, SERP, etc.',
19
+ description: 'Your SE Ranking Data API token from the API Dashboard',
20
20
  },
21
- {
22
- displayName: 'API Token (Project API)',
23
- name: 'projectApiToken',
24
- type: 'string',
25
- typeOptions: {
26
- password: true,
21
+ ];
22
+ this.authenticate = {
23
+ type: 'generic',
24
+ properties: {
25
+ headers: {
26
+ 'Authorization': '=Token {{$credentials.apiToken}}',
27
27
  },
28
- default: '',
29
- description: 'Token for Project API — project management, competitors, audit, etc. Leave empty if not using Project API resources.',
30
28
  },
31
- ];
29
+ };
32
30
  this.test = {
33
31
  request: {
34
32
  baseURL: 'https://api.seranking.com/v1',
35
33
  url: '/account/subscription',
36
34
  method: 'GET',
37
- headers: {
38
- 'Authorization': '=Token {{$credentials.apiToken}}',
39
- },
40
35
  },
41
36
  };
42
37
  }
@@ -0,0 +1,9 @@
1
+ import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class SeRankingProjectApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SeRankingProjectApi = void 0;
4
+ class SeRankingProjectApi {
5
+ constructor() {
6
+ this.name = 'seRankingProjectApi';
7
+ this.displayName = 'SE Ranking Project API';
8
+ this.documentationUrl = 'https://seranking.com/api-google-organic.html';
9
+ this.properties = [
10
+ {
11
+ displayName: 'API Token',
12
+ name: 'apiToken',
13
+ type: 'string',
14
+ typeOptions: {
15
+ password: true,
16
+ },
17
+ default: '',
18
+ required: true,
19
+ description: 'Your SE Ranking Project API token from the API Dashboard',
20
+ },
21
+ ];
22
+ this.authenticate = {
23
+ type: 'generic',
24
+ properties: {
25
+ headers: {
26
+ 'Authorization': '=Token {{$credentials.apiToken}}',
27
+ },
28
+ },
29
+ };
30
+ this.test = {
31
+ request: {
32
+ baseURL: 'https://api4.seranking.com',
33
+ url: '/sites',
34
+ method: 'GET',
35
+ },
36
+ };
37
+ }
38
+ }
39
+ exports.SeRankingProjectApi = SeRankingProjectApi;
@@ -62,6 +62,42 @@ class SeRanking {
62
62
  {
63
63
  name: 'seRankingApi',
64
64
  required: true,
65
+ displayOptions: {
66
+ show: {
67
+ resource: [
68
+ 'aiSearch',
69
+ 'backlinks',
70
+ 'domainAnalysis',
71
+ 'keywordResearch',
72
+ 'serpClassic',
73
+ 'websiteAudit',
74
+ ],
75
+ },
76
+ },
77
+ },
78
+ {
79
+ name: 'seRankingProjectApi',
80
+ required: true,
81
+ displayOptions: {
82
+ show: {
83
+ resource: [
84
+ 'accountSystem',
85
+ 'aiResultTracker',
86
+ 'analyticsTraffic',
87
+ 'backlinkChecker',
88
+ 'competitors',
89
+ 'generalData',
90
+ 'keywordGroups',
91
+ 'marketingPlan',
92
+ 'projectGroups',
93
+ 'projectManagement',
94
+ 'searchVolume',
95
+ 'subAccount',
96
+ 'urlTags',
97
+ 'websiteAuditProject',
98
+ ],
99
+ },
100
+ },
65
101
  },
66
102
  ],
67
103
  properties: [
@@ -302,6 +302,14 @@ exports.aiResultTrackerFields = [
302
302
  },
303
303
  },
304
304
  options: [
305
+ {
306
+ displayName: 'Group IDs',
307
+ name: 'groupIds',
308
+ type: 'string',
309
+ default: '',
310
+ placeholder: '12,18',
311
+ description: 'Comma-separated prompt group IDs to filter by',
312
+ },
305
313
  {
306
314
  displayName: 'Limit',
307
315
  name: 'limit',
@@ -333,6 +341,28 @@ exports.aiResultTrackerFields = [
333
341
  placeholder: 'best seo tool,seo rank tracker,website audit tool',
334
342
  description: 'Comma-separated list of keyword prompts to add (max 255 chars each)',
335
343
  },
344
+ {
345
+ displayName: 'Additional Fields',
346
+ name: 'additionalFields',
347
+ type: 'collection',
348
+ placeholder: 'Add Field',
349
+ default: {},
350
+ displayOptions: {
351
+ show: {
352
+ resource: ['aiResultTracker'],
353
+ operation: ['addPrompts'],
354
+ },
355
+ },
356
+ options: [
357
+ {
358
+ displayName: 'Group ID',
359
+ name: 'groupId',
360
+ type: 'number',
361
+ default: 0,
362
+ description: 'Target prompt group ID. Defaults to the site\'s default group.',
363
+ },
364
+ ],
365
+ },
336
366
  {
337
367
  displayName: 'Keyword-LLM Link IDs',
338
368
  name: 'k2siteLlmIds',
@@ -377,6 +407,14 @@ exports.aiResultTrackerFields = [
377
407
  placeholder: '2026-01-31',
378
408
  description: 'End date (YYYY-MM-DD). Defaults to current date.',
379
409
  },
410
+ {
411
+ displayName: 'Group IDs',
412
+ name: 'groupIds',
413
+ type: 'string',
414
+ default: '',
415
+ placeholder: '12,18',
416
+ description: 'Comma-separated prompt group IDs to filter by',
417
+ },
380
418
  {
381
419
  displayName: 'Limit',
382
420
  name: 'limit',
@@ -405,7 +443,7 @@ exports.aiResultTrackerFields = [
405
443
  },
406
444
  },
407
445
  default: 0,
408
- description: 'Value of the <code>k2site_llm_id</code> field from List Prompts (the SE Ranking docs call this parameter <code>keyword_id</code>, but the endpoint actually expects <code>k2site_llm_id</code>)',
446
+ description: 'Value of the <code>k2site_llm_id</code> field from List Prompts',
409
447
  },
410
448
  {
411
449
  displayName: 'Additional Fields',
@@ -79,13 +79,22 @@ async function AiResultTrackerOperations(index) {
79
79
  query.limit = additionalFields.limit;
80
80
  if (additionalFields.offset !== undefined)
81
81
  query.offset = additionalFields.offset;
82
- return await apiRequest_1.apiRequest.call(this, 'GET', `/sites/${siteId}/airt/llm/${llmId}/prompts`, {}, query, index);
82
+ let endpoint = `/sites/${siteId}/airt/llm/${llmId}/prompts`;
83
+ if (additionalFields.groupIds) {
84
+ const ids = additionalFields.groupIds.split(',').map((id) => id.trim());
85
+ endpoint += '?' + ids.map((id) => `group_ids[]=${encodeURIComponent(id)}`).join('&');
86
+ }
87
+ return await apiRequest_1.apiRequest.call(this, 'GET', endpoint, {}, query, index);
83
88
  }
84
89
  case 'addPrompts': {
85
90
  const llmId = this.getNodeParameter('llmId', index);
86
91
  const promptsStr = this.getNodeParameter('prompts', index);
87
92
  const prompts = promptsStr.split(',').map((p) => p.trim()).filter((p) => p.length > 0);
88
- return await apiRequest_1.apiRequest.call(this, 'POST', `/sites/${siteId}/airt/llm/${llmId}/prompts`, { prompts }, {}, index);
93
+ const additionalFields = this.getNodeParameter('additionalFields', index, {});
94
+ const body = { prompts };
95
+ if (additionalFields.groupId)
96
+ body.group_id = additionalFields.groupId;
97
+ return await apiRequest_1.apiRequest.call(this, 'POST', `/sites/${siteId}/airt/llm/${llmId}/prompts`, body, {}, index);
89
98
  }
90
99
  case 'deletePrompts': {
91
100
  const llmId = this.getNodeParameter('llmId', index);
@@ -106,7 +115,12 @@ async function AiResultTrackerOperations(index) {
106
115
  query.limit = additionalFields.limit;
107
116
  if (additionalFields.offset !== undefined)
108
117
  query.offset = additionalFields.offset;
109
- return await apiRequest_1.apiRequest.call(this, 'GET', `/sites/${siteId}/airt/llm/${llmId}/prompts/rankings`, {}, query, index);
118
+ let endpoint = `/sites/${siteId}/airt/llm/${llmId}/prompts/rankings`;
119
+ if (additionalFields.groupIds) {
120
+ const ids = additionalFields.groupIds.split(',').map((id) => id.trim());
121
+ endpoint += '?' + ids.map((id) => `group_ids[]=${encodeURIComponent(id)}`).join('&');
122
+ }
123
+ return await apiRequest_1.apiRequest.call(this, 'GET', endpoint, {}, query, index);
110
124
  }
111
125
  case 'getPromptAnswer': {
112
126
  const llmId = this.getNodeParameter('llmId', index);
@@ -5,7 +5,7 @@ const n8n_workflow_1 = require("n8n-workflow");
5
5
  let lastRequestTime = 0;
6
6
  const MIN_REQUEST_INTERVAL = 300;
7
7
  async function apiRequest(method, endpoint, body = {}, query = {}, itemIndex = 0) {
8
- var _a, _b, _c, _d, _e, _f, _g, _h;
8
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
9
9
  const now = Date.now();
10
10
  const timeSinceLastRequest = now - lastRequestTime;
11
11
  if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) {
@@ -29,23 +29,12 @@ async function apiRequest(method, endpoint, body = {}, query = {}, itemIndex = 0
29
29
  resource = '';
30
30
  }
31
31
  const isDataApi = DATA_API_RESOURCES.has(resource);
32
- const credentials = await this.getCredentials('seRankingApi');
33
- const token = isDataApi
34
- ? credentials.apiToken
35
- : credentials.projectApiToken;
36
- if (!token) {
37
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), isDataApi
38
- ? 'Data API token not configured. Add your Data API token in the SE Ranking credentials.'
39
- : 'Project API token not configured. Add your Project API token in the SE Ranking credentials.', { itemIndex });
40
- }
32
+ const credentialType = isDataApi ? 'seRankingApi' : 'seRankingProjectApi';
41
33
  const httpMethod = method.toUpperCase();
42
34
  const options = {
43
35
  method: httpMethod,
44
36
  timeout: 60000,
45
37
  url: '',
46
- headers: {
47
- 'Authorization': `Token ${token}`,
48
- },
49
38
  };
50
39
  if (query._fullUrl) {
51
40
  options.url = endpoint;
@@ -117,12 +106,24 @@ async function apiRequest(method, endpoint, body = {}, query = {}, itemIndex = 0
117
106
  }
118
107
  }
119
108
  try {
120
- const response = await this.helpers.httpRequest(options);
109
+ const response = await this.helpers.httpRequestWithAuthentication.call(this, credentialType, options);
121
110
  return response;
122
111
  }
123
112
  catch (error) {
124
- const errorData = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.body) || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.data) || {};
125
- const statusCode = error.statusCode || ((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) || 'Unknown';
113
+ if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('does not require credentials')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('No credentials'))) {
114
+ const missingCred = isDataApi
115
+ ? 'SE Ranking API credential not configured'
116
+ : 'SE Ranking Project API credential not configured';
117
+ const missingDesc = isDataApi
118
+ ? 'This resource requires the SE Ranking API credential. Click the node, find the "SE Ranking API" credential slot, and create/select your Data API token.'
119
+ : 'This resource requires the SE Ranking Project API credential. Click the node, find the "SE Ranking Project API" credential slot, and create/select your Project API token.';
120
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), missingCred, {
121
+ itemIndex,
122
+ description: missingDesc,
123
+ });
124
+ }
125
+ const errorData = ((_c = error.response) === null || _c === void 0 ? void 0 : _c.body) || ((_d = error.response) === null || _d === void 0 ? void 0 : _d.data) || {};
126
+ const statusCode = error.statusCode || ((_e = error.response) === null || _e === void 0 ? void 0 : _e.status) || 'Unknown';
126
127
  let errorMessage = 'Unknown error occurred';
127
128
  let errorDescription = '';
128
129
  if (statusCode === 400) {
@@ -143,7 +144,7 @@ async function apiRequest(method, endpoint, body = {}, query = {}, itemIndex = 0
143
144
  }
144
145
  else if (statusCode === 429) {
145
146
  errorMessage = 'Rate Limit Exceeded';
146
- const retryAfter = ((_e = (_d = error.response) === null || _d === void 0 ? void 0 : _d.headers) === null || _e === void 0 ? void 0 : _e['retry-after']) || ((_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.headers) === null || _g === void 0 ? void 0 : _g['Retry-After']) || 60;
147
+ const retryAfter = ((_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.headers) === null || _g === void 0 ? void 0 : _g['retry-after']) || ((_j = (_h = error.response) === null || _h === void 0 ? void 0 : _h.headers) === null || _j === void 0 ? void 0 : _j['Retry-After']) || 60;
147
148
  errorDescription = `Too many requests. SE Ranking requires you to wait ${retryAfter} seconds. The node now automatically adds 300ms delay between requests, but SE Ranking may have additional hourly/daily limits.`;
148
149
  }
149
150
  else if (statusCode === 500 || statusCode === 502 || statusCode === 503) {
@@ -158,7 +159,7 @@ async function apiRequest(method, endpoint, body = {}, query = {}, itemIndex = 0
158
159
  errorMessage = 'Connection Failed';
159
160
  errorDescription = 'Cannot reach SE Ranking API. Check your internet connection';
160
161
  }
161
- else if (error.code === 'ETIMEDOUT' || ((_h = error.message) === null || _h === void 0 ? void 0 : _h.includes('timeout'))) {
162
+ else if (error.code === 'ETIMEDOUT' || ((_k = error.message) === null || _k === void 0 ? void 0 : _k.includes('timeout'))) {
162
163
  errorMessage = 'Request Timeout';
163
164
  errorDescription = 'Request exceeded 60 seconds. Try with fewer items or use a faster operation';
164
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seranking/n8n-nodes-seranking",
3
- "version": "1.5.9",
3
+ "version": "1.5.12",
4
4
  "description": "n8n connector for SE Ranking API - AI Search, Backlinks, Domain Analysis, Keyword Research, Website Audit, Project Management, Project Groups, AI Result Tracker, Keyword Groups, Competitors, URL Tags, Analytics Traffic, Account System, Sub-Account Management, General Data, Marketing Plan, Backlink Checker, Search Volume",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/seranking/n8n-nodes-seranking",
@@ -51,7 +51,8 @@
51
51
  "dist/nodes/SeRanking/SeRanking.node.js"
52
52
  ],
53
53
  "credentials": [
54
- "dist/credentials/SeRankingApi.credentials.js"
54
+ "dist/credentials/SeRankingApi.credentials.js",
55
+ "dist/credentials/SeRankingProjectApi.credentials.js"
55
56
  ]
56
57
  },
57
58
  "scripts": {