Admin Guide
Configure and manage every aspect of your Open Help Desk installation.
The admin panel is accessible from the sidebar when logged in as an Admin. All configuration in this guide is done through the admin panel unless a restart is required (only for environment variable changes).
Categories, Types & Items
Ticket classification follows a three-level hierarchy: Category → Type → Item. This model, sometimes called CTI (Classification-Type-Item), is borrowed from ITSM tooling like BMC Remedy. It lets you route tickets consistently based on what kind of request they are.
Hierarchy rules
- Every ticket must have a Category. Without at least one Category, tickets cannot be created.
- Types are optional. If a Category has no Types, the Type dropdown is hidden entirely on the ticket form.
- Items are optional. If a Type has no Items, the Item dropdown is hidden.
- Each level filters the next — selecting a Category only shows Types that belong to it; selecting a Type only shows Items under it.
Worked example
Hardware
Laptop
Won't power on
Broken screen
Keyboard issue
Battery not charging
Desktop
Won't power on
Monitor issue
Printer
Not printing
Paper jam
Low ink
Software
Microsoft 365
Outlook not loading
Teams audio issue
SharePoint access
VPN
Can't connect
Slow connection
Custom application
Access Requests
New account
Active Directory
Email / Microsoft 365
VPN
Application access
Permission change
Account unlock
Facilities
Building access
Equipment request
Ergonomics
Managing the hierarchy
Navigate to Admin → Categories. The page shows a tree of all Categories with their Types and Items.
- Add Category — click New Category. Give it a name. It appears immediately in the ticket form.
- Add Type — click Add Type next to a Category. Types cannot exist without a parent Category.
- Add Item — click Add Item next to a Type.
- Reorder — drag-and-drop entries to change the order they appear in the ticket form dropdowns.
- Delete — Categories and Types with existing tickets cannot be deleted. Archive them (hide from new tickets) instead.
How CTI affects staff scope
Groups are scoped to Category/Type pairs. A group assigned to Hardware (category only) sees all hardware tickets. A group assigned to Hardware / Laptop sees only laptop-type hardware tickets. See Groups & scope for the full explanation.
Users & roles
Manage accounts from Admin → Users. The page lists all accounts with their role, last login, and MFA status.
Roles
| Role | Capabilities |
|---|---|
| Admin | Full access to all settings, all tickets, and all user accounts. Can manage categories, groups, statuses, SLA policies, email templates, webhooks, and plugins. Always retains local-auth access even when SAML is enabled. |
| Staff | Can view, reply to, assign, and update tickets within their group scope. Can look up any ticket by ID regardless of scope. Can leave internal notes not visible to users. Can resolve and close tickets. Cannot access admin settings. |
| User | Can submit tickets and view their own tickets. Can add replies while the ticket is open. Can reopen a Resolved ticket by replying within the configured reopen window. Cannot see other users' tickets. |
Changing a user's role
Click the user's name in the list and change the role dropdown. Role changes take effect on the user's next page load — their current session is re-evaluated on each request.
Creating a user
Click New user. Enter name, email, and password. Select the role. The account is created immediately. If email is configured, you can optionally send a welcome email with a password-reset link instead of setting a password manually.
Forcing a password reset
From a user's profile page, click Reset password. The user receives an email with a time-limited reset link (1 hour). Their current password remains valid until they use the link.
Disabling an account
Disabled accounts cannot log in and are not visible in assignment dropdowns. Disable rather than delete to preserve ticket history and attribution. From a user's profile: Disable account. Re-enable at any time.
Groups & scope
Staff visibility — which tickets a staff member sees in their queue — is controlled entirely by group membership. An admin always sees all tickets. A staff member not in any group sees no tickets (except by direct search by ID).
How scope is calculated
When a staff member loads their ticket queue, the system collects all CTI scope entries from all their groups, unions them, and returns tickets that match any of those entries.
Scope entries are Category/Type pairs:
- Category only (Type = null) — the staff member sees all tickets in that Category, regardless of Type or Item.
- Category + Type — the staff member sees only tickets with that exact Category and Type. Tickets in the same Category with a different Type are not shown.
Items never affect scope. A staff member scoped to Hardware / Laptop sees all laptop tickets regardless of which Item is selected.
Creating and configuring a group
- Go to Admin → Groups → New group.
- Give the group a name (e.g. "Tier 1 Support", "Networking").
- Under Scope, add one or more Category/Type pairs. Click Add scope entry, pick a Category, and optionally narrow to a Type.
- Under Members, add staff accounts.
Common configurations
| Setup | Configuration |
|---|---|
| Single team, all tickets | One group. Add all Categories (no Type). Add all staff as members. |
| Separate teams by category | One group per team. Each group scoped to the categories that team handles. Assign staff to the relevant group(s). |
| Tiered support | Tier 1 group scoped to all Categories. Tier 2 group scoped to specific Category/Type pairs for complex issues. Staff in Tier 2 also see escalated tickets because they can be assigned directly (assignment overrides scope visibility for the assignee). |
| Cross-functional specialists | Staff can belong to multiple groups. A networking specialist is in "Tier 1" (full scope) and "Networking specialists" (scoped to Software / VPN). They see all tickets from Tier 1 scope plus all VPN-specific tickets. |
Assignment vs. scope
Scope determines what a staff member sees in their queue. Assignment is separate — any staff member can be assigned any ticket, even if it's outside their queue scope. Once assigned, the ticket appears in the assignee's queue regardless of CTI.
This is intentional: you should never be unable to escalate a ticket to a specialist because of routing rules.
Ticket statuses
Manage statuses from Admin → Statuses. Statuses represent where a ticket is in its lifecycle.
System statuses
These are created automatically on first run and have special behavior. Their names and colors can be changed, but they cannot be deleted.
| Status | Special behavior |
|---|---|
| New | The initial status assigned to every newly created ticket. |
| Resolved | Setting this status starts the reopen window timer. Users can reopen the ticket by replying within this window. When set, staff are prompted to enter optional resolution notes. |
| Closed | Automatically set by a background job when the reopen window expires. No further user-initiated updates are allowed. Staff and admins can still change the status manually. |
Custom statuses
Create custom statuses to represent intermediate states meaningful to your workflow. Common additions:
- In Progress — being actively worked
- Pending — waiting on the user
- Awaiting Vendor — blocked on a third party
- On Hold — intentionally paused
- Escalated — passed to a specialist or manager
Each custom status has:
- Name — shown in the ticket detail and list views.
- Color — hex color for the status badge.
- Sort order — controls the order in status dropdowns.
Reopen window
Configure how many days after Resolved a user can reopen a ticket by replying. Set this from Admin → Settings → Ticket lifecycle → Reopen window (days). Set to 0 to disable user-initiated reopening entirely (staff and admins can still reopen manually).
SLA policies
SLA (Service Level Agreement) tracking is available when SLA_ENABLED=true. SLA policies define the maximum time that should pass before a ticket receives a first response and before it is resolved.
Policy fields
| Field | Description |
|---|---|
| Name | Display name for the policy, e.g. "Critical — 1h response" |
| Priority | Optional. Apply this policy to tickets of this priority. |
| Category | Optional. Apply this policy to tickets in this category. |
| Response time | Maximum hours from ticket creation until a staff reply is added. |
| Resolution time | Maximum hours from ticket creation until the ticket is marked Resolved. |
Policy matching
When a ticket is created, the system selects an SLA policy by matching in this order:
- Priority + Category — most specific match
- Priority only
- Category only
- A default policy (a policy with no Priority and no Category)
- No SLA — if no match
SLA indicators in the ticket queue
The ticket list shows a visual indicator for each ticket's SLA status:
- Green — within SLA
- Amber — within 20% of the deadline
- Red — SLA breached
Staff can sort the ticket queue by SLA status to prioritize tickets at risk of breaching.
Pausing SLA timers
When a ticket is moved to the Pending status (waiting on the user), the SLA timer is paused. The timer resumes when the ticket moves out of Pending. This prevents SLA breaches for tickets that are legitimately waiting on a response from the submitter.
Email templates
Email notification templates are edited from Admin → Email → Templates. Templates use Go's text/template syntax.
Available templates
| Template | When sent | Recipients |
|---|---|---|
ticket_created | A ticket is submitted | Ticket submitter |
ticket_assigned | A ticket is assigned to a staff member | Assigned staff member |
reply_added | A public reply is added | All participants (submitter + staff who have replied) |
ticket_resolved | Ticket status changes to Resolved | Ticket submitter |
ticket_closed | Ticket auto-closes after the reopen window | Ticket submitter |
guest_ticket_created | A guest submits a ticket | Guest (by email provided at submission) |
guest_access_link | Guest requests a new access link | Guest (by provided email) |
Template variables
{{ .Ticket.ID }} -- ticket UUID
{{ .Ticket.Subject }} -- ticket subject line
{{ .Ticket.Description }} -- ticket description body
{{ .Ticket.Status }} -- current status name
{{ .Ticket.Priority }} -- priority name
{{ .Ticket.Category }} -- category name
{{ .Ticket.Type }} -- type name (may be empty)
{{ .Ticket.Item }} -- item name (may be empty)
{{ .Ticket.TrackingNumber }} -- tracking number (guest access)
{{ .Ticket.CreatedAt }} -- creation timestamp (RFC 3339)
{{ .Submitter.Name }} -- submitter's full name
{{ .Submitter.Email }} -- submitter's email address
{{ .Assignee.Name }} -- assignee's name (in assigned template)
{{ .Reply.Body }} -- reply body (in reply_added template)
{{ .Reply.Author.Name }} -- reply author's name
{{ .ResolutionNotes }} -- resolution notes (in resolved template)
{{ .TicketURL }} -- full URL to view the ticket in the app
{{ .AppName }} -- configured application name
Template example
Subject: [{{ .AppName }}] Reply on: {{ .Ticket.Subject }}
Hi {{ .Submitter.Name }},
{{ .Reply.Author.Name }} replied to your ticket:
---
{{ .Reply.Body }}
---
View the full ticket: {{ .TicketURL }}
Ticket #{{ .Ticket.TrackingNumber }}
Preview and test
Use the Preview button on any template page to see a rendered preview with sample data. Use Send test to deliver a test email to your own address.
Webhooks
Webhooks let you push ticket events to external systems in real time. Configure them from Admin → Webhooks → New webhook.
Configuration
| Field | Description | |
|---|---|---|
URL | required | The HTTPS endpoint that receives the POST requests. |
Secret | recommended | A shared secret used to sign delivery payloads with HMAC-SHA256. Verify the signature server-side to reject spoofed requests. |
Events | required | Select which events trigger this webhook. You can subscribe to all events or specific ones. |
Enabled | Toggle deliveries on/off without deleting the configuration. |
Events
| Event | Triggered when |
|---|---|
ticket.created | A new ticket is submitted (any method) |
ticket.assigned | The assignee or assigned group changes |
ticket.status_changed | The ticket status changes to anything |
ticket.reply_added | A public reply is added to a ticket |
ticket.note_added | An internal note is added (staff-only) |
ticket.resolved | Status changes to Resolved specifically |
ticket.closed | Status changes to Closed (auto or manual) |
ticket.reopened | A Resolved or Closed ticket is reopened |
Payload format
POST https://your-endpoint.example.com/webhook
Content-Type: application/json
X-OHD-Event: ticket.reply_added
X-OHD-Delivery: 01JQXYZ...
X-OHD-Signature: sha256=<hmac-hex>
{
"event": "ticket.reply_added",
"delivery_id": "01JQXYZ...",
"occurred_at": "2026-04-05T14:32:00Z",
"ticket": {
"id": "01JQABC...",
"subject": "Laptop screen flickering",
"status": "In Progress",
"priority": "High",
"category": "Hardware",
"type": "Laptop",
"item": "Broken screen"
},
"actor": {
"id": "01JQDEF...",
"name": "Alex Chen",
"role": "staff"
},
"data": {
"reply_id": "01JQGHI...",
"body": "I've ordered a replacement screen, it arrives Thursday.",
"is_internal": false
}
}
Verifying the signature
The X-OHD-Signature header is sha256=<hex> where the hex is an HMAC-SHA256 of the raw request body, keyed with your webhook secret.
# Python
import hmac, hashlib
def verify_signature(secret: str, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
# Node.js
const crypto = require('crypto');
function verifySignature(secret, body, header) {
const sig = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(header));
}
Retries and delivery history
If the target URL returns a non-2xx status or times out (5 second timeout), the delivery is retried with exponential backoff: 1s, 2s, 4s, 8s, 16s. After 5 total attempts, the delivery is marked failed.
All deliveries (successful and failed) are logged and visible under Admin → Webhooks → [webhook name] → Deliveries. Each entry shows the event type, HTTP status, response body (truncated), and timing. You can manually re-trigger a failed delivery from this page.
Installing plugins
Plugins extend Open Help Desk with custom behavior. They run as sandboxed WASM modules. Manage them from Admin → Plugins.
Installing from a file
- Click Install plugin.
- Upload the
.wasmfile. - The system validates the plugin manifest embedded in the binary. If the manifest is missing or malformed, installation is rejected with a specific error.
- After successful validation, the plugin is listed as installed but disabled. Review its declared permissions (network hosts, capabilities) before enabling.
- Toggle the plugin to Enabled.
Installing from a URL
Click Install from URL and provide a direct URL to a .wasm file. The application downloads it, validates the manifest, and stores it locally. The URL is not called again after installation — it is not a live reference.
Plugin configuration
If a plugin declares a config_schema in its manifest, a configuration form appears on the plugin's detail page. Fill in the required fields (e.g. API keys, URLs). Configuration changes take effect immediately without a restart. Fields marked "secret": true are stored encrypted and are never returned in the admin UI after saving.
Enabling and disabling
Toggle a plugin on/off from the plugin list. Disabled plugins receive no events. The plugin binary remains installed and can be re-enabled at any time. No restart is required.
Uninstalling
Click Uninstall on the plugin's detail page. The WASM binary and all stored configuration are deleted. Custom fields or UI panels added by the plugin are removed from the ticket form. Ticket data that was set by the plugin (e.g. custom field values) is preserved in the database.
Error monitoring
The plugin list shows a count of errors per plugin over the last 24 hours. Click a plugin's name to see the error log. A plugin that errors on more than 50% of event deliveries over a 10-minute window is automatically disabled and an admin notification is generated. The threshold is configurable under Admin → Settings → Plugin error threshold.
For details on building plugins, see Plugin Development.
Branding
Customize the application's visual identity from Admin → Settings → Branding. Changes apply immediately with no restart required.
| Setting | Description |
|---|---|
| Application name | Shown in the browser title bar, the sidebar, and email subject lines. Default: "Open Help Desk". |
| Logo | PNG or SVG file, maximum 2 MB. Displayed in the sidebar header and in the email template header. Recommended size: 200×50px or similar landscape ratio. |
| Primary color | Hex color code used for buttons, links, active states, and status badges. Default: #2563eb. |
| Support email | Displayed in the footer of the application and as the reply-to address on outbound notifications. Should match or alias your SMTP_FROM address. |
| Support URL | Optional. A link to your organization's support portal or knowledge base, shown in email footers. |
| Favicon | ICO or PNG file for the browser tab icon. |
Branding is CSS/asset-level customization only. Layout changes, custom page sections, and additional UI components are out of scope for the built-in branding feature — use the plugin UI panel system for that.