Skip to content

WhatsApp

Ed Mozley edited this page Jun 27, 2026 · 8 revisions

WhatsApp channel

Let customers chat with an analyst over WhatsApp β€” every message becomes a normal FreeITSM ticket. It sits alongside email as another inbound channel and reuses the whole ticket pipeline: the reading-pane thread, multi-tenancy routing/isolation, origins and the requester record.

Status: Live. Twilio end-to-end (inbound β†’ ticket, in-window replies, AI summary/suggest) and the Meta WhatsApp Cloud API both work behind the same interface. Production firewall delivery is via Cloudflare Tunnel (direct mode). Templates (replying after the 24h window) and inbound media (images/files attached to the ticket) are supported, and an open channel ticket auto-refreshes every 15s. Still to come: an optional hosted relay.

How it works

A messaging channel is the channel equivalent of an email mailbox β€” one WhatsApp number wired to a provider. When a customer messages it:

  1. The provider (Twilio or Meta) pushes the message to this install's webhook: api/messaging/webhook.php?channel=<id>.
  2. FreeITSM verifies the request, finds an open conversation for that sender (or opens a new ticket), and stores the message.
  3. The ticket is tagged with the WhatsApp origin and shows in the inbox like any other ticket.
  4. The analyst replies from an inline composer in the reading pane; the reply goes back out over WhatsApp.

Inbound and outbound messages live in the same emails table as email (with a channel column), so the reading-pane conversation thread just works.

Why a webhook (and what that means for self-hosting)

Email works by FreeITSM polling a mailbox. WhatsApp is the opposite: the provider pushes inbound messages to a public HTTPS URL β€” there is no polling option. So the install must be reachable from the internet on that URL. Pick the option that matches your deployment:

Your setup How to expose the webhook
Hosted on a public server with a domain Nothing extra β€” point the provider at https://yourdomain/api/messaging/webhook.php?channel=<id>.
Self-hosted behind a firewall/NAT (production) Cloudflare Tunnel β€” a free, persistent public hostname, outbound-only (firewall-friendly). See below.
Local laptop, just testing ngrok β€” quickest, but the free URL changes on every restart, so it's dev-only: ngrok http 80.

Whichever you use, set it once in the Public base URL field at the top of Settings β†’ Messaging and every channel's webhook URL below becomes copy-paste-ready (no hand-editing localhost). Leave it blank to fall back to whatever address you're browsing from.

Cloudflare Tunnel (recommended for firewalled self-hosters)

Unlike ngrok's rotating free URL, a Cloudflare Tunnel gives you a stable hostname and runs as a background service, so it survives reboots β€” which is what production needs. It connects outbound from your box to Cloudflare, so no inbound firewall holes or port-forwarding are required.

  1. Create a free Cloudflare account and add a domain (or use a free *.trycloudflare.com quick tunnel for trials).
  2. Install cloudflared on the server running FreeITSM.
  3. Authenticate and create a named tunnel:
    cloudflared tunnel login
    cloudflared tunnel create freeitsm
    cloudflared tunnel route dns freeitsm itsm.yourdomain.com
    
  4. Point the tunnel at your local web server (port 80) in ~/.cloudflared/config.yml, then run it as a service:
    cloudflared tunnel run freeitsm     # or: cloudflared service install
    
  5. In FreeITSM, set Public base URL to https://itsm.yourdomain.com, copy each channel's webhook URL, and paste it into your provider.

Because the hostname is stable, you set it once and never touch it again β€” the key advantage over ngrok.

Providers

Choose per channel under Tickets β†’ Settings β†’ Messaging:

Provider Best for Notes
Twilio Getting started / testing Has a WhatsApp sandbox β€” you can send/receive end-to-end with no Meta business verification. Recommended first build.
Meta WhatsApp Cloud API Production, direct Cheapest long-term (no middleman), but needs Meta business verification before it works.

Both implement the same MessagingProvider interface (includes/messaging/), so switching is just a channel setting.

