العربية • Deutsch • English • Español • Français • Italiano • 日本語 • 한국어 • Nederlands • Polski • Português (BR) • Русский • Türkçe • 简体中文
A full-featured, embeddable support ticket system for ASP.NET Core. Drop it into any app -- get a complete helpdesk with SLA tracking, escalation rules, agent workflows, and a customer portal. No external services required.
escalated.dev -- Learn more, view demos, and compare Cloud vs Self-Hosted options.
- Ticket lifecycle -- Create, assign, reply, resolve, close, reopen with configurable status transitions
- SLA engine -- Per-priority response and resolution targets, business hours calculation, automatic breach detection
- Escalation rules -- Condition-based rules that auto-escalate, reprioritize, reassign, or notify
- Automations -- Time-based rules with conditions and actions
- Agent dashboard -- Ticket queue with filters, bulk actions, internal notes, canned responses
- Customer portal -- Self-service ticket creation, replies, and status tracking
- Admin panel -- Manage departments, SLA policies, escalation rules, tags, and more
- Macros and canned responses -- Batch actions and reusable reply templates
- Custom fields -- Dynamic metadata with conditional display logic
- Knowledge base -- Articles, categories, search, and feedback
- File attachments -- Upload support with configurable storage and size limits
- Activity timeline -- Full audit log of every action on every ticket
- Webhooks -- HMAC-SHA256 signed with retry logic
- API tokens -- Bearer auth with ability-based scoping
- Roles and permissions -- Fine-grained access control
- Audit logging -- All mutations tracked with old/new values
- Import system -- Multi-step wizard with pluggable adapters
- Side conversations -- Internal team threads on tickets
- Ticket merging and linking -- Merge duplicate tickets and relate issues
- Ticket subjects -- Attach host-app entities (Project, Customer, asset) a ticket is about
- Ticket splitting -- Split a reply into a new ticket
- Ticket snooze -- Snooze until a future date with auto-wake background service
- Email threading -- In-Reply-To/References/Message-ID headers for proper threading
- Inbound email -- Single webhook endpoint with Postmark + Mailgun + AWS SES parsers, signed Reply-To verification, and Message-ID-based ticket resolution
- Saved views -- Personal and shared filter presets
- Embeddable widget API -- Public endpoints for KB search, guest tickets, status lookup
- Real-time updates -- SignalR hubs for live ticket updates (opt-in)
- Capacity management -- Per-agent workload limits by channel
- Skill-based routing -- Match agents to tickets by skill tags
- CSAT ratings -- Satisfaction surveys on resolved tickets
- 2FA -- TOTP setup and verification with recovery codes
- Guest access -- Anonymous ticket creation with magic token lookup
- Inertia.js + Vue 3 UI -- Shared frontend via
@escalated-dev/escalated
- .NET 8.0+
- Entity Framework Core 8.0+
- SQL Server, SQLite, or PostgreSQL
- Node.js 18+ (for frontend assets)
dotnet add package Escalated// Program.cs
using Escalated.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEscalated(builder.Configuration, options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Escalated")));
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.MapEscalated();
app.Run();// appsettings.json
{
"ConnectionStrings": {
"Escalated": "Server=localhost;Database=MyApp;Trusted_Connection=true;"
},
"Escalated": {
"RoutePrefix": "support",
"TicketReferencePrefix": "ESC",
"DefaultPriority": "medium",
"AllowCustomerClose": true,
"AutoCloseResolvedAfterDays": 7,
"Sla": {
"Enabled": true,
"BusinessHoursOnly": false,
"BusinessHours": {
"Start": "09:00",
"End": "17:00",
"Timezone": "UTC",
"Days": [1, 2, 3, 4, 5]
}
},
"EnableRealTime": false,
"Mail": {
"Domain": "support.yourapp.com",
"InboundSecret": "a-long-random-value"
}
}
}The Mail.InboundSecret is symmetric -- it signs outbound Reply-To addresses and verifies inbound webhook requests, so forged emails targeting a stolen reply address are rejected via timing-safe HMAC.
dotnet ef migrations add InitialEscalated --context EscalatedDbContext
dotnet ef database update --context EscalatedDbContextVisit /support -- you're live.
Escalated stores references to your app's users (ticket requester, assignee,
reply author, etc.) as strings, so it works whether your user primary key
is an int, a Guid, or any other string. A numeric id is simply stored and
exchanged as its string form.
Your IUserDirectory implementation therefore exchanges user ids as string:
public Task<UserDirectoryEntry?> FindAsync(string id, CancellationToken ct = default)
{
// look up your user by its (string) id and return:
// new UserDirectoryEntry(user.Id.ToString(), user.Name, user.Email)
}Upgrade note: earlier versions used
inthere (FindAsync(int id)andUserDirectoryEntry(int Id, ...)); admin user APIs also now return user ids as JSON strings. Update yourIUserDirectoryimplementation to thestringsignatures shown above. Integer ids keep working — pass them as strings.
A ticket has a requester (the person who raised it) and a subject line (free text). Sometimes a ticket is also about one or more host-app entities — a Project, a Customer, an asset — that aren't people. Attach them as ticket subjects so agents see what the ticket concerns and can jump straight to it in your app.
Implement ITicketSubject on any attachable host model:
public class Project : ITicketSubject
{
public string TicketSubjectTitle() => Name;
public string? TicketSubjectSubtitle() => Customer == null ? null : $"Project · {Customer.Name}";
public string? TicketSubjectUrl() => $"/projects/{Id}";
public string? TicketSubjectColor() => "#2563eb";
public string? TicketSubjectIcon() => "folder";
}Register a resolver so Escalated can load host models by stored (type, id):
builder.Services.AddSingleton<ITicketSubjectResolver, MyTicketSubjectResolver>();Attach, detach, or sync subjects programmatically via TicketSubjectService, or
through the admin API when types are allowlisted in config:
"Escalated": {
"TicketSubjects": {
"Types": [
"App.Models.Project",
"App.Models.Customer"
]
}
}Each attached subject is serialized on ticket responses as
{ type, id, role, title, subtitle, url, color, icon, missing }. When the
resolver cannot find a host record, missing is true and title falls back
to TypeName#id. subject_id is stored as a string, so integer, UUID, or
string-keyed host models all work.
Admin endpoints:
POST /support/admin/tickets/{id}/subjects { "type", "id", "role?" }
DELETE /support/admin/tickets/{ticketId}/subjects/{subjectLinkId}
Programmatic attach works for any type when the allowlist is empty; the admin API only accepts allowlisted types.
Point your Postmark, Mailgun, or AWS SES (via SNS HTTP subscription) inbound webhook at:
POST /support/webhook/email/inbound?adapter=postmark
POST /support/webhook/email/inbound?adapter=mailgun
POST /support/webhook/email/inbound?adapter=ses
The adapter can be selected via the query parameter or the X-Escalated-Adapter header. Your provider must attach the shared secret as an X-Escalated-Inbound-Secret header.
The service resolves inbound messages to existing tickets via, in order: canonical Message-ID headers, signed Reply-To verification, and subject-reference tags. Unmatched messages with real content create a new ticket; SNS subscription confirmations and empty body+subject messages are skipped.
See the inbound email docs for provider setup, the response shape, and a ready-to-paste curl test recipe.
Escalated ships a Vue component library and default pages via the @escalated-dev/escalated npm package. Integrate with Inertia.js for seamless SPA rendering inside your existing layout.
npm install @escalated-dev/escalatedsrc/Escalated/
Models/ # 40+ EF Core entity models
Data/ # EscalatedDbContext with full relationship mapping
Services/ # Business logic (ticket, SLA, merge, split, snooze, etc.)
Controllers/
Admin/ # Admin panel API (CRUD for all settings)
Agent/ # Agent ticket queue and actions
Customer/ # Customer self-service portal
Widget/ # Public widget API (KB search, guest tickets)
Middleware/ # API token auth, permissions, rate limiting
Events/ # Domain events (TicketCreated, SlaBreached, etc.)
Notifications/ # Email notification interfaces and templates
Configuration/ # DI registration, options, endpoint mapping
Hubs/ # SignalR hub for real-time updates
Enums/ # TicketStatus, TicketPriority, ActivityType
Escalated includes 40+ EF Core entities covering the full helpdesk domain:
| Category | Models |
|---|---|
| Core | Ticket, Reply, Attachment, TicketActivity, TicketStatusModel, TicketLink, TicketTag, Tag, Department, SatisfactionRating |
| SLA | SlaPolicy, EscalationRule, BusinessSchedule, Holiday, Automation |
| Agents | AgentProfile, AgentCapacity, Skill, AgentSkill |
| Messaging | CannedResponse, Macro, SideConversation, SideConversationReply, InboundEmail |
| Admin | Role, Permission, ApiToken, Webhook, WebhookDelivery, Plugin, AuditLog |
| Custom | CustomField, CustomFieldValue, CustomObject, CustomObjectRecord |
| Import | ImportJob, ImportSourceMap |
| Config | EscalatedSettings, SavedView |
| Knowledge Base | Article, ArticleCategory |
All models have proper relationships, indexes, and query filters configured in EscalatedDbContext.
| Service | Responsibility |
|---|---|
TicketService |
Full ticket CRUD, status transitions, replies, tags, departments |
SlaService |
Policy attachment, breach detection, warning checks, first response recording |
AssignmentService |
Agent assignment, unassignment, auto-assign by workload |
EscalationService |
Evaluate condition-based rules, execute escalation actions |
AutomationRunner |
Time-based automation evaluation and action execution |
MacroService |
Apply macro action sequences to tickets |
TicketMergeService |
Merge source into target with reply transfer |
TicketSplitService |
Split a reply into a new linked ticket |
TicketSnoozeService |
Snooze/unsnooze with background wake service |
WebhookDispatcher |
HMAC-signed webhook delivery with retry logic |
CapacityService |
Per-agent concurrent ticket limits |
SkillRoutingService |
Match agents by skills to ticket tags |
BusinessHoursCalculator |
Business hours date math with holiday support |
TwoFactorService |
TOTP secret generation, verification, recovery codes |
AuditLogService |
Log and query entity mutations |
KnowledgeBaseService |
Article/category CRUD, search, feedback |
SavedViewService |
Personal and shared filter presets |
SideConversationService |
Internal threaded conversations on tickets |
ImportService |
Multi-step import with pluggable adapters |
SettingsService |
Key-value settings store |
Every ticket action dispatches a domain event:
| Event | When |
|---|---|
TicketCreatedEvent |
New ticket created |
TicketStatusChangedEvent |
Status transition |
TicketAssignedEvent |
Agent assigned |
TicketUnassignedEvent |
Agent removed |
ReplyCreatedEvent |
Public reply added |
InternalNoteAddedEvent |
Agent note added |
SlaBreachedEvent |
SLA deadline missed |
SlaWarningEvent |
SLA deadline approaching |
TicketEscalatedEvent |
Ticket escalated |
TicketResolvedEvent |
Ticket resolved |
TicketClosedEvent |
Ticket closed |
TicketReopenedEvent |
Ticket reopened |
TicketPriorityChangedEvent |
Priority changed |
DepartmentChangedEvent |
Department changed |
TagAddedEvent |
Tag added |
TagRemovedEvent |
Tag removed |
TicketCustomActionTriggeredEvent |
Agent triggered a custom ticket action |
Implement IEscalatedEventDispatcher to receive these events in your host application:
public class MyEventHandler : IEscalatedEventDispatcher
{
public async Task DispatchAsync<TEvent>(TEvent @event, CancellationToken ct) where TEvent : class
{
if (@event is TicketCreatedEvent created)
{
// Handle new ticket
}
}
}
// Register in DI
services.AddSingleton<IEscalatedEventDispatcher, MyEventHandler>();Host applications can add custom buttons to the agent ticket screen and handle clicks via the event dispatcher. Register actions in configuration:
{
"Escalated": {
"TicketActions": [
{
"Key": "sync-crm",
"Label": "Sync CRM",
"Variant": "primary",
"Confirmation": "Sync this ticket to the CRM?",
"Metadata": { "icon": "refresh-cw" }
}
]
}
}Visible actions are exposed on the agent ticket response as custom_actions
(each with a url and method). Triggering one
(POST /support/agent/tickets/{id}/actions/{action}) validates the action is
visible (404) and enabled (403), records an internal note for auditability, and
dispatches TicketCustomActionTriggeredEvent to your IEscalatedEventDispatcher:
if (@event is TicketCustomActionTriggeredEvent triggered && triggered.Action == "sync-crm")
{
// triggered.Ticket, triggered.UserId, triggered.Payload, triggered.Metadata
}For dynamic per-ticket visibility, register your own ITicketActionRegistry.
| Method | Route | Description |
|---|---|---|
| GET | /support/tickets |
List customer tickets |
| POST | /support/tickets |
Create ticket |
| GET | /support/tickets/{id} |
View ticket |
| POST | /support/tickets/{id}/reply |
Reply to ticket |
| POST | /support/tickets/{id}/close |
Close ticket |
| POST | /support/tickets/{id}/reopen |
Reopen ticket |
| Method | Route | Description |
|---|---|---|
| GET | /support/agent/tickets |
Ticket queue with filters |
| GET | /support/agent/tickets/{id} |
Ticket detail |
| POST | /support/agent/tickets/{id}/reply |
Reply |
| POST | /support/agent/tickets/{id}/note |
Internal note |
| POST | /support/agent/tickets/{id}/assign |
Assign agent |
| POST | /support/agent/tickets/{id}/status |
Change status |
| POST | /support/agent/tickets/{id}/priority |
Change priority |
| POST | /support/agent/tickets/{id}/macro |
Apply macro |
| POST | /support/agent/tickets/bulk |
Bulk actions |
| GET | /support/agent/tickets/dashboard |
Agent workload |
| Method | Route | Description |
|---|---|---|
| GET/POST | /support/admin/departments |
Manage departments |
| GET/POST | /support/admin/tags |
Manage tags |
| GET/POST | /support/admin/sla-policies |
Manage SLA policies |
| GET/POST | /support/admin/escalation-rules |
Manage escalation rules |
| GET/POST | /support/admin/webhooks |
Manage webhooks |
| GET/POST | /support/admin/api-tokens |
Manage API tokens |
| GET/POST | /support/admin/macros |
Manage macros |
| GET/POST | /support/admin/automations |
Manage automations |
| GET/POST | /support/admin/custom-fields |
Manage custom fields |
| GET/POST | /support/admin/business-hours |
Business schedules |
| GET/POST | /support/admin/skills |
Manage skills |
| GET/POST | /support/admin/roles |
Manage roles |
| GET | /support/admin/audit-logs |
Query audit logs |
| GET/POST | /support/admin/settings |
App settings |
| GET/PUT | /support/admin/settings/public-tickets |
Runtime guest-policy mode (unassigned / guest_user / prompt_signup). See docs.escalated.dev/public-tickets. |
| POST | /support/admin/tickets/{id}/merge |
Merge tickets |
| POST | /support/admin/tickets/{id}/split |
Split ticket |
| POST | /support/admin/tickets/{id}/snooze |
Snooze ticket |
| POST | /support/admin/tickets/{id}/link |
Link tickets |
| Method | Route | Description |
|---|---|---|
| GET | /support/widget/kb/search |
Search knowledge base |
| POST | /support/widget/tickets |
Create guest ticket |
| GET | /support/widget/tickets/{token} |
Lookup by guest token |
| POST | /support/widget/tickets/{token}/reply |
Guest reply |
| POST | /support/widget/tickets/{token}/rate |
Submit CSAT rating |
| POST | /support/widget/kb/articles/{id}/feedback |
Article feedback |
Enable SignalR for live ticket updates:
{
"Escalated": {
"EnableRealTime": true
}
}// Program.cs
app.MapHub<EscalatedHub>("/support/hub");Clients join ticket-specific groups to receive updates:
connection.invoke("JoinTicket", ticketId);
connection.on("TicketUpdated", (data) => { /* handle */ });Protect API endpoints with bearer token authentication:
app.UseMiddleware<ApiTokenAuthMiddleware>();Tokens are stored as SHA-256 hashes. Create tokens via the admin API endpoint.
app.UseMiddleware<EscalatedRateLimitMiddleware>(60, 60); // 60 requests per 60 secondsEscalated for ASP.NET Core consumes its translation catalog from the
central Escalated.Locale
NuGet package. AddEscalated() registers a chained
IStringLocalizer stack that resolves keys in this order:
- Plugin-local overrides --
.resxfiles undersrc/Escalated/Resources/Overrides/. Empty by default. - Central catalog -- embedded JSON resources from
Escalated.Locale, accessed throughEscalated.Locale.LocaleProvider.
Adding a locale upstream lights it up across every host plugin without
a code change here. Drop a resx into Resources/Overrides/ only when
you need a .NET-specific string that should not be back-ported to the
shared catalog. See src/Escalated/Resources/Overrides/README.md for
naming conventions.
dotnet testTests use xUnit with Moq and EF Core InMemory provider. Coverage includes:
- Ticket CRUD and status transitions
- SLA breach detection and warnings
- Ticket split, merge, and snooze
- Assignment and workload calculation
- Webhook subscription matching
- 2FA secret generation and verification
- Capacity management
- Model validation and enum behavior
- Escalated for Laravel -- Laravel Composer package
- Escalated for Rails -- Ruby on Rails engine
- Escalated for Django -- Django reusable app
- Escalated for AdonisJS -- AdonisJS v6 package
- Escalated for ASP.NET Core -- ASP.NET Core package (you are here)
- Shared Frontend -- Vue 3 + Inertia.js UI components
Same architecture, same Vue UI -- for every major backend framework.
Models + renderer for the admin-only newsletter broadcast feature. EF Core migration not bundled — host integrators generate one with dotnet ef migrations add NewsletterSystem after referencing this package.
using Escalated.Services.Newsletter;
var renderer = new NewsletterRenderer(new NewsletterRendererOptions
{
BaseUrl = "https://support.example.com",
DefaultTheme = "default",
TrackingEnabled = true,
ThemesDir = Path.Combine(env.ContentRootPath, "Views/NewsletterThemes"),
MarkdownToHtml = md => Markdig.Markdown.ToHtml(md),
Brand = new NewsletterBrand
{
Name = "Acme",
Accent = "#2563eb",
PhysicalAddress = "Acme Inc. · 123 Main St",
},
});
var html = renderer.Render(delivery, newsletter, contact, template);Ships: Models/Newsletter/*.cs (5 EF Core entities), Models/Contact.cs (gains MarketingOptOutAt), Services/Newsletter/NewsletterRenderer.cs (full renderer), Views/NewsletterThemes/{default,branded}.html (starter themes).
Follow-up PR: EF Core migration in-package, planner/dispatcher/tracker services using EscalatedDbContext, ASP.NET controllers.
MIT