Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
Omnichannel support (SAC)
The SAC module opens tickets from e-mail and WhatsApp, replies through the channel the message came from, and prepares an AI reply draft for the agent to review.
Status: ✅ Implemented — inbound over IMAP, WhatsApp through the Meta Cloud API and the Evolution API, and AI reply suggestions.
Four independent pieces, each of which can be enabled on its own:
| What | Requires |
|---|---|
| Customer replies land in the ticket | A reply domain with its own MX and a catch-all mailbox read over IMAP |
Mail to contact@/support@ opens a ticket | An IMAP profile pointing at the existing mailbox (no MX change) |
| WhatsApp opens a ticket | A whatsapp-official or evolution-api profile + a WhatsApp channel |
| AI reply draft | An AI integration profile |
Quick setup
- Settings → Integrations: create an Email profile with the IMAP (inbound) provider pointing at the mailbox you already use. Hit Test.
- Support → Channels: create a channel per source (for example
email-support, of kind Email). - Support → Mailboxes: add the monitored address, pick the channel and the IMAP profile. Use Test connection, then Sync now.
- Support → Inbound log: send a test e-mail and confirm it shows up — and, if it was discarded, why.
How the ticket is found
When a message arrives, SAC looks for the ticket in this order and stops at the first hit:
- Reply address —
[email protected], which we generated in theReply-To. - Thread headers —
In-Reply-To/Referencesmatching the id the provider returned when we sent. - Subject tag —
[#202608-000042], present in every e-mail sent to the requester. - An open ticket from the same sender on the same channel.
- Nothing matched → a new ticket is opened.
On WhatsApp the first three do not exist: the phone number identifies the conversation, so rule 4 applies directly.
Safety check: the protocol shows up in the subject of every e-mail and leaks on any forward. That is why steps 1–3 only append the message when the sender is the ticket requester, or already authored one of its messages. Otherwise a new ticket is opened, instead of letting anyone write into someone else's ticket.
A ticket that is already resolved or closed is not reopened: a late reply becomes a new ticket.
Reply domain
This is what makes the customer reply come back identified. Without it, the reply lands on the sending profile's from address, which says nothing about which ticket it belongs to.
- Pick a dedicated subdomain, for example
tickets.yourcompany.com. - Point a dedicated MX at it, on whichever provider you like, and create a catch-all mailbox — every
[email protected]must land there. - Settings → Configurations → Support: set Reply Domain to
tickets.yourcompany.com. - Register the catch-all mailbox under Support → Mailboxes, like any other.
From then on e-mails to the requester go out with Reply-To: <protocol>@tickets.yourcompany.com. A channel can carry its own reply domain if you need to separate brands.
The subdomain is dedicated on purpose: pointing the main domain MX here would take your corporate e-mail down.
Mailboxes (IMAP)
Reading is over IMAP because it is the only path that does not require changing the MX: the mailbox keeps working normally in Gmail, in Outlook and on the phone of whoever already uses it.
| Config field | Description |
|---|---|
host | Provider IMAP server |
port | 993 for direct TLS, 143 for STARTTLS |
username | Usually the address itself |
password | Mailbox password — or an app password (Google Workspace). Does not apply to Microsoft 365 |
secure | On = direct TLS on 993 |
Google Workspace / Gmail
Console: admin.google.com and myaccount.google.com
- In the Admin Console, make sure IMAP is allowed for the organization (Apps → Google Workspace → Gmail → IMAP access).
- On the account being read, turn on two-step verification and create an app password — the regular account password does not work for IMAP.
- Server:
imap.gmail.com, port993, direct TLS.
Microsoft 365 — use the Entra profile, not IMAP
The IMAP provider does not work with Microsoft 365. Microsoft permanently disabled Basic authentication for IMAP on 1 October 2022 — the official documentation is explicit: "Now no one (you or Microsoft support) can re-enable Basic authentication in your tenant." App passwords do not help either: the same page states the deprecation also prevents their use.
A shared mailbox additionally has sign-in blocked on Microsoft's own recommendation, so there is no password to use.
Exchange Online mailboxes are read through Microsoft Graph with app-only authentication, reusing the same Entra app registration HedHog already uses for login and Teams meetings — all three read the same integration profile.
Under Support → Mailboxes, pick the Entra profile instead of an IMAP one: the selector only lists providers that can read a mailbox in this installation. Everything else (channel, category, interval) works the same.
Mark as read does not apply here: reading uses its own cursor (a delta token), so marking changes nothing — and it would require write permission, widening access for no benefit.
The first sync opens no retroactive tickets: it only records the starting point, even on a mailbox with years of history.
Scoping it so the app cannot read every mailbox
Application Mail.Read reads every mailbox in the tenant by default. Restricting it is mandatory, and the current mechanism is RBAC for Applications (New-ApplicationAccessPolicy is marked legacy and, on top of that, rejects shared mailboxes as a direct target):
# The mailboxes must live in a mail-enabled security group.
New-ServicePrincipal -AppId <appId> -ObjectId <objectId of the Enterprise Application>
New-ManagementScope -Name "SAC" -RecipientRestrictionFilter "MemberOfGroup -eq '<group DN>'"
New-ManagementRoleAssignment -Role "Application Mail.Read" -App <appId> -CustomResourceScope "SAC"
Test-ServicePrincipalAuthorization -Identity <app> -Resource [email protected]
-ObjectId is the Enterprise Application one, not the App Registration's — using the wrong one fails authentication without saying why.
The trap that voids the whole protection: the two permission systems are a union, not an intersection. If
Mail.Readis consented in Entra and scoped through RBAC, the result is unrestricted access. Microsoft's documentation explicitly says to remove the EntraMail.Readassignment when using RBAC.
Do not use the portal's "Grant admin consent" button. The documentation warns that granting tenant-wide consent "may revoke permissions that have already been granted tenant-wide" — on an app that also signs your users in, that takes their access down. Grant the new app role with
New-MgServicePrincipalAppRoleAssignment, and export the current grants first (Get-MgOauth2PermissionGrant,Get-MgServicePrincipalAppRoleAssignment).
cPanel, Zoho and others
Use the IMAP server your provider reports, usually mail.yourdomain.com on port 993.
Mailbox options
| Option | What it is for |
|---|---|
| Folder | INBOX in most cases. Changing the folder restarts the read cursor, because IMAP UIDs are per folder. |
| Interval | Minimum 30s; the 60s default fits most cases. |
| Strip quoted reply | Cuts the quoted history. Without it, every reply brings the whole thread back and the ticket doubles in size. |
| Discard auto-replies | Drops vacation replies, bounces and list mail, recording the reason in the inbound log. |
| Mark as read | Keeps the mailbox clean for whoever also opens it in a mail client. |
The first read of a mailbox only picks up unread messages: adopting a mailbox with history must not mean opening a thousand retroactive tickets.
Each number becomes a row under Support → WhatsApp numbers, bound to an integration profile and to a WhatsApp channel. A profile can hold several numbers, and each number can land on a different channel.
The channel must be of kind WhatsApp: the channel kind is what makes the agent reply go out over WhatsApp. Pointing at an e-mail channel would make the reply try to leave by e-mail, to a requester who has no e-mail.
Meta Cloud API
Console: developers.facebook.com
- Create a WhatsApp integration profile with the WhatsApp Official API provider (see the WhatsApp recipe for the fields).
- Under Support → WhatsApp numbers, click Provision webhook and copy the URL.
- In the Meta console, under WhatsApp → Configuration, paste the URL into Callback URL and the profile verify token.
- Subscribe to the
messagesfield. - Fill Phone number ID with the
phone_number_idMeta reports.
Evolution API
Console: your instance dashboard
- Create a WhatsApp profile with the Evolution API provider (
host,token,instance_name). - Under Support → WhatsApp numbers, click Provision webhook and copy the URL.
- In the instance Webhook settings, paste the URL and enable the
messages.upsertevent. - Fill Instance with the exact instance name.
Evolution does not sign the request body. Authentication is the profile
apikey, compared in constant time, and the profile must have the token filled in — with no token, nothing is accepted. Make sure your instance sends theapikeyin the body or the header.
Messages sent by the company itself (fromMe) and group messages are discarded: the first would open a ticket on every agent reply, and the second has no single requester on the other end.
AI reply suggestions
On every new ticket and every customer reply, SAC prepares a draft for the agent. Nothing is ever sent automatically.
- Settings → Integrations: create an AI profile (OpenAI, Gemini, Claude or DeepSeek).
- Settings → Configurations → Support: turn on Enable AI reply suggestions and pick the AI Profile.
- Open a ticket: the draft shows up above the reply editor.
On screen the agent has three ways out:
- Confirm and send — publishes the draft as the reply, through the same path as a hand-typed one.
- Refine — describes the tweak in plain language ("shorter", "mention the 5 business day window") and gets a new version.
- Edit — drops the text into the editor for manual changes.
What goes into the context
Only the public messages of the ticket. Internal notes never do — that is where the team writes what is not meant for the customer. A few resolved tickets from the same category go in as a tone reference, favouring the well-rated ones.
| Setting | Default |
|---|---|
| History size | 10 messages |
| Similar tickets | 5 |
| Model | the one from the chosen profile |
Cost and containment
- The Enable AI reply suggestions switch ships off: spending tokens per ticket is the operator's decision.
- One automatic draft per received message, enforced by a database index — a WhatsApp conversation with ten messages in a row does not become ten model calls.
- The estimated cost of each call shows up in the core AI execution log.
Editing the prompts
The prompts live under Settings → AI Instructions and can be adjusted without a deploy. The system prompt is security sensitive: it carries the defence that makes customer content be treated as data, never as instructions. If an override removes that defence, the system discards the edited text and falls back to the original — the protection cannot be removed through the UI.
Inbound log
Support → Inbound log shows everything that arrived, including what was discarded and why. It is the screen that answers "the customer says they e-mailed and nobody replied".
| Status | What it means |
|---|---|
| Processed | Became a ticket, or a reply in one |
| Discarded | Auto-reply, list mail, bounce, loop, or no content |
| Failed | Error while interpreting; can be reprocessed from the screen |
Messages replayed by the provider are recognized and do not create duplicate tickets.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| "IMAP authentication failed" | On Google Workspace: account password instead of an app password. On Microsoft 365: no password works — the IMAP provider does not apply there |
| The folder does not exist | Different separator, such as INBOX.Support instead of INBOX/Support |
| Customer reply opens a new ticket | Reply domain not configured, or the customer replied from another address |
| WhatsApp webhook answers 403 | apikey differs from the profile token (Evolution), or wrong app secret (Meta) |
| Message arrives but no ticket | Number not registered under WhatsApp numbers, or the source is inactive |
| No AI draft shows up | Feature disabled, or no AI profile picked in the settings |