Chatwoot WhatsApp Integration

Connect WhatsApp Cloud API to a Chatwoot inbox with Dualhook: route Meta's message webhooks straight to Chatwoot, match the verify token, and keep the inbox app-secret-free.

Use this guide to connect your own WhatsApp Business Account to a Chatwoot WhatsApp Cloud API inbox using Dualhook as the Meta Tech Provider. The model is simple but specific: Dualhook handles Meta Embedded Signup and the webhook override, while Chatwoot is the actual message-webhook receiver, the conversation store, and the tool that sends outbound messages. Dualhook is never in the message-content path.

This page documents the current Chatwoot WhatsApp Cloud API behavior as of June 2026. The one make-or-break detail — whether Chatwoot will accept inbound POSTs that you cannot HMAC-sign yourself — is covered precisely in Inbound Validation.

Prerequisites

  • A working Dualhook connection created through Meta Embedded Signup, with Webhook Override available. Because Dualhook owns the Meta Tech Provider app, you do not need your own Meta app or Meta App Review. Dualhook captures your WABA_ID and PHONE_NUMBER_ID during onboarding and stores the long-lived access token, which it can reveal on demand through its audited reveal-secret flow.
  • A Chatwoot workspace that supports WhatsApp — either Chatwoot Cloud (app.chatwoot.com) or self-hosted. For self-hosted, you need a public HTTPS domain with FRONTEND_URL set correctly, because Chatwoot builds the WhatsApp callback URL from FRONTEND_URL and Meta must be able to reach it.
  • A current Chatwoot build. 2026 releases added BSUID handling to the WhatsApp payload pipeline, which matters for Meta's contact-identity changes (see Production Caveats).
  • At least one approved message template in WhatsApp Manager if you plan to start conversations or message customers outside the 24-hour customer-service window.

The values you will paste into Chatwoot, all available from your Dualhook connection:

Phone number          -> your WhatsApp number in E.164, e.g. +15551234567
Phone number ID       -> PHONE_NUMBER_ID from the Dualhook connection
Business Account ID   -> your WABA_ID
API key               -> the long-lived access token Dualhook reveals

Chatwoot labels the Cloud API access token "API key" in the manual setup form, while the rest of the WhatsApp ecosystem calls it an access token — they are the same value. Dualhook can reveal it for you; see Reveal access tokens.

No META_APP_SECRET? In the Embedded Signup flow, the message-path X-Hub-Signature-256 header is signed with Dualhook's Meta app secret, which you do not have and cannot get. You therefore cannot HMAC-verify inbound webhooks with your own app secret. That is fine for this integration as long as you use a plain manual whatsapp_cloud inbox: Chatwoot only enforces X-Hub-Signature-256 when the inbox carries an app secret or was created via Embedded Signup. Keep the inbox app-secret-free, keep the callback URL unguessable, and use the same verify token in Chatwoot and Dualhook. See Inbound Validation and Messaging Webhook.

Architecture Overview

With Dualhook, Meta delivers message-path webhooks straight to Chatwoot, not to Dualhook. Dualhook completes Embedded Signup, holds the Tech Provider app, and configures the webhook override by calling Meta POST /{WABA_ID}/subscribed_apps with your destination URL and verify token. That is why you can connect your WABA without owning a Meta app or passing App Review yourself.

Meta
  ├─ messages / statuses / message errors  (DIRECT from Meta)
  │    -> https://<chatwoot-host>/webhooks/whatsapp/+15551234567
  │    -> GET verify-token handshake (echoes hub.challenge)
  │    -> POST events -> conversation in inbox

  └─ management / operational events
       -> Dualhook  (template status, number/account quality, alerts)
       -> forwarded to your ops destination with X-Dualhook-* headers

Chatwoot (outbound)
  -> POST https://graph.facebook.com/v25.0/<PHONE_NUMBER_ID>/messages  (DIRECT to Meta)

