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": "W1 - IN TikTok Adapter (Secure)",
"active": true,
"settings": {
"executionTimeout": 300,
"saveExecutionProgress": true,
"saveManualExecutions": true
},
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "tiktok-webhook",
"responseMode": "responseNode",
"options": {
"rawBody": true
}
},
"name": "TikTok Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-2400,
0
]
},
{
"parameters": {
"language": "javascript",
"jsCode": "const crypto = require('crypto');\nconst fs = require('fs');\nconst path = require('path');\n\nfunction parseTikTok(rawBody) {\n if (!rawBody || typeof rawBody !== 'object') return null;\n // Assuming a structure similar to the previous implementation as a starting point\n const entry = rawBody.entry?.[0];\n if (!entry) return null;\n const change = entry.changes?.[0];\n if (!change) return null;\n const val = change.value || {};\n \n if (val.message_id === undefined && val.sender_id === undefined) return null;\n\n return {\n provider: 'tiktok',\n msg_id: val.message_id || val.mid || crypto.randomUUID(),\n from: val.sender_id || '',\n text: val.message?.text || '',\n timestamp: new Date().toISOString(),\n attachments: [],\n meta: {\n recipient_id: val.recipient_id || '',\n campaign: val.referral_param || ''\n }\n };\n}\n\nconst rawBodyInput = $json.body ?? $json;\nconst parsed = parseTikTok(rawBodyInput);\n\nconst headers = ($json.headers ?? {});\nconst ipRaw = (headers['x-forwarded-for'] || '').toString();\nconst ip = ipRaw.split(',')[0].trim();\nconst inboundReceivedAt = new Date().toISOString();\nconst correlationId = (headers['x-correlation-id'] || crypto.randomUUID()).toString();\n\nconst token = (headers['x-api-token'] || '').toString().trim();\nconst tokenHash = token ? crypto.createHash('sha256').update(token).digest('hex') : '';\n\nconst envelope = parsed ? {\n contract_version: 'v1',\n provider: 'tiktok',\n msg_id: parsed.msg_id,\n from: parsed.from,\n text: parsed.text,\n timestamp: parsed.timestamp,\n attachments: [],\n meta: { ...parsed.meta, ip }\n} : null;\n\nreturn [{\n json: {\n channel: 'tiktok',\n userId: parsed?.from || 'unknown',\n tenantId: '',\n restaurantId: '',\n conversationKey: '',\n inbound_envelope: envelope,\n metadata: {\n msgId: parsed?.msg_id || crypto.randomUUID(),\n timestamp: inboundReceivedAt,\n ip,\n userAgent: (headers['user-agent'] || '').toString()\n },\n message: {\n type: 'text',\n text: (parsed?.text || '').toString().trim()\n },\n _auth: {\n tokenPresent: !!token,\n tokenHash\n },\n _timing: {\n inbound_received_at: inboundReceivedAt,\n correlation_id: correlationId\n },\n _sec: {\n textHash: crypto.createHash('sha256').update(parsed?.text || '').digest('hex')\n }\n }\n}];"
},
"name": "B0 - Parse & Canonicalize",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2150,
0
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH c AS (SELECT client_id, client_name, tenant_id, restaurant_id, scopes FROM api_clients WHERE is_active=true AND token_hash = $1 LIMIT 1) SELECT client_id, client_name, tenant_id, restaurant_id, COALESCE(scopes, '[]'::jsonb) AS scopes, EXISTS(SELECT 1 FROM api_clients WHERE is_active=true AND token_hash = $1) AS matched;",
"additionalFields": {
"queryParams": "={{[$json._auth.tokenHash]}}"
}
},
"name": "B0 - Resolve Client (DB)",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
-1900,
0
]
},
{
"parameters": {
"language": "javascript",
"jsCode": "const e = $json;\nconst matched = !!e.matched;\nlet tenantId = e.tenant_id || '';\nlet restaurantId = e.restaurant_id || '';\nlet authMode = matched ? 'api_client' : 'deny';\nlet scopes = [];\ntry { scopes = Array.isArray(e.scopes) ? e.scopes : JSON.parse(e.scopes || '[]'); } catch { scopes = []; }\n\nconst conversationKey = tenantId ? `${tenantId}:${restaurantId}:tiktok:${e.userId}` : '';\nconst authOk = authMode !== 'deny';\nconst scopeOk = authOk && (scopes.includes('inbound:write') || scopes.includes('*'));\n\nconst tenant_context = {\n tenant_id: tenantId || null,\n restaurant_id: restaurantId || null,\n source: authMode,\n scopes\n};\n\nreturn [{\n json: {\n ...e,\n tenantId,\n restaurantId,\n conversationKey,\n tenant_context,\n _auth: {\n ...e._auth,\n authOk,\n scopeOk,\n denyReason: authOk ? (scopeOk ? '' : 'SCOPE_DENY') : 'AUTH_DENY'\n }\n }\n}];"
},
"name": "B0 - Apply Auth Context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1650,
0
]
},
{
"parameters": {
"language": "javascript",
"jsCode": "const crypto = require('crypto');\nconst e = $json;\nconst ctx = e.tenant_context || {};\nconst secret = ($env.TENANT_CONTEXT_SECRET || 'fallback-secret-6789').toString();\nconst seal = crypto.createHmac('sha256', secret).update(JSON.stringify(ctx)).digest('hex');\nreturn [{json:{...e, tenant_context_seal: seal}}];"
},
"name": "B0 - Seal Tenant Context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1400,
0
]
},
{
"parameters": {
"workflowId": "W0_MODULE_GUARD",
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"json": "={{ { module_key: 'channel_tiktok', tenant_id: $json.tenantId } }}"
}
}
},
"id": "module-guard-tiktok",
"name": "B0 - Module Guard",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1,
"position": [
-900,
-150
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.allowed }}",
"operation": "isTrue"
}
]
}
},
"id": "guard-check-tiktok",
"name": "B0 - Guard OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-700,
-150
]
},
{
"parameters": {
"responseCode": 403,
"responseBody": "={{JSON.stringify({error:'gated_access',reason:$json.reason || 'Channel disabled'})}}",
"options": {}
},
"id": "guard-error-tiktok",
"name": "RESP - 403 Forbidden",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
-500,
50
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json._auth.authOk}}",
"operation": "isTrue"
},
{
"value1": "={{$json._auth.scopeOk}}",
"operation": "isTrue"
}
]
}
},
"name": "B0 - Token OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-1150,
0
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH ins AS (INSERT INTO idempotency_keys (conversation_key, msg_id, channel) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING RETURNING 1) SELECT COALESCE((SELECT 1 FROM ins), 0) AS inserted;",
"additionalFields": {
"queryParams": "={{[$json.conversationKey, $json.metadata.msgId, $json.channel]}}"
}
},
"name": "B0 - Idempotency (DB)",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
-900,
-100
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json.inserted === 1}}",
"operation": "isTrue"
}
]
}
},
"name": "B0 - Is New Msg?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-650,
-100
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO inbound_messages (conversation_key, msg_id, channel, message_type, text_hash)\nVALUES ($1, $2, $3, $4, $5)\nRETURNING 1;",
"additionalFields": {
"queryParams": "={{[$json.conversationKey, $json.metadata.msgId, $json.channel, $json.message.type, $json._sec.textHash]}}"
}
},
"name": "B0 - Log Message (DB)",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
-400,
-150
]
},
{
"parameters": {
"workflowId": "={{$env.CORE_WORKFLOW_ID}}",
"options": {
"waitTillFinished": false
}
},
"name": "B1 - Execute CORE_AGENT",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1,
"position": [
-150,
-150
]
},
{
"parameters": {
"responseCode": 200,
"responseBody": "={{JSON.stringify({status:'received',channel:'tiktok',msg_id:$json.metadata?.msgId})}}",
"options": {}
},
"name": "RESP - 200 OK",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
100,
-150
]
},
{
"parameters": {
"responseCode": 401,
"responseBody": "={{JSON.stringify({error:'unauthorized',reason:$json._auth.denyReason})}}",
"options": {}
},
"name": "RESP - 401 Unauthorized",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
-900,
150
]
}
],
"connections": {
"TikTok Webhook": {
"main": [
[
{
"node": "B0 - Parse & Canonicalize",
"type": "main",
"index": 0
}
]
]
},
"B0 - Parse & Canonicalize": {
"main": [
[
{
"node": "B0 - Resolve Client (DB)",
"type": "main",
"index": 0
}
]
]
},
"B0 - Resolve Client (DB)": {
"main": [
[
{
"node": "B0 - Apply Auth Context",
"type": "main",
"index": 0
}
]
]
},
"B0 - Apply Auth Context": {
"main": [
[
{
"node": "B0 - Seal Tenant Context",
"type": "main",
"index": 0
}
]
]
},
"B0 - Seal Tenant Context": {
"main": [
[
{
"node": "B0 - Token OK?",
"type": "main",
"index": 0
}
]
]
},
"B0 - Token OK?": {
"main": [
[
{
"node": "B0 - Module Guard",
"type": "main",
"index": 0
}
],
[
{
"node": "RESP - 401 Unauthorized",
"type": "main",
"index": 0
}
]
]
},
"B0 - Module Guard": {
"main": [
[
{
"node": "B0 - Guard OK?",
"type": "main",
"index": 0
}
]
]
},
"B0 - Guard OK?": {
"main": [
[
{
"node": "B0 - Idempotency (DB)",
"type": "main",
"index": 0
}
],
[
{
"node": "RESP - 403 Forbidden",
"type": "main",
"index": 0
}
]
]
},
"B0 - Idempotency (DB)": {
"main": [
[
{
"node": "B0 - Is New Msg?",
"type": "main",
"index": 0
}
]
]
},
"B0 - Is New Msg?": {
"main": [
[
{
"node": "B0 - Log Message (DB)",
"type": "main",
"index": 0
}
],
[
{
"node": "RESP - 200 OK",
"type": "main",
"index": 0
}
]
]
},
"B0 - Log Message (DB)": {
"main": [
[
{
"node": "B1 - Execute CORE_AGENT",
"type": "main",
"index": 0
}
]
]
},
"B1 - Execute CORE_AGENT": {
"main": [
[
{
"node": "RESP - 200 OK",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
W1 - IN TikTok Adapter (Secure). Uses postgres. Webhook trigger; 15 nodes.
Source: https://github.com/zerAda/RestaurantAgentAutomation/blob/41a4d42dcd66e57b1e87b4750c0fd5fbf7058f68/workflows/W1_IN_TIKTOK.json — 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.
Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.
W2 - IN Instagram Adapter (Secure). Uses postgres, httpRequest. Webhook trigger; 28 nodes.
W3 - IN Messenger Adapter (Secure). Uses postgres, httpRequest. Webhook trigger; 28 nodes.
Engagement Tracking Workflow. Uses postgres, httpRequest. Webhook trigger; 22 nodes.