Flows

Website Chat Widget Flow

Embedding and using the website chat widget.

Website Chat Widget — Full Feature Flow

Feature area: Social Messaging / Website Chat
Branch: phase-2
Last updated: 2026-06-09

Superseded — real-time delivery only. As of 2026-06-12 the website chat widget uses SignalR WebSockets for real-time push in both directions. The poll endpoint (GET /widget/chat/{token}/messages) has been removed. See website-chat-websocket-flow.md for the current implementation. This document remains useful for: channel creation, embed snippet, store-side CRM endpoints, and the REST fallback (POST /widget/chat/{token}/messages) which is still supported.


Overview

The Website Chat Widget lets a store embed a live-chat widget on any website. Unlike WhatsApp and Instagram, there is no OAuth handshake — the store creates a channel via a single API call and receives an embed snippet. Customers interact through the widget; the store replies either manually from the CRM inbox or automatically via the AI agent.

Delivery for this channel is poll-based: the widget periodically asks the server for new messages. Delivery is now WebSocket-based (SignalR). The polling flow described below is superseded. No external API, no webhooks, no Meta credentials required.


How It Differs from WhatsApp / Instagram

WhatsApp / InstagramWebsite Chat
ConnectionOAuth → MetaSingle API call
Credential storageEncrypted token + app secretNone (token is the channel identity)
Inbound deliveryMeta sends POST to webhookWidget POSTs directly to Sendy
Outbound deliveryGraph API callDB write → widget polls
AI agentYesYes
Manual store replyYesYes

Entities Used

The same SocialChannel, SocialConversation, SocialMessage, and AiMessageJob entities from the social messaging feature are reused without modification. Website channels are identified by ChannelType = Website and use ExternalAccountId as the channel token (a random Guid.NewGuid().ToString("N")).

SocialChannel.ChannelToken is exposed in API responses only when ChannelType == Website — it is the same value as ExternalAccountId, surfaced separately for clarity.


Flow 1 — Store Creates a Website Channel

POST /api/v1/store/social/channels/website
Authorization: Bearer <store JWT>
Permission: store.social_channels.manage

Body:
{
  "displayName": "Support Chat",
  "aiProviderCode": "anthropic",        // optional, default: anthropic
  "aiModelCode": "claude-sonnet-4-6",   // optional, default: claude-sonnet-4-6
  "systemPromptOverride": null          // optional
}

Server (WebsiteChatService.CreateChannelAsync):

1. Verify store context (JWT → storeId)
2. EnsureCanConnectChannelAsync → check AllowsSocialMessaging + MaxSocialChannels
3. Check no existing Website channel for this store (409 if one already exists)
4. channelToken = Guid.NewGuid().ToString("N")
5. Create SocialChannel {
     ChannelType = Website,
     ExternalAccountId = channelToken,   ← this IS the token
     AccessTokenEncrypted = ""no Meta token needed
   }
6. Return CreateWebsiteChannelResponse

Response:

{
  "channelId": "3fa85f64-...",
  "channelToken": "a1b2c3d4e5f6...",
  "embedSnippet": "<script src=\"https://api.example.com/widget.js\" data-channel=\"a1b2c3...\" async></script>"
}

The store pastes embedSnippet into their website's HTML. One channel per store — attempting to create a second returns 409.

Note: GET /api/v1/store/social/channels/auth/website returns 400 — Website channels do not use OAuth. The guard lives in MetaOAuthService.BuildAuthorizationUrlAsync.


Flow 2 — Customer Sends a Message

The widget calls this endpoint directly (no authentication required):

POST /api/v1/widget/chat/{channelToken}/messages

Body:
{
  "sessionId": "visitor-uuid-or-fingerprint",
  "text": "Hello, I have a question about my order",
  "contactDisplayName": "Ahmed"          // optional, set once
}

Server (WebsiteChatService.SendMessageAsync):

