C Concierge Documentation
Docs / Channels / WhatsApp
Channel · 1 of 4

WhatsApp auto‑reply.

WhatsApp is one of four channels in the unified messaging pipeline. Incoming WhatsApp messages are normalized into InboundMessage structs and processed through the common pipeline, which evaluates reply rules, generates the response, and logs metadata.

WhatsApp Business API
Shared WABA · Embedded Signup · Workers AI llama‑4‑scout
Status
Live
Cost
1 credit / AI

Adding a WhatsApp number

  1. Open the admin

    Navigate to Admin → WhatsApp Accounts → Connect WhatsApp Number.

  2. Complete Embedded Signup

    Meta’s in‑flow widget walks you through registering the phone number and attaching it to your shared WABA. The phone number and phone number ID are configured automatically when the flow returns.

  3. Verify webhook

    Concierge sends a test webhook to itself to confirm the round trip. If you see a green check on the account row, you’re live.

Manual fallback

If you already know your phone number ID, use /admin/whatsapp/manual instead. Useful for migrating from another platform or when Embedded Signup isn’t available in your region.

Configuring auto-reply

Each WhatsApp account has its own ReplyConfig with two layers:

  • Default reply: edited inline on the account’s edit page. Sets the fallback when no rule matches. Pick canned text or AI prompt.
  • Reply rules: managed at /admin/rules/whatsapp/{account_id}. An ordered list checked top‑down on every inbound message; first match wins.
Account fieldsWhatsAppAccount.auto_reply (ReplyConfig)
FieldTypeDescription
enabledboolMaster switch; turns auto-reply off without losing rules.
rulesVec<ReplyRule>Ordered list. First match wins. Each rule pairs a matcher (keyword substring or BGE‑embedding cosine similarity over a user‑written intent description) with a response (canned text or AI prompt).
default_ruleReplyRuleMandatory fallback. Fires when no rule in rules matches.
wait_secondsu320–30s buffer; lets quick‑fire bursts collapse into one reply via the REPLY_BUFFER Durable Object.

Canned response

Sent verbatim. No AI call, no credit charge. Good for hours/closed/FAQ replies.

json example · canned
{
  "id": "hours",
  "label": "Hours / location",
  "matcher": {
    "kind": "static_text",
    "keywords": ["hours", "open", "closed", "address"]
  },
  "response": {
    "kind": "canned",
    "text": "We're open 7am–7pm every day. Come say hi! ☕"
  }
}

AI prompt response

The matched rule’s prompt is concatenated to the tenant’s persona prompt (set at /admin/persona) and sent to Workers AI as the system instruction. The sender’s name and message are passed as user‑message context. One credit deducts before the call.

json example · prompt rule
{
  "id": "booking",
  "label": "Booking requests",
  "matcher": {
    "kind": "prompt",
    "description": "wants to book, reschedule, or check availability for an appointment",
    "threshold": 0.72
  },
  "response": {
    "kind": "prompt",
    "text": "Ask which service and which day they prefer; mention a stylist will confirm shortly."
  }
}
i
Models

Reply generation uses @cf/meta/llama-4-scout-17b-16e-instruct. Prompt‑injection scanning + persona safety classification use the smaller @cf/meta/llama-3.1-8b-instruct-fast. Rule embeddings use @cf/baai/bge-base-en-v1.5. Latency is typically 600–1200ms; the customer sees it as “typing…” in WhatsApp.

Persona

The system prompt for every AI reply is the tenant‑wide persona prompt at /admin/persona (preset, builder, or custom) concatenated with the matched rule’s prompt. AI replies are blocked unless the persona’s safety status is Approved; canned default replies still send.

See Persona & safety check for the async classifier flow.

Credit deduction

Each AI‑mode reply (rule with a Prompt response) deducts one credit from the tenant’s balance. Canned responses are free. Credits are deducted before the AI call (optimistic) and restored if generation or send fails. When credits reach zero, AI replies stop silently; canned default replies still send.

See Billing for managing credits.

!
Silent stop on zero

When you run out of credits, Concierge stops AI replies without notifying the customer. The inbound message is still logged. We do not send error messages to customers because that would itself cost money, and produce a bad impression.

Platform model

All WhatsApp numbers share a single platform token (WHATSAPP_ACCESS_TOKEN). This is a system user token for your WhatsApp Business Account. You don’t need per‑customer tokens. Customers add their phone numbers to your WABA via Meta’s Embedded Signup flow.

This means: one Meta app, one WABA, many phone numbers. The hosted service uses this same model. When you connect on concierge.calculon.tech, your number joins our WABA.

Message logging

Inbound and outbound message metadata (direction, sender ID, recipient ID, timestamp) is logged to the unified messages table. No message content is stored. The body lives in memory long enough to be passed to the AI and dispatched to the outbound API, then is dropped.

sql migrations/0001_create_schema.sql · partial
CREATE TABLE messages (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  tenant_id   TEXT NOT NULL,
  channel     TEXT NOT NULL,    -- whatsapp | instagram | email | discord
  direction   TEXT NOT NULL,    -- inbound | outbound
  sender_id   TEXT NOT NULL,
  recipient_id TEXT NOT NULL,
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
  -- intentionally no body, no subject, no attachments
);

Webhook payload reference

The Worker exposes the standard Meta WhatsApp webhook contract. GET /webhook/whatsapp handles verification (echoes hub.challenge when hub.verify_token matches WHATSAPP_VERIFY_TOKEN); POST /webhook/whatsapp ingests messages.

Endpoints  signed by Meta
GET
/webhook/whatsapp?hub.mode=subscribe…: verification handshake
POST
/webhook/whatsapp: signed with X-Hub-Signature-256
verify
HMAC‑SHA256 with META_APP_SECRET · rejected if the signature is invalid
subscribed
messages field on the WhatsApp product