Skip to content
Recipes

WhatsApp Support Bot with n8n + AI

A customer messages your business WhatsApp number. Within a second:

  1. AI classifies the intent — question, complaint, or small-talk.
  2. n8n routes based on the classification.
  3. You either auto-reply with a templated answer or assign the conversation to your support team for a human to pick up.

You write zero backend code. Everything lives in an n8n workflow on top of @kirimdev/n8n-nodes-kirim.

  • Declarative — the workflow IS the runtime; no Docker image to ship.
  • First-class AI nodes — OpenAI, Anthropic, local Ollama all drop-in.
  • HMAC verification is automatic — the Kirim Trigger node refuses unsigned payloads, so you can’t accidentally ship an unauthenticated webhook to production.
  • Observability — every execution is a row in n8n’s history with full input/output snapshots.

If you’re already on Make / Zapier / Pipedream, the same pattern applies — substitute their Kirim integration. The fallback at the end of this page is for teams who’d rather own the code.

┌─────────────────┐
│ Customer WA │
└────────┬────────┘
│ message.received
┌────────────────────┐
│ Kirim Trigger │ ← n8n node, HMAC verified
│ (event: msg.recv) │
└────────┬───────────┘
┌────────────────────┐
│ AI Agent node │ → returns JSON { intent, reply }
│ (OpenAI/Anthropic)│
└────────┬───────────┘
┌────────────────────┐
│ Switch on intent │
└─┬───────┬────────┬─┘
│ │ │
▼ ▼ ▼
question complaint small_talk
│ │ │
▼ ▼ ▼
Send Update Send Text
Template Conv (AI reply)
(assign +
label)

One trigger in, three exits out. Every branch ends with a Kirim node call so the customer always gets a response (or a human gets pinged).

  1. Install the community node.

    In your n8n instance: Settings → Community Nodes → Install → enter @kirimdev/n8n-nodes-kirim → confirm. Wait for the install to finish (n8n restarts the node runtime automatically).

  2. Add a Kirim API credential.

    Credentials → New → Kirim API. Paste your kdv_live_… key. Save. The same credential is used by both the Kirim Trigger and the Kirim action nodes.

  3. Add the Kirim Trigger node.

    Drag Kirim Trigger onto the canvas as the workflow’s start node. Configure:

    • Event: message.received
    • Phone Number ID: 106540352242922 (or * to receive from every connected number in the org)

    Click Listen for Test Event and send yourself a test WhatsApp message. You should see the raw event arrive — that confirms the subscription is wired and HMAC verification passed.

  4. Add the AI Agent node.

    Drag AI Agent (or OpenAI / Anthropic if you want a single-shot call) after the trigger. System prompt:

    You are a WhatsApp support triage classifier for an Indonesian
    ecommerce store.
    Classify the incoming message as exactly one of:
    - "question" — customer asking about product, pricing, or service
    - "complaint" — customer has a problem (broken item, late delivery, refund)
    - "small_talk" — greeting, thanks, off-topic chatter
    Respond with strict JSON, no prose:
    {
    "intent": "question" | "complaint" | "small_talk",
    "reply": "<a one-sentence friendly Bahasa Indonesia reply suitable for small_talk; empty string otherwise>"
    }

    User prompt: {{ $json.data.messages[0].text.body }} (n8n’s expression syntax pulls the inbound text from the Meta-shaped payload).

  5. Add a Switch node.

    Route on {{ $json.intent }} with three rules:

    ConditionOutput
    equals "question"→ Output 0
    equals "complaint"→ Output 1
    equals "small_talk"→ Output 2
  6. Branch — question → send template auto-reply.

    Drag a Kirim node onto output 0. Operation: Message → Send Template.

    • Phone Number ID: {{ $('Kirim Trigger').item.json.phone_number_id }}
    • To: {{ $('Kirim Trigger').item.json.data.messages[0].from }}
    • Template name: auto_reply_question
    • Language: id_ID

    auto_reply_question is a pre-approved template along the lines of “Halo! Tim kami akan membalas dalam 1 jam kerja. Sementara itu, FAQ lengkap di {{1}}.” with one URL parameter.

  7. Branch — complaint → assign to a human.

    Drag a Kirim node onto output 1. Operation: Conversation → Update.

    • Conversation ID: {{ $('Kirim Trigger').item.json.data.conversation_id }}
    • Assigned User ID: usr_01HXSUPPORTLEADUSERID (a member of your support team)
    • Status: open

    Then a second Kirim node — Conversation → Add Label:

    • Label: auto-triaged

    The human picks the conversation up from the kirim.dev inbox UI; they see the inbound message plus the AI’s classification stored as a label.

  8. Branch — small_talk → send AI reply as text.

    Drag a Kirim node onto output 2. Operation: Message → Send Text.

    • To: {{ $('Kirim Trigger').item.json.data.messages[0].from }}
    • Body: {{ $('AI Agent').item.json.reply }}

    This works because small-talk happens inside the 24-hour conversation window — the customer literally just messaged you, so a free-form text reply is allowed (see Send text).

  9. Add a final Label step for audit.

    On every branch, add a Kirim Conversation → Add Label with label auto-triaged. You can then filter the inbox by that label in the dashboard to spot-check the bot’s decisions.

  10. Activate.

    Toggle the workflow to Active. n8n persists the webhook subscription with kirim.dev; deliveries now flow continuously.