1. Validate channelToken → load SocialChannel (ChannelType=Website, not deleted)
2. Check channel.IsEnabled (422 if disabled)
3. Upsert SocialConversation keyed on (SocialChannelId, sessionId):
     - First message: create conversation with Status=Open
     - Subsequent messages: update ContactDisplayName if not yet set
4. Create SocialMessage { Direction=Inbound, Status=Delivered }
5. If channel.IsEnabled && channel.IsAgentEnabled:
     Create AiMessageJob { Status=Pending } → AI will reply automatically
6. Return full conversation history (all messages for this sessionId)

Response:

{
  "conversationId": "guid...",
  "messages": [
    { "id": "...", "direction": "Inbound",  "contentText": "Hello...", "isAiGenerated": false, "createdAt": "..." },
    { "id": "...", "direction": "Outbound", "contentText": "Hi! How can I help?", "isAiGenerated": true, "createdAt": "..." }
  ]
}

The widget uses sessionId to tie all messages to the same visitor. The choice of sessionId is left to the frontend — a localStorage UUID works well.


Flow 3 — Widget Polls for New Messages

The widget calls this endpoint on an interval to pick up replies:

GET /api/v1/widget/chat/{channelToken}/messages
  ?sessionId=visitor-uuid
  &after=2026-06-09T10:05:00Z    // optional — only return messages after this timestamp

Server (WebsiteChatService.PollMessagesAsync):

1. Load channel by token
2. Load conversation by (channelId, sessionId)
3. If no conversation yet → return empty message list (not an error)
4. Query SocialMessages where ConversationId = conv.Id AND CreatedAt > after
5. Return messages ordered by CreatedAt ASC

The after parameter lets the widget do incremental polling — only fetching messages it hasn't seen yet. On first load, omit after to get the full history.


Flow 4 — AI Agent Replies Automatically

When IsAgentEnabled = true, the Hangfire background job picks up the AiMessageJob created in Flow 2:

AiMessageDispatchJob (runs every minute):

1. Load pending jobs for Website channels
2. Fetch last 20 messages as conversation history
3. Build system prompt (AiSystemPromptBuilder)
4. Call configured AI provider (Anthropic / OpenAI)
5. On success:
     - Create SocialMessage { Direction=Outbound, IsAiGenerated=true }
     - NO external API call (Website channel — DB write is the delivery)
     - Update AiMessageJob { Status=Sent }
     - SocialTokenTracker.RecordUsageAsync(inputTokens, outputTokens)
6. On failure:
     - Exponential backoff; max 3 attempts before Status=Dead

The reply appears in the widget on the next poll — typically within seconds.


Flow 5 — Store Staff Replies Manually

Store staff open the CRM inbox and send a message from the dashboard:

POST /api/v1/store/social/conversations/{conversationId}/messages
Authorization: Bearer <store JWT>
Permission: store.conversations.manage

Body: { "text": "Your order ships tomorrow!" }

Server (SocialConversationService.SendMessageAsync):

1. Load conversation → verify it belongs to this store
2. Check channel.IsEnabled
3. ChannelType == Website → skip external API call
4. Create SocialMessage {
     Direction = Outbound,
     IsAiGenerated = false,
     SentByUserId = current.Id,
     Status = Sent
   }
5. Update conversation.LastMessageAt
6. Return SocialMessageResponse

The widget picks up the reply on its next poll — same delivery path as the AI agent.


Complete Flow Diagram