Tip β€” Test button. Each channel row has a Test button that runs three checks and shows a βœ…/❌ report (no real message is sent): Credentials (read-only provider call), Webhook reachability (FreeITSM calls the channel's own public webhook URL and confirms it round-trips β€” catches a down tunnel or wrong Public base URL), and Inbound handling (a synthetic message is run through ingest to confirm it becomes a ticket, then removed). Run it after entering credentials and after setting the webhook to confirm the whole path works.

Credentials

  • Twilio: Account SID + Auth Token. The channel's WhatsApp number is the "From".
  • Meta: Phone number ID + Access token + App secret, plus a Verify token you choose (used for Meta's webhook subscription handshake).

Credentials are encrypted at rest (AES-256-GCM, same as mailbox secrets).

Inbound delivery: direct vs relay

Each channel has an ingress mode:

  • Direct (recommended) β€” the provider reaches this install's webhook URL directly, authenticated by the provider's own request signature (Twilio's X-Twilio-Signature, Meta's X-Hub-Signature-256). Combined with Cloudflare Tunnel (above), this is the recommended setup even for firewalled self-hosters: the tunnel gives you a public URL without exposing a port, and message content flows only between you and the provider.
  • Relay β€” a small hosted service forwards messages on to installs that can't be reached at all. Authenticated by a shared secret (X-FreeITSM-Relay-Secret). The webhook endpoint already accepts relayed requests, but the relay service itself is not built yet β€” it's a future option for the rare case a Cloudflare Tunnel won't do. Trade-off worth knowing: a relay means message content transits a third-party service, so direct + tunnel is preferred wherever possible.

Routing to a company (multi-tenancy)

Channel routing mirrors email routing exactly, but keyed on the sender's phone number (numbers have no domain):

  1. Single-company install β†’ the Default company.
  2. Pinned channel (assigned to a company) β†’ that company; sender ignored.
  3. Shared channel β†’ the exact sender number is mapped to a company β†’ that company; otherwise triage (surfaces under Default until filed).

On a single-company install none of this is visible β€” it behaves exactly as today.

The 24-hour service window

WhatsApp only allows free-text replies within 24 hours of the customer's last message. Outside that window, providers block free text and require a pre-approved template message.

FreeITSM tracks this per conversation (tickets.last_inbound_at). Inside the window, the composer works normally. Once it closes, the composer swaps the free-text box for a template picker (see below) so the analyst can still re-open the conversation with an approved template.

Message templates (replying after the 24h window)

A template is a message format pre-approved by Meta/Twilio. FreeITSM can't approve templates β€” you create and approve each one at your provider, then add its definition here so analysts can pick and fill it.

Manage them under Tickets β†’ Settings β†’ Messaging β†’ Message templates:

Field Notes
Name A label for analysts, e.g. "Ticket update nudge".
Provider Twilio or Meta β€” must match the channel it'll be used on.
Reference Twilio: the Content SID (HX…) of the approved template. Meta: the approved template name.
Language Used by Meta (e.g. en, en_US). Ignored by Twilio.
Body The approved text, with {{1}}, {{2}} … where the analyst fills variables. Must match what the provider approved.

When a ticket's 24h window has closed, the reading-pane composer shows a template picker: the analyst chooses a template (only those matching the channel's provider are offered), fills the {{n}} values (with a live preview), and sends. The filled message is stored in the thread. The customer's reply re-opens the free-text window.

AI helpers

From the composer on a WhatsApp ticket:

  • Suggest β€” drafts a concise reply for you to review/edit before sending (nothing is sent automatically).
  • Summarise β€” summarises the conversation and saves it as an internal note on the ticket.

Both reuse the Tickets β†’ Settings β†’ Reply Cleanup AI key/provider, so usage shows on that billing line.

Media and live updates

  • Inbound media (images, documents, voice notes, etc.) is downloaded from the provider and stored as a normal ticket attachment β€” it shows in the reading-pane attachment bar and downloads like any email attachment. Any caption becomes the message body. (Twilio fetches the media URL with account auth; Meta does the media-id β†’ URL β†’ bytes two-step.) If a download fails, the message still arrives with a clear marker.
  • Live refresh: while you're viewing a channel ticket, the conversation polls for new inbound messages every 15 seconds, so replies appear without reloading. It won't disturb a reply you're typing.

Setting up a channel (Twilio sandbox, quickest)

  1. In Twilio, open the WhatsApp sandbox and note your Account SID, Auth Token, and sandbox number.
  2. In FreeITSM: Tickets β†’ Settings β†’ Messaging β†’ Add. Provider = Twilio, enter the number + SID + token, save.
  3. Copy the channel's webhook URL from the list. If testing locally, start ngrok http 80 and swap the host for your ngrok host.
  4. Paste that URL into the Twilio sandbox's "When a message comes in" field.
  5. Join the sandbox from your phone (send the join code), then message it β€” a ticket appears in the inbox.
  6. Open the ticket and reply from the composer; the reply lands on your phone.

Setting up Meta WhatsApp Cloud API (production)

Twilio's sandbox is ideal for trying the channel out, but for production direct from Meta (no middleman, cheapest per message) use the Cloud API. It needs a Meta developer app and business verification, so it's more setup than Twilio β€” do it once you're ready to go live.

  1. At developers.facebook.com, create an app of type Business, then add the WhatsApp product to it.
  2. In WhatsApp β†’ API Setup, note the Phone number ID and a temporary access token. For production, generate a permanent token via a System User in Business Settings (a temporary token expires in 24h).
  3. Find your App Secret under App settings β†’ Basic.
  4. In FreeITSM: Settings β†’ Messaging β†’ Add, Provider = Meta. Enter the Phone number ID, Access token, App secret, and a Verify token of your choosing (any random string). Set the channel's WhatsApp number to your business number. Save.
  5. Copy the channel's webhook URL (set your Public base URL first β€” a real domain or Cloudflare Tunnel host).
  6. Back in the Meta app, under WhatsApp β†’ Configuration β†’ Webhook, set the Callback URL to that webhook URL and the Verify token to the same string you entered in FreeITSM, then click Verify and save. Meta calls the URL with a GET handshake; FreeITSM echoes the challenge automatically.
  7. Still under Webhook, subscribe to the messages field. Inbound messages will now arrive.

The Graph API version defaults to a recent one (currently v21.0) and Meta retires a version ~2 years after release. When that day comes you can move to a newer version without a code change: edit the Meta channel and set the optional Graph API version field (under the Meta credentials) to e.g. v22.0. Leave it blank to use the built-in default.

Files

  • includes/messaging/ β€” MessagingProvider (interface), TwilioProvider, MetaCloudProvider, messaging.php (factory + helpers), ingest.php (message β†’ ticket).
  • api/messaging/ β€” webhook.php (inbound), send_message.php (analyst reply), send_template.php, ai_summary.php, ai_suggest_reply.php, test_channel.php, get_channels.php / save_channel.php / delete_channel.php, get_templates.php / save_template.php / delete_template.php, save_base_url.php.
  • Tables: messaging_channels, tenant_channel_senders, messaging_templates; plus emails.channel / emails.channel_id and tickets.last_inbound_at.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally