Feature: Channels
Channels are the communication surfaces through which a client team contacts their assigned Specialist persona. Each inbound message is normalized and routed into the conversation system regardless of source channel. Channel routing is one direction: inbound from the client, outbound from the Specialist (operated by Humanity Protocol's Expert + AI agent team).
โ ๏ธ Historical audit context โ see archived audit archive/MVP_CLIENT_CHANNELS_AUDIT_2026-05-22.md for the full readiness PM-cut snapshot from 2026-05-22. The headline finding (outbound dispatch broken on non-email channels) has since been resolved by PR #567 (
ChannelDispatcherService); #576 / PR #621 closed the auto-respond mirror. Inbound and outbound media now flow end-to-end on Email/WhatsApp/Slack/Telegram viaDispatchExpertReplyParams.attachments(#669 closes #559). Telegram signature verification fix landed infix(security): correct JWT expiry docs and fix Telegram webhook auth(commit080f458). Read the status table below with these caveats.
Channel Overviewโ
| Channel | Status | Auth Method | Notes |
|---|---|---|---|
| โ Live (Gmail API + DWD; CF Worker fallback during 2-week soak) | Gmail API polling via cron + Resend (transactional only) | 2026-06-05 cutover (#1722, closes #1060): inbound now comes via Gmail API polling (*/30 * * * * *) against per-OSA Workspace mailboxes; outbound replies are sent from the Specialist mailbox via DWD service-account impersonation (single GCP service account, no per-OSA OAuth tokens). Cloudflare Worker remains as fallback during a 2-week prod soak โ tracking issues #1440, #1441 stay open until retirement. See three-system overview below + Long-Term Plan section. | |
| โ Live (Twilio) | HMAC-SHA1 (Twilio sig) | One inbound number per environment (WHATSAPP_PHONE_NUMBER); all orgs on the env share it. Sender identity (which org a phone belongs to) is established by self-service pairing codes (whatsapp_linked_numbers). Open: identity attribution bug #677 (P0). | |
| Slack | โ Live (Connect flow + manifest paste) | HMAC-SHA256 (per-org signing_secret from integration_credentials) | Connect flow flipped Coming-Soon โ Available in #816 (closes #706). Manifest suggester at /client/admin/channels/slack via GET /orgs/:orgId/channels/slack/manifest (#1034). Inbound now lives in SlackModule (api/src/channels/slack/) per #1212/#1158. Per-org signing_secret + bot_token resolved via integration_credentials; env-var fallback is dev-only / disallowed in production (#1219, BLK-004). |
| Telegram | โ Live | HMAC-SHA256 secret token | |
| Web (widget) | โ Live (embeddable) | Visitor JWT via POST /public/widget/session (24h TTL) | Embeddable customer-facing chat widget (channel: "web"). Standalone JS bundle a client embeds on their own site; visitors chat with their Specialist over REST + Socket.IO. Originally PR #926 (closes #43). See Web (embeddable widget) below, the feature spec, and the embed integration guide. |
| Teams | โ ๏ธ Stub | Bot Framework | Needs credentials (#122) |
| โ ๏ธ Stub | App secret | XML parsing not yet implemented |
Config scope on /client/settings/channels (org-level vs per-user)โ
The client Channels settings page is an org-level surface: every role
that can access it (org_role owner/admin, plus superadmin) sees the same
configuration, because the data is scoped by org_id โ one config per org, not
per user.
| Section | Backend scope | Same for all roles? |
|---|---|---|
| Integrations (Slack / Email / Teams credentials) | WHERE org_id (integrations/credentials/credentials.service.ts findAll) | โ org-level |
| Email whitelist | WHERE org_id (organizations.service.ts getEmailWhitelist) | โ org-level |
| Slack install status | WHERE org_id + type (channels/slack OAuth status) | โ org-level |
| WhatsApp linked numbers | WHERE user_id (whatsapp-pairing.service.ts listForUser) | โ per-user (by design) |
WhatsApp linked numbers are per-user, not org-level โ intentionally. A WhatsApp link binds an individual's phone to the org's WhatsApp channel (
whatsapp_linked_numberscarries bothuser_idandorg_id, butGET /whatsapp/linked-numbersfilters by the caller'suser_id). So two admins of the same org can legitimately see different WhatsApp link lists (each sees their own). This is expected โ personal data surfaced on the org settings page, not an org-level config inconsistency. Everything else on the page is org-scoped and identical across roles.
Emailโ
Overviewโ
h.work uses three distinct email systems. Do not confuse them.
| System | Purpose | Provider | Who configures |
|---|---|---|---|
| Transactional | Invites, notifications, OTPs | Resend (RESEND_API_KEY) | HP internal |
| Specialist inbound (primary, 2026-06-05+) | Clients email bob@{HWORK_DOMAIN}`` | Gmail API polling + DWD (System 4 below) | HP infra |
| Specialist inbound (fallback during 2-week soak) | same | Cloudflare Email Routing โ webhook (System 2 below; tracking #1441) | HP infra |
| Expert personal | HP staff mailboxes (david@ex.{HWORK_DOMAIN}``) | Google Workspace | HP IT |
2026-06-05 status: System 4 (Gmail API polling + DWD impersonation) shipped via PR #1722 and is now the primary inbound + outbound path for Specialist email. System 2 (Cloudflare Email Routing) remains as a fallback during a 2-week prod soak; #1441 tracks the final decommission. The Sytem 2 description below is preserved for the soak window โ read System 4 first.
System 1 โ Transactional Email (Resend)โ
Used for platform-generated emails: onboarding invites, OTP codes, welcome emails, AM completion notifications.
- Provider: Resend (
RESEND_API_KEYenv var, already set in Railway) - From address:
h.work <onboarding@h852.work>(staging) โonboarding@h.workonce domain is verified in Resend - Code:
api/src/email/email.service.ts(EmailService) - Pending: h.work domain needs to be verified in Resend dashboard for production use
System 2 โ Specialist Inbound Email (Cloudflare โ Webhook โ Resend)โ
โ ๏ธ Current implementation (MVP). This system treats the Specialist email as a lightweight routing alias. The long-term plan is to replace this with full Google Workspace identities (see Long-Term Plan: Google Workspace Identities below).
Used when clients email their assigned Specialist (bob@h.work) to start or continue a conversation.
Architecture:
Client email โ bob@h.work (Cloudflare MX record on h.work)
โ
โผ
Cloudflare Email Routing
(forward rule: *@h.work โ /channels/email/inbound)
โ
โผ
POST /channels/email/inbound (our webhook)
โ
โผ
ConversationsService.findOrCreateByChannel()
โ
โผ
Expert queue for review
โ
โผ
Expert approves โ Resend API sends reply from bob@h.work (DKIM)
Key details:
- Specialist email is a routing alias โ not a real mailbox; Cloudflare routes inbound, Resend sends outbound
- Sender authorization is two-tier: org membership is the primary check (the FROM address must belong to a user with a row in
org_membershipsfor one of the Specialist's orgs); the legacyemail_whiteliststable is consulted only as a fallback for senders without a portal account (e.g. personal addresses) - Unknown senders receive a one-shot auto-reply ("This specialist inbox is for registered users onlyโฆ") sent from the Specialist's own routing address so threading visually matches
- Reply threading via
In-Reply-Toheader โ subsequent emails attach to existing conversation - Customer ID is deterministic:
email:<from_address> - Outbound replies sent via Resend API, DKIM-signed as the Specialist routing address
- Returns 200 immediately; processing is async (
setImmediate) - NOT SendGrid โ old code used SendGrid Inbound Parse; this has been replaced with Cloudflare Email Routing + Resend
- All inbound/outbound email events logged to
email_audit_logtable - Per-subject threading via
In-Reply-To;channel_message_idused for dedup
Setup requirements (HP infra):
| Requirement | Details |
|---|---|
| Cloudflare Email Routing | Enable on {HWORK_DOMAIN} in Cloudflare dashboard |
| Cloudflare Worker | Deploy relay worker (see docs/ops/EMAIL-CHANNEL-SETUP.md) |
| Forward rule | *@{HWORK_DOMAIN}`` โ Cloudflare Worker โ POST /channels/email/inbound |
| MX record | Cloudflare sets automatically when Email Routing is enabled |
| Domain in Resend | Verify {HWORK_DOMAIN} in Resend dashboard for DKIM on outbound replies |
Per-org credentials (stored in integration_credentials table, optional override):
| Key | Description |
|---|---|
resend_api_key | Resend API key for outbound (falls back to RESEND_API_KEY env var) |
from_email | Specialist's routing address (e.g. bob@h.work) |
System 3 โ Expert Personal Email (Google Workspace)โ
HP staff (david@ex.h.work) have personal mailboxes via Google Workspace on the ex.h.work subdomain.
- These are NOT routed through the platform
- Expert responses to clients always go out as the Specialist address (
bob@h.work), never fromdavid@ex.h.work - HP IT manages Google Workspace externally; no platform code involved
System 4 โ Gmail API inbound polling (per-OSA Workspace mailboxes) โ #1722โ
2026-06-05 status: Gmail API polling shipped via PR #1722 (commit
d8333558, closes #1060). This supersedes the original Pub/Sub design (#1432) and is now the primary inbound path. The Cloudflare relay (System 2) remains as a fallback during a 2-week prod soak; tracking issues #1440 and #1441 stay open until retirement.
For OSAs that have a provisioned Google Workspace mailbox, inbound email is
fetched via the Gmail API on a polling cron schedule (*/30 * * * * * on
Railway dev โ every 30 seconds). The earlier Pub/Sub push design was dropped
in favour of pull-mode polling for operational simplicity (no webhook to
maintain, no Pub/Sub topic lifecycle, idempotent on retry).
Architecture:
Client email โ {firstname}.{org-slug}@h.work (real Gmail mailbox)
โ
โผ
Gmail mailbox accumulates messages
โ
โผ
Railway cron (*/30 * * * * *) โ GmailIngestCron
โ iterates over provisioned OSAs
โ GCP service account (DWD) impersonates each OSA user
โ Gmail API users.messages.list (q: newer than last historyId)
โ users.messages.get for each new message
โผ
ConversationsService.findOrCreateByChannel()
Relationship to System 2 (Cloudflare): Gmail API polling is the primary
inbound path for provisioned OSAs; the Cloudflare relay remains as a fallback
during the 2-week soak and for un-provisioned OSAs. Routing is decided by
org_specialist_assignments.gsuite_status โ provisioned OSAs use polling,
others fall through to Cloudflare.
Follow-ups filed 2026-06-06: #2064 (RFC 2822 Message-ID handling), #2065 (historyId observability).
Long-Term Plan: Google Workspace Identitiesโ
Status (2026-06-05): โ SHIPPED โ DWD single-SA impersonation live (PR #1722, commit
d8333558, closes #1060). Inbound = Gmail API polling cron; outbound = Specialist mailboxsend-asvia DWD impersonation. Per-OSA OAuth tokens (ADR-0002 Phase 2) are deprecated โOAuthTokenServiceis no longer in the dispatch hot path; a single GCP service account with domain-wide delegation now impersonates each OSA mailbox. Cloudflare relay (System 2) remains as fallback during the 2-week prod soak.
Decision: Each Org ร Specialist Assignment (OSA) is provisioned a real Google Workspace user. The alias format is {firstname}.{org-slug}@{HWORK_DOMAIN} (note: not bob@h.work โ this gives every (Org, Specialist) pair its own Workspace identity). Specialists become fully autonomous identities that the runtime can operate programmatically via a single platform-owned service account with DWD.
What shipped (2026-06-05, PR #1722 supersedes ADR-0002 Phase 2):
| Component | PR | Description |
|---|---|---|
| Gmail API polling cron | #1722 | */30 * * * * * cron fetches new messages per provisioned OSA via DWD impersonation (replaces Pub/Sub design from #1432) |
| Outbound via DWD impersonation | #1722 | ChannelDispatcherService sends Specialist replies through Gmail API as the OSA user โ no per-OSA OAuth tokens involved |
WorkspaceProvisioningProcessor | #1076 | BullMQ worker that drives Admin Directory provisioning + lifecycle (still current) |
| OSA Gsuite lifecycle schema | #1065 | org_specialist_assignments.email_alias, workspace_user_id, gsuite_status |
WorkspaceLifecycleModule | #1074 | NestJS module wiring the above |
assignSpecialist provisioning hook | #1089 | OrganizationsService.assignSpecialist enqueues a provisioning job |
| Specialist Email field dropped from Create form | #1097 | The catalog Specialist no longer carries an Email field โ the alias is per-assignment, not per-catalog-Specialist |
OAuthTokenService | Per-OSA OAuth token store โ superseded by DWD impersonation in #1722; no longer in the dispatch path | |
| Replaced by DWD impersonation in #1722 |
Architecture (live):
GCP Service Account (single, platform-owned)
+ domain-wide delegation on h.work Workspace
โ
โโ impersonate {firstname}.{org-slug}@h.work โ Gmail API (polling + send)
โโ impersonate {firstname}.{org-slug}@h.work โ Calendar API (create events, freebusy, Meet links)
โโ impersonate {firstname}.{org-slug}@h.work โ Drive API (share files, manage folders)
โ
โโ Admin Directory API (no impersonation needed)
โโ POST /admin/directory/v1/users โ WorkspaceProvisioningProcessor on assignSpecialist
โโ DELETE /admin/directory/v1/users/{key} โ on OSA removal
Key properties (as shipped 2026-06-05):
- Single service account JSON key in Railway env โ no per-OSA OAuth tokens stored or refreshed at runtime
- Domain-wide delegation grants set once in the Google Admin Console by HP IT
- All Gmail/Calendar/Drive API calls impersonate the target per-OSA Workspace user via DWD
- Provisioning triggered from
OrganizationsService.assignSpecialistโ enqueues aWORKSPACE_JOB_CREATEjob (#1076/#1089) - Inbound: Gmail API polling cron (
*/30 * * * * *); Cloudflare relay is the fallback during 2-week soak - Outbound: Gmail API
send-asthe per-OSA user via DWD; Resend is not used for Specialist conversation outbound (Resend remains only for invites/onboarding/transactional viaemail/email.service.ts) - Cost: ~$6/user/month (Google Workspace Business Starter). Licences scale with active OSA count
What stays the same:
- Outbound always from a Specialist-class address โ a real per-OSA Gmail mailbox via DWD impersonation (Resend is not used for Specialist outbound)
- Persona integrity โ clients still only see the Specialist alias, never the Expert's personal address
email_audit_logtable continues to log all events
Sender Authorizationโ
The inbound handler checks senders in this order:
- Org membership (primary): join
org_membershipstousersonLOWER(users.email) = <FROM>andorg_id IN (<specialist's orgs>). Most users โ anyone with a portal login โ are authorized this way without any extra setup. email_whitelistsexact email match (fallback): for users without a portal account who still need to email in.email_whitelistsdomain match (fallback): wildcard authorization for an entire corporate domain.- No match โ unknown sender: auto-reply "This specialist inbox is for registered users onlyโฆ" from the Specialist's address, then drop. Logged to
email_audit_logwithaction=auto-replied:unknown_sender.
Whitelist Management APIโ
GET /onboarding/:token/whitelistโ list entriesPOST /onboarding/:token/whitelistโ add entry (during onboarding)DELETE /onboarding/:token/whitelist/:idโ remove entry- Default: empty. Most orgs no longer need any whitelist entries โ invited team members are authorized via
org_memberships. Add entries only for senders who don't have portal logins.
WhatsAppโ
WhatsApp is delivered via one Twilio number per environment (staging has its own number, production has its own). All clients on the env share that single inbound number; the Specialist/persona identity (Bob, Nova, etc.) is carried entirely at the conversation/Specialist layer, not by the dialled phone number. Senderโorg identity is established at runtime via self-service pairing codes generated from the client portal.
Single-Number Modelโ
Each environment has exactly one inbound WhatsApp number, configured via the WHATSAPP_PHONE_NUMBER env var. The platform looks up which org a message belongs to by the sender's phone (From) via whatsapp_linked_numbers, never by the receiving number (To). One number can therefore serve every org on the env.
Outbound replies always go from the same number that received the message (i.e. WHATSAPP_PHONE_NUMBER).
Legacy: the
whatsapp_number_pooltable and the per-Specialist pool-assignment flow were the v1 design. They are retained for back-compat but no longer required for routing โgetOrgWhatsAppNumber()readsWHATSAPP_PHONE_NUMBERdirectly.
Pairing Code Flow (canonical sender onboarding)โ
- User signs into the portal โ Settings โ WhatsApp (
/client/settings/whatsapp/). - Dashboard generates a one-time
HW-XXXXcode (15-minute TTL, copy + refresh buttons, live countdown). Stored inwhatsapp_pairing_codeswithuserId+orgId. - User texts the code to the env's WhatsApp number on WhatsApp.
- Inbound webhook detects the
HW-XXXXpattern, consumes the code, and creates a row inwhatsapp_linked_numbers (userId, orgId, phoneE164). A confirmation reply "Connected to{OrgName}" is sent. - Future messages from that phone route automatically to the user's org conversation.
- The same Settings page lists linked numbers and lets the user unlink them.
Constraints:
- One phone โ one org max. Re-pairing the same phone to a different org is rejected with a "you're already connected, unlink at /client/settings/profile" reply, and the new code is not burned.
- A user may link multiple phones to the same org (e.g. personal + work).
- Invalid / expired / already-used codes get a "not recognised, generate a new one from the dashboard" reply; the code in the message is ignored, not consumed.
Inbound Routing Priorityโ
The inbound handler (routeTwilioWhatsApp in api/src/channels/channels.controller.ts) tries these in order:
- Pairing-code message โ if the body matches
^HW-[A-Z0-9]{4}$(case-insensitive) โ consume code + reply confirmation (or rejection per the rules above). Checked first so a user can re-pair even if already linked. whatsapp_linked_numberslookup onFromโ route to the linked org's conversation.whatsapp_sender_whitelistlookup (To+From) โ legacy fallback for orgs that pre-date the pairing flow.- Unknown sender โ auto-reply pointing to the dashboard at
https://app.{HWORK_DOMAIN}/client/settings/profile(rate-limited once per 24h per sender via Redis).
Architectureโ
Inbound WhatsApp message
โ
โผ
WhatsApp Servers (Meta)
โ
โผ
Twilio WhatsApp Gateway
โ POST /channels/whatsapp/inbound
โ X-Twilio-Signature (HMAC-SHA1)
โผ
humanwork-api (NestJS)
โ 1. Verify Twilio HMAC-SHA1 signature
โ 2. Deduplicate via Redis (MessageSid, 1-hour TTL)
โ 3. Match against routing priority (pairing code โ linked โ whitelist โ unknown)
โ 4. For pairing codes: write whatsapp_linked_numbers, mark code used
โ 5. For known senders: dispatch to ConversationsService / ExpertQueue
โผ
ConversationsService โ ExpertQueue
Tablesโ
| Table | Purpose |
|---|---|
whatsapp_linked_numbers | Pairing result: (userId, orgId, phoneE164) โ the canonical routing key |
whatsapp_pairing_codes | Live HW-XXXX codes with TTL, userId, orgId |
whatsapp_number_pool | Legacy. No longer required for routing; WHATSAPP_PHONE_NUMBER env var supersedes it. Admin endpoints remain for back-compat |
whatsapp_sender_whitelist | Deprecated. Legacy routing fallback only. Do not configure for new orgs |
Other Key Detailsโ
- Outbound replies are sent via Twilio REST API from the same number that received the message (always
WHATSAPP_PHONE_NUMBERfor the env). - Webhook:
POST /channels/whatsapp/inboundโ Twilio deliversapplication/x-www-form-urlencodedpayload. - Signature verification: HMAC-SHA1.
TWILIO_AUTH_TOKEN(main account) is preferred, withTWILIO_SUBACCOUNT_TOKENas fallback for subaccount-owned senders. The env's WhatsApp number lives on the main account, so verification typically succeeds againstTWILIO_AUTH_TOKEN.
Configuration (Railway env vars)โ
| Variable | Description |
|---|---|
WHATSAPP_PHONE_NUMBER | E.164 inbound number for this env (one per env). All orgs share it |
TWILIO_ACCOUNT_SID | Main account SID โ used for outbound REST calls (WhatsApp senders are registered on the main account) |
TWILIO_API_KEY | Main account API Key SID โ used to authenticate outbound sends |
TWILIO_API_SECRET | Main account API Key Secret โ paired with TWILIO_API_KEY |
TWILIO_AUTH_TOKEN | Main account Auth Token โ preferred token for inbound signature verification |
TWILIO_SUBACCOUNT_SID | Per-env subaccount SID |
TWILIO_SUBACCOUNT_TOKEN | Per-env subaccount Auth Token โ fallback signature verification when sender is owned by the subaccount |
TWILIO_WEBHOOK_BASE_URL | Public base URL for this env (e.g. https://api.h.work) |
For full setup, see docs/ops/TWILIO-SETUP-CHECKLIST.md.
Slackโ
Statusโ
โ
Live (Connect flow + manifest paste). Per-org credentials are stored encrypted in integration_credentials; the env-var fallback (SLACK_SIGNING_SECRET, SLACK_BOT_TOKEN) is dev-only and disallowed in production (#1219, BLK-004).
- Module location:
api/src/channels/slack/{slack.controller,slack.service,slack-engagement.service,slack-search.service}.tsโ extracted fromchannels.controller.tsin #1212 / #1158. - Client Connect card:
/client/admin/channels/slackโ flipped Coming-Soon โ Available in #816 (closes #706). - Manifest suggester:
GET /orgs/:orgId/channels/slack/manifestreturns a paste-ready Slack App manifest scoped to the org's webhook base URL (#1034). - "Add to Slack" OAuth install: #1498 lands the OAuth-based install flow (one-click "Add to Slack" button + redirect handler that exchanges the code for a bot token and stores it in
integration_credentials). Operational runbook:docs/runbooks/slack-oauth-install.mdfor the manifest paste flow vs. the OAuth install flow. - Mention-gated channel ingest: the Slack adapter only ingests channel messages that mention the bot; full-channel ingest is gated behind the
slack_engaged_threadsbook-keeping. Off-mention chatter is dropped at the adapter (#818). Thread context is cached inchannel_context_messagesfor the agent; an hourly retention sweep prunes inactive threads.
Architectureโ
Slack event (app_mention or message in an engaged thread)
โ
โผ
POST /channels/slack/events (SlackController, api/src/channels/slack/)
โ HMAC-SHA256 signature verification using per-org signing_secret
โ resolved from integration_credentials (SlackService.resolveOrgId)
โ 5-minute replay window
โผ
SlackEngagementService โ decide ingest vs. drop (mention-gated)
โ
โผ
ConversationsService.findOrCreateByChannel()
โ customerId = "slack:{teamId}:{channelId}"
โผ
Agent processes โ response posted to Slack channel (per-OSA impersonation)
- Returns 200 immediately (async processing)
- Handles
url_verificationchallenge for Slack App setup - Replay attack protection via
X-Slack-Request-Timestamp - Outbound supports media attachments per #669; Slack outbound media is wired through
ExpertReplyDto.attachmentsin #1318 (closes #1157) โ see Reliability primitives below.
Configuration Keys (per-org, stored in integration_credentials)โ
| Key | Description |
|---|---|
bot_token | Slack Bot OAuth token (xoxb-...) โ env var SLACK_BOT_TOKEN is dev-only fallback |
signing_secret | From Slack App settings โ env var SLACK_SIGNING_SECRET is dev-only fallback |
app_id | Optional โ for multi-app routing |
Telegramโ
Architectureโ
POST /channels/telegram/webhookโ receives Bot API updates- Validates
X-Telegram-Bot-Api-Secret-Tokenheader (HMAC-SHA256) - Routes to conversation system
Configuration Keysโ
| Key | Description |
|---|---|
bot_token | Telegram Bot token |
webhook_secret | Optional HMAC secret for webhook validation |
Status: Live. Implemented in fix/issue7-telegram branch, merged to master.
Web (embeddable widget)โ
The web channel (channel: "web") is an embeddable, customer-facing chat
widget a client drops onto their own website. Unlike the other channels โ which
are inbound webhooks/polling against vendor APIs โ this one runs in the
visitor's browser and talks to the h.work API directly. It is the only channel
where the end visitor is anonymous (no vendor-side identity) and is authenticated
by a short-lived visitor JWT rather than a provider signature.
Architectureโ
- Standalone JS bundle
@humanwork/chat-widget(packages/chat-widget/), embedded via<script>on the client site. POST /public/widget/sessionmints a 24h visitor JWT for{ orgSlug, visitorId? }(@Public(), 30 req/min, wildcard CORS). Unknown org โ 404; malformedvisitorIdโ 400.- The widget then uses the standard conversation API as a
client-role caller (GET/POST /conversations,GET /conversations/:id,POST /conversations/:id/messages) and the/notificationsSocket.IO namespace for realtime (agent_message_sent,agent_typing,join_conversation/leave_conversation). - Responses are masked by
ClientConversationViewInterceptor(#2353) so a visitor cannot distinguish a human Expert from the AI.
Auth Methodโ
| Layer | Mechanism |
|---|---|
| Session mint | POST /public/widget/session โ JWT (role: "client", widgetVisitorId, single-org orgMemberships, 24h TTL, #2925) |
| API calls | Authorization: Bearer <visitor-jwt>; auto-refresh near expiry, transparent re-mint on 401 |
| Visitor identity | per-org UUID persisted in browser localStorage (humanwork.chat.visitorId.{orgSlug}) |
Status: Live. Originally PR #926 (closes #43); session auth + realtime added since. Full details in the feature spec; embedding instructions (CORS/CSP, script tag, theming) in the embed integration guide.
Teams (Microsoft)โ
Stub implementation only. Bot Framework connector is wired but not tested.
POST /channels/teams/webhookโ handler exists, marked// needs-testing
Status: NOT_STARTED (BLK-032). Requires Microsoft App registration + Bot Framework credentials.
WeChatโ
Stub implementation only.
GET /channels/wechat/webhookโ verification challenge handlerPOST /channels/wechat/webhookโ inbound stub (XML parsing not implemented)
Status: NOT_STARTED (BLK-042, P3). Requires WeChat Official Account registration.
Channel Admin Endpointsโ
These endpoints provide operational visibility for org admins:
| Endpoint | Description |
|---|---|
GET /orgs/:orgId/channels/status | Health status for all configured channels |
GET /orgs/:orgId/channels/webhook-urls | Exact webhook URLs to paste into channel dashboards |
Integration Credentials Managementโ
All per-org channel credentials are stored encrypted in integration_credentials (AES-256-GCM). API responses mask credential values (first 4 chars + ****).
| Endpoint | Description |
|---|---|
POST /orgs/:orgId/integrations | Create or upsert channel credentials |
GET /orgs/:orgId/integrations | List integrations (masked) |
DELETE /orgs/:orgId/integrations/:id | Remove integration |
POST /orgs/:orgId/integrations/:id/test | Test connection (lightweight API call) |
Reliability Primitives (cross-channel)โ
These primitives apply to all live channels (Email/WhatsApp/Slack/Telegram). They were factored out of the channel-specific code paths during the 2026-05 reliability sweep.
| Primitive | Source | Description |
|---|---|---|
CircuitBreaker | #848 | Per-channel breaker that opens on consecutive transport failures; outbound dispatch fails fast while the breaker is open and emits a metric |
| Channel DLQ + retry worker | #850 (closes #744) | Failed inbound webhooks land in channel_dead_letter; the retry worker re-drives them with exponential backoff |
| Prometheus metrics | #1023 (closes #757) | DLQ depth, outbound BullMQ queue depth + failure rate, breaker state, dedup hits โ all exported under /metrics |
| Outbound dispatch BullMQ queue | #936 | Outbound expert replies route through a BullMQ queue instead of setImmediate; pod restarts no longer lose in-flight sends |
| DB-level dedup constraints | #929 | Unique indexes on (channel, channel_message_id) enforce dedup at the database layer regardless of in-process Redis cache state |
| Per-channel outbound media | #669 (closes #559); #1318 wires Slack outbound attachments through ExpertReplyDto (closes #1157) | Email / WhatsApp / Slack / Telegram all accept DispatchExpertReplyParams.attachments; the channel adapter handles native attachment APIs (Slack files.upload, Twilio MediaUrl, Gmail multipart, etc.) |
Roadmapโ
| Channel | Timeline | Notes |
|---|---|---|
| P3 | Requires WeChat Official Account | |
| Teams | P2 | BLK-032 โ needs Microsoft App registration + Bot Framework |