Store admin                       Sendy API                        Customer widget
    │                                 │                                    │
    ├─ POST /channels/website ────────►│                                    │
    │◄──── { channelToken, snippet } ─┤                                    │
    │                                 │                                    │
    │  (pastes snippet into website)  │                                    │
    │                                 │                                    │
    │                                 │◄─── POST /widget/chat/{token}/messages ──┤
    │                                 │     { sessionId, text }            │
    │                                 │                                    │
    │                                 ├── upsert Conversation              │
    │                                 ├── save SocialMessage (Inbound)     │
    │                                 ├── queue AiMessageJob               │
    │                                 │──── return messages ──────────────►│
    │                                 │                                    │
    │                      [Hangfire - every minute]                       │
    │                                 │                                    │
    │                                 ├── call AI provider                 │
    │                                 ├── save SocialMessage (Outbound)    │
    │                                 │                                    │
    │                                 │◄── GET /widget/chat/{token}/messages?after=... ┤
    │                                 │──── return new outbound message ──►│
    │                                 │                                    │
    ├─ POST /conversations/{id}/messages ►│                                │
    │  { text: "Manual reply" }       │                                    │
    │                                 ├── save SocialMessage (Outbound)    │
    │◄──── SocialMessageResponse ─────┤                                    │
    │                                 │◄── GET /widget/chat/{token}/messages?after=... ┤
    │                                 │──── return manual reply ──────────►│

Endpoints Reference

Store-side (requires JWT + permission)

MethodPathPermissionDescription
POST/api/v1/store/social/channels/websitestore.social_channels.manageCreate the website channel, get embed snippet
GET/api/v1/store/social/channelsstore.social_channels.viewLists all channels including website; channelToken is populated for Website type
PATCH/api/v1/store/social/channels/{channelId}store.social_channels.manageUpdate display name, AI model, system prompt
POST/api/v1/store/social/channels/{channelId}/toggle-agentstore.social_channels.manageEnable / disable AI auto-reply
DELETE/api/v1/store/social/channels/{channelId}store.social_channels.manageDisconnect / soft-delete the channel
GET/api/v1/store/social/conversationsstore.conversations.viewCRM inbox — lists all conversations across all channels
GET/api/v1/store/social/conversations/{conversationId}store.conversations.viewFull message history for one conversation
POST/api/v1/store/social/conversations/{conversationId}/messagesstore.conversations.manageSend a manual reply
POST/api/v1/store/social/conversations/{conversationId}/resolvestore.conversations.manageMark conversation resolved
POST/api/v1/store/social/conversations/{conversationId}/reopenstore.conversations.manageReopen a resolved conversation

Widget-side (AllowAnonymous — public)

MethodPathDescription
POST/api/v1/widget/chat/{channelToken}/messagesCustomer sends a message; returns full conversation history
GET/api/v1/widget/chat/{channelToken}/messages?sessionId=&after=Widget polls for new messages

Database Migration

dotnet ef database update

Migration: AddWebsiteChannelType

Adds Website = 2 to the SocialChannelType enum at the database level. No new tables — the existing SocialChannels, SocialConversations, and SocialMessages tables handle this channel type transparently.


Source References

LayerFile
Widget controllerSendy.Api/Controllers/V1/Public/WebsiteChatWidgetController.cs
Channel creation endpointSendy.Api/Controllers/V1/Store/StoreSocialChannelsController.cs
Manual reply endpointSendy.Api/Controllers/V1/Store/StoreConversationsController.cs
Website chat serviceSendy.Application/Services/Messaging/WebsiteChatService.cs
Website chat interfaceSendy.Application/Interfaces/Services/Messaging/IWebsiteChatService.cs
Conversation service (manual reply)Sendy.Application/Services/Messaging/SocialConversationService.cs
AI dispatcher (Website branch)Sendy.Infrastructure/Jobs/AiMessageDispatchJob.cs
Channel type enumSendy.Domain/Enums/Messaging/SocialChannelType.cs
Request DTOsSendy.Application/DTOs/Requests/Messaging/WebsiteChatRequests.cs
Response DTOsSendy.Application/DTOs/Responses/Messaging/WebsiteChatResponses.cs

For the AI agent internals, token billing, and CRM inbox details, see social-messaging-flow.md.

Source: DOCS/flows/website-chat-widget-flow.md