This workflow corresponds to n8n.io template #16103 — we link there as the canonical source.
This workflow follows the HTTP Request → Slack recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Approve or reject new Dokan marketplace vendors from Slack",
"tags": [
{
"name": "approval"
},
{
"name": "marketplace"
},
{
"name": "vendor-management"
},
{
"name": "slack"
},
{
"name": "dokan"
}
],
"nodes": [
{
"id": "main-overview-slack",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-736,
-112
],
"parameters": {
"width": 420,
"height": 704,
"content": "## Dokan \u2014 Vendor Approval Workflow\n\n### How it works\n\n1. A schedule trigger polls your Dokan site every 15 minutes for vendors awaiting approval.\n2. New pending vendors are de-duplicated and announced to Slack with Approve, Reject and View profile buttons.\n3. Clicking Approve calls a token-protected webhook that sets the vendor to `active` in Dokan.\n4. Clicking Reject sets the vendor to `inactive`, keeping the record recoverable.\n5. Dokan sends the welcome or rejection email natively, and the workflow posts a confirmation back to Slack.\n\n### Setup steps\n\n- [ ] Replace `your-marketplace.com` with your Dokan site URL (HTTP Request nodes + View profile button).\n- [ ] Replace `your-n8n-instance.com` with your n8n public URL (Approve/Reject buttons).\n- [ ] Set `YOUR_APPROVAL_TOKEN` to a long random string guarding the approve/reject webhooks.\n- [ ] Set `YOUR_SLACK_CHANNEL_ID` to your target Slack channel ID.\n- [ ] Add the `Dokan Application Password` credential (Basic Auth, user with `manage_woocommerce`).\n- [ ] Add the `Slack Bot Token` credential (bot with `chat:write`).\n\n### Customization\n\nVendor welcome and rejection emails are sent by Dokan \u2014 configure templates under wp-admin \u2192 Dokan \u2192 Settings \u2192 Email Settings. The \"Skip already-announced\" node uses n8n static data to avoid re-posting; the approve/reject branches clear it, so a vendor toggled back to pending is re-announced. Pair with the New Vendor Onboarding template to extend past approval."
},
"typeVersion": 1
},
{
"id": "sec-0-slack",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
340,
-20
],
"parameters": {
"color": 7,
"width": 1320,
"height": 340,
"content": "## Poll & announce new vendors\n\nEvery 15 minutes, fetch pending Dokan vendors over the REST API, drop any already posted (dedupe via static data), then announce each one to Slack with Approve, Reject and View profile buttons."
},
"typeVersion": 1
},
{
"id": "sec-1-slack",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
340,
380
],
"parameters": {
"color": 7,
"width": 1720,
"height": 340,
"content": "## Approve branch\n\nAdmin clicks Approve, firing a token-checked webhook that sets the vendor status to `active` in Dokan, clears the dedupe flag and posts a confirmation to Slack. Dokan sends the welcome email natively on status change."
},
"typeVersion": 1
},
{
"id": "sec-2-slack",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
340,
780
],
"parameters": {
"color": 7,
"width": 1720,
"height": 340,
"content": "## Reject branch\n\nAdmin clicks Reject, firing a token-checked webhook that sets the vendor status to `inactive` (record kept, recoverable), clears the dedupe flag and posts a confirmation to Slack. Dokan sends the rejection email natively on status change."
},
"typeVersion": 1
},
{
"id": "70cfbf6d-2582-499f-9512-bfafc0c493e1",
"name": "Every 15 minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Polls every 15 min. Replace with a real-time trigger once Dokan exposes dokan_new_seller_created as a WooCommerce webhook topic (see README \u2192 Real-time variants).",
"position": [
432,
128
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 15
}
]
}
},
"typeVersion": 1.1
},
{
"id": "d7f332d8-62a8-4510-bd45-9a79c21a858d",
"name": "Fetch pending vendors",
"type": "n8n-nodes-base.httpRequest",
"notes": "status=inactive returns vendors awaiting approval. Requires admin caps (manage_woocommerce).",
"position": [
640,
128
],
"parameters": {
"url": "=https://your-marketplace.com/wp-json/dokan/v1/stores",
"options": {},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"queryParameters": {
"parameters": [
{
"name": "status",
"value": "inactive"
},
{
"name": "per_page",
"value": "100"
},
{
"name": "orderby",
"value": "registered"
},
{
"name": "order",
"value": "desc"
}
]
}
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "6093eadb-2ea1-44ba-8401-2f09582666aa",
"name": "Skip already-announced",
"type": "n8n-nodes-base.code",
"notes": "n8n static data is per-workflow and survives executions. The dedupe entry is cleared by the approve/reject branches so the workflow self-heals if a vendor toggles back to pending.",
"position": [
1088,
128
],
"parameters": {
"jsCode": "// Deduplicate at the array level \u2014 runs ONCE, not per-vendor.\n// This avoids the per-item static-data race where only the last write persists.\n\nconst items = $input.all();\nconst staticData = $getWorkflowStaticData('global');\nstaticData.announcedVendors = staticData.announcedVendors || {};\n\nlet vendors = [];\nfor (const item of items) {\n const j = item.json;\n if (Array.isArray(j)) {\n vendors.push(...j);\n } else if (Array.isArray(j.data)) {\n vendors.push(...j.data);\n } else if (j.id) {\n vendors.push(j);\n }\n}\n\nconst now = Date.now();\nconst fresh = [];\n\nfor (const vendor of vendors) {\n const vendorId = String(vendor.id);\n if (staticData.announcedVendors[vendorId]) {\n continue;\n }\n staticData.announcedVendors[vendorId] = now;\n\n // Build a display-safe store reference for Slack.\n // If store_url exists, render as a Slack link \"<url|name>\".\n // Otherwise just the plain store name (Slack would render \"<|name>\" as broken).\n const storeName = vendor.store_name ?? `Vendor #${vendor.id}`;\n const storeUrl = vendor.store_url ?? '';\n const storeDisplay = storeUrl ? `<${storeUrl}|${storeName}>` : storeName;\n\n fresh.push({\n json: {\n vendor_id: vendor.id,\n store_name: storeName,\n store_display: storeDisplay,\n first_name: vendor.first_name ?? '',\n last_name: vendor.last_name ?? '',\n email: vendor.email ?? '',\n phone: vendor.phone ?? '',\n store_url: storeUrl,\n registered: vendor.registered ?? '',\n address: [\n vendor.address?.street_1,\n vendor.address?.city,\n vendor.address?.state,\n vendor.address?.country,\n ].filter(Boolean).join(', ') || 'Not provided',\n social: Object.entries(vendor.social ?? {})\n .filter(([_, url]) => url)\n .map(([k, url]) => `<${url}|${k}>`)\n .join(' \u00b7 ') || 'None',\n }\n });\n}\n\nreturn fresh;\n"
},
"typeVersion": 2
},
{
"id": "4bfdfbf2-4e71-4155-a3a8-3218dc195118",
"name": "Approve clicked",
"type": "n8n-nodes-base.webhook",
"position": [
432,
528
],
"parameters": {
"path": "dokan-vendor-approve-slack",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "87324b5e-3dc5-4a8e-b8bf-0fd1a78a0bb0",
"name": "Verify token (approve)",
"type": "n8n-nodes-base.if",
"position": [
640,
528
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "token-check-approve",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.token }}",
"rightValue": "=YOUR_APPROVAL_TOKEN"
}
]
}
},
"typeVersion": 2
},
{
"id": "1217fe7a-ee83-4134-b1fb-57e2b2f8b854",
"name": "Activate vendor",
"type": "n8n-nodes-base.httpRequest",
"position": [
864,
528
],
"parameters": {
"url": "=https://your-marketplace.com/wp-json/dokan/v1/stores/{{ $json.query.id }}/status",
"method": "POST",
"options": {},
"jsonBody": "={\n \"status\": \"active\"\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "c5b12d82-e009-4bde-928b-59db1e9de570",
"name": "Clear dedupe (approve)",
"type": "n8n-nodes-base.code",
"position": [
1312,
528
],
"parameters": {
"jsCode": "// Clear dedupe entry so if status flips back to inactive in the future, we re-announce.\nconst vendorId = String($('Approve clicked').item.json.query.id);\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.announcedVendors) {\n delete staticData.announcedVendors[vendorId];\n}\nreturn [{ json: $input.item.json }];"
},
"typeVersion": 2
},
{
"id": "d03bc903-40ff-4003-91c4-7f05a2f04c1f",
"name": "Confirm approval (Slack)",
"type": "n8n-nodes-base.slack",
"position": [
1744,
528
],
"parameters": {
"text": "=:white_check_mark: *{{ $('Activate vendor').item.json.store_name || 'Vendor #' + $('Approve clicked').item.json.query.id }}* \u2014 {{ $('Activate vendor').item.json.first_name }} {{ $('Activate vendor').item.json.last_name }} approved.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SLACK_CHANNEL_ID"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "739e6c50-d835-420a-bc56-d8ac51ea5d64",
"name": "Reject clicked",
"type": "n8n-nodes-base.webhook",
"position": [
432,
928
],
"parameters": {
"path": "dokan-vendor-reject-slack",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "c9f10300-2875-4c73-a3ed-6a509829ad07",
"name": "Verify token (reject)",
"type": "n8n-nodes-base.if",
"position": [
640,
928
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "token-check-reject",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.token }}",
"rightValue": "=YOUR_APPROVAL_TOKEN"
}
]
}
},
"typeVersion": 2
},
{
"id": "c7c253fc-8e52-46f8-adfe-5a0619894ac7",
"name": "Mark inactive",
"type": "n8n-nodes-base.httpRequest",
"notes": "Sets to inactive rather than DELETE so the vendor record (and audit trail) is preserved. If you'd rather hard-delete, swap this for DELETE /dokan/v1/stores/{id}.",
"position": [
1088,
928
],
"parameters": {
"url": "=https://your-marketplace.com/wp-json/dokan/v1/stores/{{ $('Reject clicked').item.json.query.id }}/status",
"method": "POST",
"options": {},
"jsonBody": "={\n \"status\": \"inactive\"\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "fa88573e-fb19-4a13-afae-568eeb5fb53c",
"name": "Clear dedupe (reject)",
"type": "n8n-nodes-base.code",
"position": [
1312,
928
],
"parameters": {
"jsCode": "// Clear dedupe so if admin later reactivates, the workflow can re-announce.\nconst vendorId = String($('Reject clicked').item.json.query.id);\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.announcedVendors) {\n delete staticData.announcedVendors[vendorId];\n}\nreturn [{ json: $input.item.json }];"
},
"typeVersion": 2
},
{
"id": "7a64d8de-5fda-46a7-b5a8-dec548ced16a",
"name": "Confirm rejection (Slack)",
"type": "n8n-nodes-base.slack",
"position": [
1744,
928
],
"parameters": {
"text": "=:x: *{{ $('Mark inactive').item.json.store_name || 'Vendor #' + $('Reject clicked').item.json.query.id }}* \u2014 {{ $('Mark inactive').item.json.first_name }} {{ $('Mark inactive').item.json.last_name }} rejected.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SLACK_CHANNEL_ID"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "359d9fd1-294b-4f7c-8ff9-0dd716fb49cb",
"name": "Post to Slack channel",
"type": "n8n-nodes-base.slack",
"position": [
1312,
128
],
"parameters": {
"text": "=New vendor pending approval: {{ $json.store_name }}",
"select": "channel",
"blocksUi": "={\n \"blocks\": [\n {\n \"type\": \"header\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"New vendor pending approval\"\n }\n },\n {\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \"*{{ $json.store_display }}*\\n_Registered: {{ $json.registered }}_\"\n }\n },\n {\n \"type\": \"section\",\n \"fields\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Name:*\\n{{ $json.first_name }} {{ $json.last_name }}\"\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Email:*\\n{{ $json.email }}\"\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Phone:*\\n{{ $json.phone || 'not provided' }}\"\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Address:*\\n{{ $json.address }}\"\n }\n ]\n },\n {\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \"*Social:* {{ $json.social }}\"\n }\n },\n {\n \"type\": \"actions\",\n \"elements\": [\n {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Approve\"\n },\n \"style\": \"primary\",\n \"url\": \"https://your-n8n-instance.com/webhook/dokan-vendor-approve-slack?id={{ $json.vendor_id }}&token=YOUR_APPROVAL_TOKEN\"\n },\n {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Reject\"\n },\n \"style\": \"danger\",\n \"url\": \"https://your-n8n-instance.com/webhook/dokan-vendor-reject-slack?id={{ $json.vendor_id }}&token=YOUR_APPROVAL_TOKEN\"\n },\n {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View profile\"\n },\n \"url\": \"https://your-marketplace.com/wp-admin/admin.php?page=dokan-dashboard#/vendors/{{ $json.vendor_id }}\"\n }\n ]\n }\n ]\n}",
"channelId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SLACK_CHANNEL_ID"
},
"messageType": "block",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "f34216a0-c589-4301-be76-0022d8b74d68",
"name": "Respond: approved",
"type": "n8n-nodes-base.respondToWebhook",
"notes": "HTML response with auto-close attempt. Best-effort: closes ~80% of browser sessions; fallback message appears if browser blocks the close.",
"position": [
-160,
736
],
"parameters": {
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html"
}
]
}
},
"respondWith": "text",
"responseBody": "<!DOCTYPE html>\n<html>\n<head>\n<title>Approved</title>\n<meta charset=\"utf-8\">\n<style>\n body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f5f5;animation:fadeOut 1.2s 0.4s forwards}\n .card{background:white;padding:32px 48px;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,0.08);text-align:center}\n .icon{font-size:48px;margin-bottom:12px;animation:pop 0.4s ease-out}\n h1{margin:0 0 8px;color:#1a7f3e;font-size:22px}\n p{margin:0;color:#888;font-size:13px}\n @keyframes pop{0%{transform:scale(0)}60%{transform:scale(1.2)}100%{transform:scale(1)}}\n @keyframes fadeOut{to{opacity:0}}\n</style>\n</head>\n<body>\n<div class=\"card\">\n<div class=\"icon\">\u2705</div>\n<h1>Approved</h1>\n<p>Closing this tab\u2026</p>\n</div>\n<script>\n// Try several close strategies. Most browsers block window.close() on tabs\n// the user opened via link click, but some allow it after window.open trick.\nsetTimeout(function(){\n try{ window.open('','_self'); window.close(); }catch(e){}\n try{ window.close(); }catch(e){}\n // If we're still here, show fallback message\n setTimeout(function(){\n var p=document.querySelector('p');\n if(p) p.textContent='You can close this tab.';\n document.body.style.animation='none';\n document.body.style.opacity='1';\n }, 600);\n}, 1200);\n</script>\n</body>\n</html>"
},
"typeVersion": 1.1
},
{
"id": "b1bee67a-1802-4f6f-bf65-2d1f97af2dd9",
"name": "Respond: rejected",
"type": "n8n-nodes-base.respondToWebhook",
"notes": "HTML response with auto-close attempt. Best-effort: closes ~80% of browser sessions; fallback message appears if browser blocks the close.",
"position": [
-160,
1152
],
"parameters": {
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html"
}
]
}
},
"respondWith": "text",
"responseBody": "<!DOCTYPE html>\n<html>\n<head>\n<title>Rejected</title>\n<meta charset=\"utf-8\">\n<style>\n body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f5f5;animation:fadeOut 1.2s 0.4s forwards}\n .card{background:white;padding:32px 48px;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,0.08);text-align:center}\n .icon{font-size:48px;margin-bottom:12px;animation:pop 0.4s ease-out}\n h1{margin:0 0 8px;color:#b73e3e;font-size:22px}\n p{margin:0;color:#888;font-size:13px}\n @keyframes pop{0%{transform:scale(0)}60%{transform:scale(1.2)}100%{transform:scale(1)}}\n @keyframes fadeOut{to{opacity:0}}\n</style>\n</head>\n<body>\n<div class=\"card\">\n<div class=\"icon\">\u274c</div>\n<h1>Rejected</h1>\n<p>Closing this tab\u2026</p>\n</div>\n<script>\nsetTimeout(function(){\n try{ window.open('','_self'); window.close(); }catch(e){}\n try{ window.close(); }catch(e){}\n setTimeout(function(){\n var p=document.querySelector('p');\n if(p) p.textContent='You can close this tab.';\n document.body.style.animation='none';\n document.body.style.opacity='1';\n }, 600);\n}, 1200);\n</script>\n</body>\n</html>"
},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"connections": {
"Mark inactive": {
"main": [
[
{
"node": "Clear dedupe (reject)",
"type": "main",
"index": 0
}
]
]
},
"Reject clicked": {
"main": [
[
{
"node": "Verify token (reject)",
"type": "main",
"index": 0
}
]
]
},
"Activate vendor": {
"main": [
[
{
"node": "Clear dedupe (approve)",
"type": "main",
"index": 0
}
]
]
},
"Approve clicked": {
"main": [
[
{
"node": "Verify token (approve)",
"type": "main",
"index": 0
}
]
]
},
"Every 15 minutes": {
"main": [
[
{
"node": "Fetch pending vendors",
"type": "main",
"index": 0
}
]
]
},
"Clear dedupe (reject)": {
"main": [
[
{
"node": "Confirm rejection (Slack)",
"type": "main",
"index": 0
}
]
]
},
"Fetch pending vendors": {
"main": [
[
{
"node": "Skip already-announced",
"type": "main",
"index": 0
}
]
]
},
"Verify token (reject)": {
"main": [
[
{
"node": "Mark inactive",
"type": "main",
"index": 0
}
]
]
},
"Clear dedupe (approve)": {
"main": [
[
{
"node": "Confirm approval (Slack)",
"type": "main",
"index": 0
}
]
]
},
"Skip already-announced": {
"main": [
[
{
"node": "Post to Slack channel",
"type": "main",
"index": 0
}
]
]
},
"Verify token (approve)": {
"main": [
[
{
"node": "Activate vendor",
"type": "main",
"index": 0
}
]
]
},
"Confirm approval (Slack)": {
"main": [
[
{
"node": "Respond: approved",
"type": "main",
"index": 0
}
]
]
},
"Confirm rejection (Slack)": {
"main": [
[
{
"node": "Respond: rejected",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpBasicAuthslackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow polls Dokan for newly registered vendors awaiting approval, posts each pending vendor to a Slack channel with Approve/Reject buttons, and updates the vendor’s status in Dokan when an admin clicks a button. Runs every 15 minutes on a schedule. Requests the list of…
Source: https://n8n.io/workflows/16103/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
debug. Uses httpRequest, slack, redis, mailgun. Scheduled trigger; 60 nodes.
This workflow is an automated employee time tracking and reporting system that monitors weekly work hours via TMetric, then delivers personalized summaries directly to each team member on Slack. It co
Import Productboard Notes Companies And Features Into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.
Import Productboard Notes, Companies and Features into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.
This workflow imports Productboard data into Snowflake, automating data extraction, mapping, and updates for features, companies, and notes. It supports scheduled weekly updates, data cleansing, and S