A trimmed export of the key connections — drop into n8n via Import from JSON as a starting point, then add your credentials and template names:

{
"name": "WA Support Triage",
"nodes": [
{
"name": "Kirim Trigger",
"type": "@kirimdev/n8n-nodes-kirim.kirimTrigger",
"parameters": { "event": "message.received", "phoneNumberId": "106540352242922" },
"credentials": { "kirimApi": "Kirim Prod" }
},
{
"name": "AI Triage",
"type": "@n8n/n8n-nodes-langchain.agent",
"parameters": {
"systemMessage": "You are a WhatsApp support triage classifier...",
"responseFormat": "json"
}
},
{
"name": "Switch on intent",
"type": "n8n-nodes-base.switch",
"parameters": {
"rules": [
{ "value": "={{$json.intent}}", "operation": "equals", "compareValue": "question" },
{ "value": "={{$json.intent}}", "operation": "equals", "compareValue": "complaint" },
{ "value": "={{$json.intent}}", "operation": "equals", "compareValue": "small_talk" }
]
}
}
],
"connections": {
"Kirim Trigger": { "main": [[{ "node": "AI Triage", "type": "main", "index": 0 }]] },
"AI Triage": { "main": [[{ "node": "Switch on intent", "type": "main", "index": 0 }]] }
}
}

Prefer code? The same flow with @kirimdev/sdk and any HTTP server.

import { Hono } from 'hono'
import { Kirim } from '@kirimdev/sdk'
import { verifyWebhookSignature } from '@kirimdev/sdk/webhooks'
import OpenAI from 'openai'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })
const openai = new OpenAI()
const PHONE_NUMBER_ID = '106540352242922'
const SUPPORT_USER_ID = 'usr_01HXSUPPORTLEADUSERID'
const SYSTEM = `You are a WhatsApp support triage classifier...
Respond with strict JSON: {"intent":"question|complaint|small_talk","reply":""}`
const app = new Hono()
app.post('/kirim-hook', async (c) => {
const raw = await c.req.text()
const ok = await verifyWebhookSignature(
raw,
c.req.header('x-kirim-signature'),
process.env.KIRIM_WEBHOOK_SECRET!,
)
if (!ok) return c.text('bad signature', 401)
const event = JSON.parse(raw)
if (event.type !== 'message.received') return c.text('ok')
const inbound = event.data.messages?.[0]
if (!inbound?.text?.body) return c.text('ok')
// 1. Triage with the LLM.
const ai = await openai.chat.completions.create({
model: 'gpt-4o-mini',
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: SYSTEM },
{ role: 'user', content: inbound.text.body },
],
})
const { intent, reply } = JSON.parse(ai.choices[0].message.content!)
const phone = kirim.phoneNumbers(PHONE_NUMBER_ID)
// 2. Route.
if (intent === 'question') {
await phone.messages.send({
messaging_product: 'whatsapp',
to: inbound.from,
type: 'template',
template: {
name: 'auto_reply_question',
language: { code: 'id_ID' },
components: [
{ type: 'body', parameters: [{ type: 'text', text: 'https://shop.example/faq' }] },
],
},
})
} else if (intent === 'complaint') {
await kirim.conversations.update(event.data.conversation_id, {
assigned_user_id: SUPPORT_USER_ID,
status: 'open',
})
} else {
await phone.messages.send({
messaging_product: 'whatsapp',
to: inbound.from,
type: 'text',
text: { body: reply || 'Halo! 👋' },
})
}
// 3. Audit label.
await kirim.conversations.addLabel(event.data.conversation_id, { label: 'auto-triaged' })
return c.text('ok')
})
export default app

Webhook event catalog

Every event kirim.dev publishes, its source (Meta vs kirim.dev), and the payload shape. → /webhooks/events/

Conversation API

Assign, label, resolve — the inbox primitives the bot calls. → /api/