Dualhook (setup)
  -> Meta Embedded Signup
  -> POST /{WABA_ID}/subscribed_apps
       override_callback_uri = Chatwoot webhook URL
       verify_token          = Chatwoot webhook verify token

Inbound message content travels Meta → Chatwoot and never passes through Dualhook — that is Dualhook's core privacy promise: it never stores or reads message content. Operational events that are not message content (template approvals, phone-number and account quality, account alerts) flow Meta → Dualhook → your ops destination, forwarded with X-Dualhook-* headers. See Webhook Override and Messaging Webhook.

Privacy split. Once you point WhatsApp at Chatwoot, Chatwoot becomes part of your message-content boundary. Even though Dualhook stores no message content, Chatwoot stores full conversation history — contacts, message bodies, media, and metadata — in its own database, subject to your deployment and retention settings. Review Compliance & Data Retention and Architecture & Security for that boundary.

Create the WhatsApp Cloud API Inbox in Chatwoot

Because your WABA is onboarded through Dualhook (not through Chatwoot's own Embedded Signup button), use Chatwoot's manual WhatsApp Cloud API flow. This is also the configuration that keeps the inbox app-secret-free, which is required for Dualhook compatibility.

  1. In Chatwoot, go to Settings → Inboxes → Add Inbox and choose WhatsApp.
  2. Select WhatsApp Cloud as the provider (not Twilio, not 360Dialog). If Chatwoot offers both Connect with WhatsApp Business (Embedded Signup) and a manual option, choose the manual path.
  3. Fill in the fields with the values from your Dualhook connection:
Inbox name                     ACME Support
Phone number                   +15551234567        # E.164, leading +
Phone number ID                106540352242922     # PHONE_NUMBER_ID
WhatsApp Business Account ID    102290129340398     # WABA_ID
API key                        EAA...              # access token Dualhook reveals
  1. Add agents/teams to the inbox and finish creation.
  2. Open the inbox and copy the two values Chatwoot generates: the Webhook URL (callback URL) and the Webhook verification token.

On inbox creation, Chatwoot auto-generates a 32-character hex webhook verify token for whatsapp_cloud channels, and builds the callback URL from FRONTEND_URL. The current route is phone-number-specific, not based on phone_number_id:

https://<chatwoot-host>/webhooks/whatsapp/<phone_number>

So for +15551234567:

# Chatwoot Cloud
https://app.chatwoot.com/webhooks/whatsapp/+15551234567

# Self-hosted (host = your FRONTEND_URL)
https://support.example.com/webhooks/whatsapp/+15551234567

The <phone_number> segment is the exact E.164 number stored on the inbox. Chatwoot routes the inbound POST by matching that path segment to a WhatsApp channel and verifying the payload's phone_number_id matches the channel's stored phone_number_id. Use the URL exactly as Chatwoot shows it — do not construct your own.

Heads-up: Chatwoot also tries to set the override itself. For a manual whatsapp_cloud inbox (not created via Embedded Signup), Chatwoot auto-runs its webhook setup on create, calling Meta POST /{WABA_ID}/subscribed_apps with its own callback URL and verify token, using the access token you supplied. Because that token operates on Dualhook's Tech Provider app, this call typically sets the override to the same Chatwoot URL you would configure in Dualhook. To make the end state deterministic, set Dualhook's destination URL and verify token to exactly the values Chatwoot generated — then whichever side writes the override last, the result is identical. If Chatwoot logs a webhook-setup error (e.g. Meta error #100, "must be subscribed to messages first"), that is harmless as long as Dualhook has set the override correctly.

Point Dualhook at Chatwoot

On connection (and whenever you update it), Dualhook calls Meta POST /{WABA_ID}/subscribed_apps and sets:

  • override_callback_uri = your destination URL (the Chatwoot Webhook URL)
  • verify_token = the token you set on the Dualhook connection

Configure the Dualhook connection with the exact values Chatwoot generated:

Destination webhook URL   https://support.example.com/webhooks/whatsapp/+15551234567
Verify token              7f0c6b0e8eab5d63c4a0d64c4b4d1a22

The verify tokens must be identical. The token on the Dualhook connection must match Chatwoot's generated Webhook verification token character-for-character. Meta's verification GET hits Chatwoot directly, and Chatwoot compares it against its stored token; a mismatch returns 401.

See Webhook Override for the full subscription mechanics, and Getting Started and Embedded Signup for context.

Verification Handshake

Meta sends a GET to the Chatwoot callback URL with:

  • hub.mode=subscribe
  • hub.verify_token=<YOUR_VERIFY_TOKEN>
  • hub.challenge=<CHALLENGE_VALUE>

Chatwoot handles this automatically. It looks up the channel by the <phone_number> path segment, compares the incoming hub.verify_token against the channel's stored verify token, and:

  • on match → echoes the raw hub.challenge with 200 and logs Whatsapp webhook verified;
  • on mismatch (or unknown phone number) → returns 401 Unauthorized.

You write no code for this. You only have to ensure:

Chatwoot inbox webhook verification token  ==  Dualhook connection verify token

A 401 at this step almost always means a token mismatch or a phone-number-format mismatch on the path segment — not an SSL or firewall problem.

Inbound Validation and Keeping the URL Unguessable

This is the make-or-break compatibility point, and it is worth stating precisely because Chatwoot's behavior is conditional, not "always" or "never".

Chatwoot runs a signature check before processing the inbound POST, but whether it actually enforces X-Hub-Signature-256 is decided in this order:

  1. If no channel matches the request → enforce.
  2. If the channel's provider is not whatsapp_cloudskip.
  3. If the channel carries an app secret in its config (any of app_secret, app_secret_key, client_secret, api_secret) → enforce.
  4. Otherwise → enforce only if the inbox was created via Embedded Signup.

Translated into the two configurations that matter:

  • Manual whatsapp_cloud inbox, no app secret, not embedded-signup → signature verification is skipped, and a genuine, unmodified Meta POST is accepted as-is. This is the configuration that works with Dualhook, because you never hold Dualhook's Meta app secret.
  • Embedded-signup inbox, or any inbox with an app secret present → Chatwoot enforces HMAC and returns 401 if the signature is missing or does not validate. Under Dualhook this would fail, because Meta signs the message path with Dualhook's app secret, which will never match a secret you hold.

So the working Chatwoot configuration for Dualhook is:

  1. Use the manual WhatsApp Cloud API inbox.
  2. Supply the Dualhook-revealed access token as the Chatwoot API key.
  3. Use the Chatwoot-generated webhook verification token on the Dualhook connection.
  4. Do not add a Meta app secret to this inbox, and do not flag it as embedded-signup.

Because there is no signature gate on this path, your real protections are the verify-token handshake plus an unguessable callback URL. Treat the URL like a secret: keep it out of tickets, public repos, screenshots, and chat threads; rotate it if it leaks; and front the endpoint with HTTPS (plus normal logging hygiene and edge protections on self-hosted). Chatwoot also deduplicates Meta's at-least-once retries, so duplicate deliveries do not create duplicate conversations.

Sending Templates and Outbound

Once connected, Chatwoot sends outbound messages directly to Meta using the access token and phone_number_id you stored on the inbox. Dualhook is not in the send path (though it can reveal the access token to you). Internally, Chatwoot posts to the Graph API messages endpoint with the inbox's token.

Free-form replies are allowed only inside the 24-hour customer-service window. To start a conversation or re-engage outside that window, you must use an approved template:

  • Templates are created and approved in WhatsApp Manager (Meta's side); Chatwoot does not author them.
  • Approved templates sync into the Chatwoot inbox automatically on a schedule, and you can trigger a manual Sync Templates from inbox settings.
  • Agents pick a template from the composer's template picker and fill in variables. Chatwoot's enhanced templates support media headers and buttons.

To sanity-check the same credentials outside Chatwoot, Meta's current examples use the v25.0 messages path:

curl -X POST "https://graph.facebook.com/v25.0/${PHONE_NUMBER_ID}/messages" \
  -H "Authorization: Bearer ${WHATSAPP_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "15551234567",
    "type": "template",
    "template": {
      "name": "order_update_v1",
      "language": { "code": "en_US" }
    }
  }'

For variables, media headers, and button parameters, see Sending Template Messages.

Test with Dualhook

  1. Confirm the Chatwoot inbox exists and you have copied its Webhook URL and Webhook verification token.
  2. In Dualhook, set the destination URL and verify token to those exact Chatwoot values.
  3. Run Dualhook Test Ping to confirm the destination is reachable.
  4. Confirm Meta verification succeeded — Chatwoot logs Whatsapp webhook verified, and the WABA's messages field is subscribed.
  5. Send a real WhatsApp message to your business number and confirm a conversation appears in the Chatwoot inbox.
  6. Reply from Chatwoot and confirm delivery, and that delivery/read statuses update on the message.
  7. Re-run an end-to-end test after any token refresh, Chatwoot upgrade, reverse-proxy change, or WAF rule change.

Production Caveats

  • Chatwoot stores conversation history. Unlike Dualhook, Chatwoot retains contacts, message bodies, and media. Set retention, access, and export policies in Chatwoot to match your compliance posture. See Compliance & Data Retention.
  • Chatwoot's BSUID/username support is incomplete — test before relying on it. From March 31, 2026, message webhooks carry Meta's business-scoped user ID (user_id); the API supports sending to BSUIDs from May 2026; and from June 2026 username-adopting users can omit wa_id/from. Recent Chatwoot releases added BSUID payload parsing, but contact resolution still keys identity on the phone number (wa_id), so when wa_id/from are absent or arrive as BSUIDs you can hit duplicate contacts or failed replies — this is an open upstream gap (Chatwoot issue #13837). Keep Chatwoot upgraded, test username/BSUID payloads end-to-end, and expect contact-resolution gaps until upstream lands full support. See the BSUID Transition Guide.
  • Per-message pricing. WhatsApp moved from conversation-based to per-message pricing on July 1, 2025. You are charged when a template message is delivered, at rates that vary by template category and the recipient's country calling code; service replies inside the 24-hour window are free. Pricing is Meta's, not Dualhook's or Chatwoot's.
  • Coexistence. If you keep using the WhatsApp Business app on the same number, use Dualhook's coexistence onboarding. In coexistence, app-sent messages arrive at the webhook as smb_message_echoes (Chatwoot handles these), API-sent messages can appear in the app, and the WhatsApp Business app must be opened periodically (Meta's ~13-day guidance) or webhook delivery can degrade. See WhatsApp Coexistence and Maintain Account Health.
  • One override per WABA; one callback route per number. Dualhook's webhook override is WABA-scoped (one override_callback_uri + verify token per WABA), while Chatwoot's route is phone-number-specific. For multiple numbers, create one inbox per number; each gets its own <phone_number> path and verify token. Keeping one number per WABA makes the override deterministic and easy to audit.
  • Self-hosted reachability. Meta must reach Chatwoot over public HTTPS. If the host goes down or loses its certificate, Meta deliveries fail until it is healthy again.

Related

  • WhatsApp Webhook OverrideHow Dualhook uses WhatsApp Webhook Override to route message webhooks directly from Meta to your server.
  • Messaging WebhookReal-time webhook events for inbound messages, delivery statuses, and errors.
  • Sending Template MessagesHow to send template messages via Cloud API with variables, media headers, and URL buttons.
  • WhatsApp CoexistenceHow Coexistence mode works: using WhatsApp Business App and Cloud API on the same number.
Browse more docsStart Free Trial