Self-hosted live chat with real-time messaging, AI chatbot, and modern dashboard.
| Component | Requirement |
|---|---|
| Runtime | .NET 8 ASP.NET Core Runtime Required |
| Database | SQL Server 2014+ (Express edition is fine) Required |
| OS | Windows Server 2016+, Windows 10+, or Linux |
| RAM | 1 GB minimum, 2 GB recommended |
| Disk | 200 MB for application, database grows with usage |
Download and install the ASP.NET Core Runtime 8.0 from:
https://dotnet.microsoft.com/download/dotnet/8.0
Choose the Hosting Bundle if deploying on IIS, or the ASP.NET Core Runtime for standalone deployment.
Create a blank database on your SQL Server instance:
CREATE DATABASE DChat;
Then run the schema script to create all tables:
sqlcmd -S YOUR_SERVER -d ZChat -i install-source\ZCHAT-APP-DB.sql
This creates the 14 base tables (SupportSession, SupportMessage, SupportAgent, SupportDepartment, SupportFeedback, SupportCustomer, SupportMissChat, User, SampleUsers, Settings, Portal, Rule, LogMessage, AgentOnline). The server creates the remaining feature tables (contacts, labels, channels, help center, automation and so on) automatically on first start.
Then insert default accounts:
sqlcmd -S YOUR_SERVER -d ZChat -i install-source\seed-data.sql
This creates a default admin and agent account so you can log in immediately.
Edit Web\appsettings.json and update the connection string:
{
"ConnectionStrings": {
"DChat": "Server=YOUR_SERVER;Database=ZChat;Trusted_Connection=true;TrustServerCertificate=true"
}
}
Server=YOUR_SERVER;Database=ZChat;User Id=sa;Password=YOUR_PASSWORD;TrustServerCertificate=true
Web\dchat.lic file DChat runs as the free Community edition with unlimited agents. If you have a Premium or Enterprise licence, place it at Web\dchat.lic in the application root.
Web\dchat.lic, restart the DChat server before signing in to the dashboard.
Fast local smoke test
run-local.cmd
This helper starts the packaged server on http://127.0.0.1:5050 using .\SQLEXPRESS and zchatdemo by default. Use it when you want a quick developer test without editing files first.
Option A: Standalone (recommended for testing)
cd Web
dotnet DChat.Web.dll --urls "http://0.0.0.0:5000"
Option B: Windows Service
sc create DChatServer binPath="C:\DChat\Web\DChat.Web.exe --urls http://0.0.0.0:5000"
sc start DChatServer
DChat.Web.exe / DChat.Web.dll (was ZChat.Web). Point an existing service at it - sc stop ZChatServer, sc config ZChatServer binPath= "C:\ZChat\Web\DChat.Web.exe --urls http://0.0.0.0:5000", sc start ZChatServer - or, for IIS with your own web.config, change ZChat.Web to DChat.Web in it. Then delete the old ZChat.*.dll/.exe/.pdb files from Web: while they are there, anything still starting ZChat.Web runs the old version. Licence files, connection strings, the database, widget embeds and webhook receivers keep working unchanged; see READ_ME.txt for the full list.Option C: IIS (with Hosting Bundle)
Web folderOption D: Docker
docker-compose up -d
The bundled Docker compose file creates the database (named ZChat, as before the product was renamed, so an upgraded install keeps its data) and applies the schema automatically on first startup.
Navigate to http://YOUR_SERVER:5000/dashboard in your browser.
Default credentials (created by seed-data.sql):
| Role | Username | Password |
|---|---|---|
| Admin | admin | admin123 |
| Agent | agent1 | agent123 |
Add one of these snippets before the closing </body> tag on your website:
<script src="http://YOUR_SERVER:5000/widget/dchat.iife.js"
data-server-url="http://YOUR_SERVER:5000"
data-site-id="default"
data-greeting="Hello! How can we help you?"
data-primary-color="#2563eb"></script>
<script src="http://YOUR_SERVER:5000/widget/dchat.iife.js"></script>
<script>
DChat.init({
serverUrl: 'http://YOUR_SERVER:5000',
siteId: 'default',
greeting: 'Hello! How can we help you?',
primaryColor: '#2563eb'
});
</script>
The chat widget will appear as a floating button in the bottom-right corner.
The Windows desktop app is a separate download, dchat-agent-console.zip (version 2.0.10). Unzip it anywhere and run DChat.Agent.exe; the .NET runtime and the Windows App SDK are bundled. Enter your server URL and sign in. Windows 10 1809 or later, 64-bit.
It connects over SignalR and includes:
{{variables}}Single sign-on from the app, signatures and template variables need a 2.0.6 server. Against a 2.0.5 server the SSO button stays disabled and variables are inserted as written.
/dashboard provides full agent functionality in the browser without any additional installation.Update appsettings.json with a strong secret key:
"Jwt": {
"SecretKey": "YOUR-SECRET-KEY-AT-LEAST-32-CHARACTERS-LONG",
"Issuer": "DChat",
"Audience": "DChat",
"ExpirationMinutes": 480
}
Configure AI chatbot in the Dashboard under Settings > AI Chatbot. Supported providers:
For production, always use HTTPS. Options:
appsettings.jsonadmin/admin123 and agent1/agent123 only let you set a new password: every other call answers 403 password_change_required until you do. Delete the seed accounts once you have your own administrator.Web\App_Data holds per-install secrets generated on first run: jwt-signing-key (signs agent sessions), channel-secrets-key (decrypts the inbox, SSO, Slack and widget-identity secrets stored in the database) and vapid-keys (web push), plus uploads, the knowledge base and logs. Back it up together with the database. Never publish, serve, commit or package it: anyone with these files can forge agent sessions and read your channel credentials.Network:TrustedProxies / Network:TrustedProxyNetworks. Otherwise every visitor is recorded as the proxy's address and rate limits and IP blocks act on the proxy. Until then X-Forwarded-For is ignored.Web folder (keep Web\appsettings.json, your licence file and Web\App_Data) and start it again. The new tables and columns are created at startup: EmailThreadMessage (email threading); WidgetSite and SupportSession.SiteId (multi-site widgets); ChannelSurvey and Inbox.SurveyJson (channel surveys); Company, CompanyDomain, CompanyAttribute, CompanyNote and Contact.CompanyId (companies); ReportEmailSubscription and ReportEmailSend (scheduled report emails). Existing data is left alone.In-Reply-To / References headers (also for mail answered before the upgrade); mail without them joins the sender's latest open conversation only when the subject matches within 7 days (inbox setting Subject threading window (days), 0 = off). A customer who writes about a new subject now gets a new conversation. Conversations from before the upgrade are not split or merged. Agent replies by email now carry their own Message-ID and threading headers, and web addresses in them become links./api/docs generated from the server, listing every public operation and who may call it.DChat.Web.exe / DChat.Web.dll: point a Windows service, your own IIS web.config or any script that starts ZChat.Web at the new name and delete the old ZChat.* files from Web - see Upgrading from ZChat (2.0.8 and earlier) under step 4 above and the full list in READ_ME.txt. Nothing else breaks: old widget snippets (zchat.iife.js, ZChat.init), zchat.lic / ZChat.lic, X-ZChat-* webhook and inbound headers, the ZChat connection string, the ZCHAT_* Docker variables and the zchat-* Docker volumes all keep working.Web folder (keep Web\appsettings.json, your licence file and Web\App_Data) and start it again. The new tables (visitor shop events and shop triggers, import jobs, AI action runs, bot flows with their versions and statistics, legal holds) are created at startup; existing data is left alone.Security:IpAllowlist:Disabled to true and restart); conversation retention with preview, confirmation and legal holds (free, off by default).Web folder (keep Web\appsettings.json, your licence file and Web\App_Data) and start it again. The new knowledge-source, bot-test and AI-feedback tables are created at startup; existing data is left alone.ZChat.lic or zchat.lic, also on a case-sensitive (Linux/Docker) file system. Keep the file you have.Chatbot:EmbeddingModel, or leave it empty for the provider's default: OpenAI text-embedding-3-small; Ollama nomic-embed-text (run ollama pull nomic-embed-text on the Ollama host). Anthropic has no embeddings API, so it searches by keyword only. none switches embeddings off; the assistant then still answers from your sources by keyword match.Webhooks:AllowPrivateTargets is true. Set it only if you deliberately crawl an intranet site.Web folder (keep Web\appsettings.json, Web\zchat.lic and Web\App_Data) and start it again. New tables and columns (teams, Linear links, SMS/WhatsApp campaigns, dashboard apps) are created at startup; existing data is left alone.SupportSession and SupportMessage that make the inbox list about 20 times faster. They are built once, at the first start, before the server answers; with millions of messages this can take several minutes. Let it finish; if the server is stopped midway the build starts again next time.ALLOW_SNAPSHOT_ISOLATION for its database, so the inbox list no longer blocks live chats under load. That needs ALTER permission on the database. Without it DChat works exactly as before and logs one warning. To turn it on, run this once as the database owner (ZChat is the default database name; use yours), then restart DChat:ALTER DATABASE [ZChat] SET ALLOW_SNAPSHOT_ISOLATION ONWeb folder (keep Web\appsettings.json, Web\zchat.lic and Web\App_Data) and start it again. New tables and columns are created at startup; existing data is left alone.POST /api/v1/widget-identity/secret/reveal from a dashboard sign-in. The old GET ?reveal=true answers 400 reveal_moved.dchat-agent-console.zip 2.0.6; its SSO sign-in, signatures and template variables need a 2.0.6 server.Optional keys in Web\appsettings.json. Everything else is set from the dashboard.
| Key | Purpose |
|---|---|
Channels:SecretKey | Encrypts inbox, SSO, Slack and widget-identity secrets in the database. By default a key is generated into App_Data\channel-secrets-key. Pin one (base64 of 32 random bytes) if several instances share one database. If the file is lost, those secrets have to be entered again. |
Channels:PublicBaseUrl | Fallback for the Public URL on the Inboxes page (the page setting wins). |
Push:Subject | Your contact for push services: mailto:you@example.com or an https URL of yours. Default https://dchat.com/. Set your own. |
Push:VapidPrivateKey | Web push key (base64 PKCS#8, P-256). By default one is generated into App_Data\vapid-keys. Pin it if several instances share one database. |
Push:AllowPrivateEndpoints | true allows non-https or private push endpoints. For testing only. Default false. |
Webhooks:AllowPrivateTargets | true allows webhook and API-channel callback URLs on private or local networks. Default false. |
Network:TrustedProxies, Network:TrustedProxyNetworks, Network:ForwardLimit | Which reverse proxies' X-Forwarded-For headers are trusted. Empty by default. |
Slack:SyncSeconds | How often new conversations are posted to Slack (1-300, default 5). |
HelpCenter:ViewFlushSeconds | How often article view counts are written (default 30). |
AutoAssign.Enabled setting. You switch it on only under Automation > Auto-assignment in the dashboard, and it starts off after an upgrade. It is a Premium feature.Dashboard > Inboxes > Public URL is the address the internet reaches this server at, for example https://chat.example.com (no path, no query). Channels, voice and file sending all use it.
https://chat.example.com/api/v1/channels/{inboxId}/webhook. A Voice inbox shows /api/v1/voice/{inboxId}/incoming instead.Email inboxes are free. Every other channel needs Premium to create or change. After a lapse an existing inbox keeps receiving messages, but agents cannot reply through it; you can always switch it off. Secret fields are encrypted and never shown again. Generated values (verify tokens, inbound tokens) are shown once, when the inbox is created. Copy them then, or use rotate later to get a new one.
| Channel | Credentials | Where the webhook URL goes |
|---|---|---|
| Telegram | Bot token from @BotFather (/newbot) | Save the inbox, then press Connect webhook. DChat calls Telegram's setWebhook with the URL and a generated secret, which Telegram sends back in X-Telegram-Bot-Api-Secret-Token. Needs an https Public URL. |
| SMS (Twilio), WhatsApp (Twilio) | Account SID, Auth token (verifies X-Twilio-Signature), From number in E.164 (e.g. +14155550100) or Messaging service SID | Twilio console, on the number, messaging service or WhatsApp sender: set "A message comes in" to Webhook, HTTP POST, the inbox webhook URL. It must match the Public URL character for character. |
| WhatsApp Cloud API (Meta) | Phone number ID, Access token (system user with whatsapp_business_messaging), App secret (verifies X-Hub-Signature-256), optional WhatsApp Business Account ID (for Sync templates) | Meta app dashboard > WhatsApp > Configuration. Callback URL is the inbox webhook URL. Verify token is the generated value; DChat answers hub.challenge. Subscribe the messages field. |
| Facebook Messenger, Instagram DMs | Page ID (Instagram: account ID), Page access token, App secret | Meta app dashboard > Messenger (or Instagram) > Webhooks. Callback URL is the inbox webhook URL and Verify token is the generated value. Subscribe to messages. |
| LINE | Channel secret (verifies X-Line-Signature), Channel access token | LINE Developers console > Messaging API. Set Webhook URL to the inbox webhook URL, turn "Use webhook" on and auto-reply messages off. Replies use the push API and count against your LINE quota. |
| API (your own app) | Callback URL (public http(s)); Inbound token and HMAC secret are generated | Your app POSTs to the inbox webhook URL with X-DChat-Inbox-Token (or Authorization: Bearer). Agent replies are POSTed to your Callback URL, signed as shown below. |
| Email free | Inbound secret (generated), optional Reply-To address, mode conversation or missed_chat | Your mail gateway POSTs the same JSON as /api/v1/email/inbound to the inbox webhook URL with X-DChat-Inbound-Secret. Replies go out through the SMTP server in Settings. Pass the sender-authentication verdict too (below). |
A From header is whatever the sender typed. If your mail gateway knows whether a message passed SPF, DKIM and DMARC, pass the result on. Add "spf", "dkim" and "dmarc" fields (pass, fail, softfail, none...) to the JSON, or forward the receiving server's Authentication-Results header in "headers". This works for /api/v1/email/inbound and for email inboxes. DMARC decides when present. Otherwise one passing SPF or DKIM result is enough. When the verdict is present and failing, the message is not threaded onto the sender's earlier conversation or linked to their contact. It starts a new conversation marked [Sender not verified], with a private note for agents, and the response carries "senderVerified": false. Replies still go to the address the mail claimed, so a forger never sees them. With no verdict, mail is handled as before.
API channel message and reply signature:
POST /api/v1/channels/{inboxId}/webhook
X-DChat-Inbox-Token: <inbound token>
Content-Type: application/json
{"contact":{"identifier":"customer-42","name":"Jane","email":"jane@example.com"},
"message":{"id":"your-unique-id","text":"Hello"}}
// replies to your Callback URL carry:
X-DChat-Timestamp: <timestamp>
X-DChat-Signature: sha256=HMAC-SHA256(hmac secret, timestamp + "." + body)
browser, forward or browser_then_forward) and fill in Forward to, Ring for (seconds), Voicemail, Transcribe voicemail and the greeting.A call comes in = Webhook, HTTP POST, https://YOUR_HOST/api/v1/voice/{inboxId}/incoming
Call status changes = https://YOUR_HOST/api/v1/voice/{inboxId}/statushttps://YOUR_HOST/api/v1/voice/{inboxId}/outbound (HTTP POST). The Calls page shows the exact URLs. Paste the API key SID, API key secret and TwiML app SID into the voice inbox.+1, +44). Outbound calls are billed to your Twilio account; leave it empty to allow any number.As with SMS, the Public URL must match the URLs Twilio calls exactly.
Configure it under Dashboard > Single Sign-On.
https://YOUR_HOST/api/v1/sso/oidc/callback.{authority}/.well-known/openid-configuration), Client ID, Client secret, Scopes (default openid profile email) and Client authentication (client_secret_basic or client_secret_post). Press Check discovery to catch typos.https://YOUR_HOST/api/v1/sso/saml/metadata. The ACS is https://YOUR_HOST/api/v1/sso/saml/acs (HTTP-POST).email, preferred_username, name). An email the provider marks unverified is refused.https://YOUR_HOST/hc/{portal-slug}/{locale}. Import knowledge base copies the App_Data\kb files into a portal as draft articles.help.example.com in the portal settings. Point that name's DNS (CNAME or A record) at this server, and bind the host name and its TLS certificate in IIS or your reverse proxy so requests reach DChat with that Host header. The portal then answers at the domain's root. /api, /widget and /dashboard keep working on it.App_Data\vapid-keys. Back it up; if it is replaced, browsers have to subscribe again.Push:Subject to mailto:you@example.com or an https URL of yours.http://localhost). On iPhone and iPad, the dashboard must first be added to the home screen.read or write, optional expiry, at most 25 active. The token (zat_...) is shown once. Send it as Authorization: Bearer zat_....https://YOUR_HOST/api/docs, and the OpenAPI document at /api/docs/openapi.json.identifierHash as the lower-case hex HMAC-SHA256 of the signed-in user's id, keyed with the secret string exactly as shown (do not hex-decode it):
// Node
const identifierHash = require('crypto').createHmac('sha256', SECRET).update(String(user.id)).digest('hex');
// PHP
$identifierHash = hash_hmac('sha256', $user->id, $secret);
// C#
var identifierHash = Convert.ToHexString(HMACSHA256.HashData(
Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(userId))).ToLowerInvariant();DChat.setUser('user-4711', {
identifierHash: '<computed on your server>',
name: 'Ann Lee', email: 'ann@example.com', plan: 'pro'
});When the hash matches, agents see Verified identity and the chat is filed under that contact. Tick Ignore identities that do not verify so that a page cannot attach itself to someone else's history. Replacing the secret invalidates every hash computed with the old one.
| Call | What it does |
|---|---|
DChat.setUser(identifier, { identifierHash, name, email, phone, ...attributes }) | Tells DChat who the visitor is (identifier at most 150 characters). Extra keys are stored as custom attributes. |
DChat.setCustomAttributes({ plan: 'pro', seats: 12 }) | Sets contact attributes. null removes one. |
DChat.open() / close() / toggle() | Opens, closes or toggles the chat window. |
DChat.reset() | Forgets the user, the conversation and the visitor id. Call it on sign-out. |
DChat.on(event, callback) | Events: ready, open, close, message, unread. Returns an unsubscribe function. |
chat:write and users:read, and install it to your workspace.https://YOUR_HOST/api/v1/integrations/slack/events (public HTTPS). The Integrations page shows this URL as the server sees itself, so behind a proxy use your public address instead. Subscribe to the bot events message.channels, plus message.groups for a private channel.xoxb-...), the Signing secret and the Channel ID (like C0123ABCDEF, not the channel name). Optionally add a Dashboard address for links. Tick "Post new conversations to Slack and relay thread replies", save, then press Test connection.Each new conversation becomes a Slack thread, and replies in the thread reach the customer. Only conversations that start after you switch it on are posted.
| Tier | Includes |
|---|---|
| Community (free) | Unlimited agents with no licence file: widget, dashboard, email and email inboxes, help center, web push, API tokens, widget identity verification, webhooks. |
| Premium | AI assistant, custom roles, removing "Powered by DChat", automation rules, macros, auto-assignment, per-agent chat limits, required attributes, CSAT review notes, extra channels (Telegram, WhatsApp, SMS, Messenger, Instagram, LINE, API), voice, Slack. |
| Enterprise | Premium plus single sign-on (OIDC / SAML) and advanced SLA policies. |
When paid coverage ends, the paid features switch off and nothing else does. Nothing is deleted, and agents are never locked out:
A paid feature can always be switched off on any tier; switching it on needs the tier. API refusals carry the code requires_premium or requires_enterprise. The dashboard's Plan & Licence page shows the tier in force.
| Endpoint | Method | Description |
|---|---|---|
/ | GET | Health check |
/api/v1/auth/login | POST | Agent login (returns JWT) |
/api/v1/dashboard/stats | GET | Live dashboard statistics |
/api/v1/agents | GET | List agents |
/api/v1/departments | GET | List departments |
/api/v1/sessions | GET | Search chat sessions |
/api/v1/sessions/{id}/messages | GET | Get session transcript |
/api/v1/settings | GET | Get all settings |
/hubs/chat | WebSocket | SignalR real-time hub |
/api/docs | GET | Full API reference (OpenAPI document at /api/docs/openapi.json) |
/api/v1/channels/{inboxId}/webhook | GET/POST | Channel provider webhooks (signed per provider) |
--urls parameter to a different port.