A modern, idiomatic C# client library for the SolarWinds Web Help Desk (WHD) REST API.
- Targets .NET 8
- Uses
HttpClientandasync/awaitthroughout - Supports API Key and Basic Auth authentication
- Covers tickets, clients, assets, locations, request types, notes, and lookup data
- Handles pagination and error responses
- Name-based metadata resolution — refer to priorities, request types, and custom fields by display name
dotnet add package LS.WHD.ClientNote: The package is not yet published to NuGet. Clone and reference the project directly for now.
using LS.WHD.Client;
// 1. Configure the client
var client = new WhdClient(new WhdClientOptions
{
BaseUrl = "https://helpdesk.example.com:8081",
AuthMode = LS.WHD.Client.Authentication.AuthMode.ApiKey,
ApiKey = "your-api-key-here"
});
// 2. List the first 25 open tickets
var tickets = await client.Tickets.ListAsync();
foreach (var t in tickets)
Console.WriteLine($"{t.Id}: {t.Subject}");
// 3. Clean up
client.Dispose();SolarWinds Web Help Desk supports two authentication methods.
Generate an API key in WHD → Admin → Accounts → API Key for a technician account.
var options = new WhdClientOptions
{
BaseUrl = "https://helpdesk.example.com:8081",
AuthMode = LS.WHD.Client.Authentication.AuthMode.ApiKey,
ApiKey = "YOUR_API_KEY"
};The key is appended as a query parameter (?apiKey=…) to every request, which is the convention documented in the WHD REST API guide.
var options = new WhdClientOptions
{
BaseUrl = "https://helpdesk.example.com:8081",
AuthMode = LS.WHD.Client.Authentication.AuthMode.BasicAuth,
Username = "admin",
Password = "p@ssword"
};| Property | Type | Default | Description |
|---|---|---|---|
BaseUrl |
string |
— (required) | Base URL of the WHD server (no trailing slash) |
AuthMode |
AuthMode |
ApiKey |
Authentication method |
ApiKey |
string? |
null |
API key (used with AuthMode.ApiKey) |
Username |
string? |
null |
Username (used with AuthMode.BasicAuth) |
Password |
string? |
null |
Password (used with AuthMode.BasicAuth) |
IgnoreSslErrors |
bool |
false |
Skip TLS certificate validation (dev only) |
Timeout |
TimeSpan |
30 s | Per-request HTTP timeout |
WHD REST APIs require numeric IDs for priorities, request types, and custom fields. This library lets you use display names instead by loading lookup metadata into an in-memory cache at startup.
Call InitializeMetadataCacheAsync() once after creating the client:
var client = new WhdClient(options);
await client.InitializeMetadataCacheAsync();Use NamedCreateTicketRequest and NamedUpdateTicketRequest with client.CreateTicketAsync
and client.UpdateTicketAsync:
// Create a ticket — use display names for priority, request type, and custom fields
var ticket = await client.CreateTicketAsync(new NamedCreateTicketRequest
{
Subject = "Printer is broken",
Detail = "Office printer on floor 2 prints blank pages.",
ClientId = 101,
PriorityType = "High", // resolved to its numeric ID automatically
RequestType = "IT Support", // resolved to its numeric ID automatically
CustomFields =
[
new NamedCustomFieldValue { Field = "Department", Value = "Finance" },
new NamedCustomFieldValue { Field = "Cost Center", Value = "CC-200" }
]
});
// Update a ticket — again use names; only non-null fields are sent
await client.UpdateTicketAsync(ticket!.Id, new NamedUpdateTicketRequest
{
StatusType = "Resolved",
PriorityType = "Low"
});Numeric ID strings are also accepted, so existing workflows that pass IDs still work:
// "2" is parsed as the numeric ID 2 — no cache lookup needed
await client.CreateTicketAsync(new NamedCreateTicketRequest
{
Subject = "Quick fix",
PriorityType = "2" // same as PriorityTypeId = 2 in the numeric API
});You can call the resolver directly if you need the numeric ID for another purpose:
long priorityId = client.Metadata.ResolvePriorityId("High");
long requestTypeId = client.Metadata.ResolveRequestTypeId("IT Support");
long fieldId = client.Metadata.ResolveCustomFieldId("Department");
long statusId = client.Metadata.ResolveStatusTypeId("Resolved");Call RefreshAsync() whenever metadata changes in WHD:
await client.Metadata.RefreshAsync();
// or equivalently:
await client.InitializeMetadataCacheAsync();| Situation | Exception |
|---|---|
| Cache not yet initialized | InvalidOperationException |
| Name not found in cache | WhdMetadataException |
| Name matches multiple entries | WhdMetadataException (ambiguous) |
using LS.WHD.Client.Exceptions;
try
{
await client.CreateTicketAsync(new NamedCreateTicketRequest
{
Subject = "Test",
PriorityType = "Typo Priority" // does not exist
});
}
catch (WhdMetadataException ex)
{
Console.WriteLine($"Could not resolve {ex.MetadataKind} '{ex.LookupValue}': {ex.Message}");
}If two entries share the same name in WHD, resolution by name throws an ambiguous error.
In that case pass the numeric ID string directly (e.g. "2") as a workaround.
// List tickets (paged)
var tickets = await client.Tickets.ListAsync(
new PaginationOptions { Limit = 50, Start = 0 });
// Get single ticket
var ticket = await client.Tickets.GetAsync(1234);
// Create ticket (numeric IDs — original API)
var created = await client.Tickets.CreateAsync(new CreateTicketRequest
{
Subject = "Printer is broken",
Detail = "Office printer on floor 2 prints blank pages.",
ClientId = 101,
RequestTypeId = 10,
PriorityTypeId = 2,
LocationId = 5
});
// Create ticket (display names — requires InitializeMetadataCacheAsync first)
var created = await client.CreateTicketAsync(new NamedCreateTicketRequest
{
Subject = "Printer is broken",
ClientId = 101,
PriorityType = "High",
RequestType = "IT Support"
});
// Update ticket (only provided fields are sent)
var updated = await client.Tickets.UpdateAsync(1234, new UpdateTicketRequest
{
StatusTypeId = 3, // e.g. "Resolved"
TechId = 7
});
// Update ticket (by name)
await client.UpdateTicketAsync(1234, new NamedUpdateTicketRequest
{
StatusType = "Resolved",
TechId = 7
});
// Delete ticket
await client.Tickets.DeleteAsync(1234);// List clients
var clients = await client.Clients.ListAsync(new PaginationOptions { Limit = 25 });
// Get a specific client
var helpDeskClient = await client.Clients.GetAsync(101);
// Search clients by name / username / email
var results = await client.Clients.SearchAsync("john.doe");// List assets
var assets = await client.Assets.ListAsync();
// Get a specific asset
var asset = await client.Assets.GetAsync(200);
// Search assets
var results = await client.Assets.SearchAsync("HP LaserJet");// List all locations
var locations = await client.Locations.ListAsync();
// Get a specific location
var loc = await client.Locations.GetAsync(5);// List all request types
var types = await client.RequestTypes.ListAsync();
// Get a specific request type
var rt = await client.RequestTypes.GetAsync(10);// List all notes for a ticket
var notes = await client.Notes.ListByTicketAsync(ticketId: 1234);
// Get a specific note
var note = await client.Notes.GetAsync(noteId: 55);
// Add a note to a ticket
var newNote = await client.Notes.CreateAsync(new CreateNoteRequest
{
TicketId = 1234,
NoteText = "Replaced the toner cartridge. Printer is operational.",
IsClientNote = true, // visible to the client
IsSolutionNote = true
});// Get all status types (e.g. Open, Closed, Resolved, …)
var statuses = await client.Lookups.GetStatusTypesAsync();
// Get all priority types (e.g. Critical, High, Medium, Low)
var priorities = await client.Lookups.GetPriorityTypesAsync();
// Get all technicians
var techs = await client.Lookups.GetTechsAsync();Most list endpoints accept a PaginationOptions parameter:
// Fetch page 3 of assets (0-based offset, 50 per page)
var page3 = await client.Assets.ListAsync(new PaginationOptions
{
Limit = 50,
Start = 100 // 50 * (pageNumber - 1)
});The WHD API uses limit and start query parameters (not page/perPage).
Keep requesting pages until the returned list is empty or shorter than Limit.
A failed HTTP response (any non-2xx status code) throws a WhdApiException:
using LS.WHD.Client.Exceptions;
try
{
var ticket = await client.Tickets.GetAsync(999999);
}
catch (WhdApiException ex) when (ex.StatusCode == 404)
{
Console.WriteLine("Ticket not found.");
}
catch (WhdApiException ex)
{
Console.WriteLine($"API error {ex.StatusCode}: {ex.Message}");
}The client communicates with the classic WHD REST endpoint:
{BaseUrl}/helpdesk/WebObjects/HELPDESK.woa/ra/{Resource}
This is the primary REST API path documented in the SolarWinds Web Help Desk REST API guide.
In development environments with self-signed certificates, set IgnoreSslErrors = true:
var options = new WhdClientOptions
{
BaseUrl = "https://internal-whd-server:8443",
ApiKey = "key",
IgnoreSslErrors = true // ⚠ do not use in production
};The SolarWinds Web Help Desk REST API has some documented limitations that are reflected in this client's design:
- Attachments: The
TicketNotesresource supports note creation; binary attachment upload is not officially documented in the REST API and is therefore not implemented. - Filtering: The API supports
limit,start, andsearchparameters. Complex server-side filtering (e.g. by date range, by status) is not uniformly documented and therefore not included; filter results client-side where needed. - Create/Update Clients and Assets: The WHD REST API exposes these objects as read-only in its public documentation; write operations for clients and assets are not implemented.
src/
LS.WHD.Client/
Authentication/ – AuthMode enum
Exceptions/ – WhdApiException, WhdMetadataException
Http/ – WhdHttpClient, WhdHttpClientFactory, QueryParameters
Metadata/ – IWhdMetadataCache, WhdMetadataCache (name→ID resolution)
Models/ – Request/response DTOs (incl. NamedCreateTicketRequest, NamedUpdateTicketRequest)
Services/ – ITicketService, IClientService, IAssetService, …
WhdClient.cs – Main entry point
WhdClientOptions.cs
LS.WHD.Client.Mcp/
Tools/ – MCP tool classes (TicketTools, ClientTools, AssetTools, …)
Program.cs – Host builder + STDIO transport
WhdConfiguration.cs – Reads env vars and builds WhdClientOptions
WhdClientService.cs – Singleton wrapper with lazy metadata initialization
tests/
LS.WHD.Client.Tests/
MetadataCacheTests.cs
SerializationTests.cs
WhdHttpClientTests.cs
QueryParametersTests.cs
PaginationOptionsTests.cs
WhdApiExceptionTests.cs
LS.WHD.Client.Mcp is a Model Context Protocol (MCP) server that wraps the
LS.WHD.Client library and exposes WHD operations as MCP tools. LLM clients (such as
Claude Desktop, VS Code Copilot Chat, and any MCP-compatible host) can connect to it and
call WHD operations directly from chat or agent workflows.
The Model Context Protocol is an open standard that lets AI assistants talk to external tools and data sources in a structured, type-safe way.
All settings are read from environment variables at startup.
| Variable | Required | Description |
|---|---|---|
WHD_BASE_URL |
✅ | Base URL of the WHD server, e.g. https://helpdesk.example.com:8081 |
WHD_AUTH_MODE |
ApiKey (default) or BasicAuth |
|
WHD_API_KEY |
API key when WHD_AUTH_MODE=ApiKey |
|
WHD_USERNAME |
Username when WHD_AUTH_MODE=BasicAuth |
|
WHD_PASSWORD |
Password when WHD_AUTH_MODE=BasicAuth |
|
WHD_IGNORE_SSL_ERRORS |
true to skip TLS validation (development only) |
From source:
cd src/LS.WHD.Client.Mcp
# ApiKey auth
WHD_BASE_URL=https://helpdesk.example.com:8081 \
WHD_API_KEY=your-api-key \
dotnet runPublished executable:
dotnet publish src/LS.WHD.Client.Mcp -c Release -o ./publish/mcp
WHD_BASE_URL=https://helpdesk.example.com:8081 \
WHD_API_KEY=your-api-key \
./publish/mcp/LS.WHD.Client.McpThe server communicates over STDIO (standard input / output), which is the conventional transport for locally-launched MCP servers.
Add an entry to your claude_desktop_config.json:
{
"mcpServers": {
"whd": {
"command": "dotnet",
"args": ["run", "--project", "/path/to/src/LS.WHD.Client.Mcp"],
"env": {
"WHD_BASE_URL": "https://helpdesk.example.com:8081",
"WHD_AUTH_MODE": "ApiKey",
"WHD_API_KEY": "your-api-key"
}
}
}
}Or, using a published binary instead of dotnet run:
{
"mcpServers": {
"whd": {
"command": "/path/to/publish/mcp/LS.WHD.Client.Mcp",
"env": {
"WHD_BASE_URL": "https://helpdesk.example.com:8081",
"WHD_AUTH_MODE": "ApiKey",
"WHD_API_KEY": "your-api-key"
}
}
}
}| Tool | Description |
|---|---|
list_tickets |
Returns a paged list of tickets |
get_ticket |
Returns a single ticket by ID |
create_ticket |
Creates a new ticket (priority / request type accept display names) |
update_ticket |
Updates an existing ticket (status / priority / request type accept display names) |
list_clients |
Returns a paged list of client (end-user) accounts |
search_clients |
Searches clients by username or email |
list_assets |
Returns a paged list of assets |
search_assets |
Searches assets by name or tag |
list_locations |
Returns all configured locations |
list_request_types |
Returns all configured request type categories |
add_ticket_note |
Adds a note / comment to a ticket |
list_ticket_notes |
Returns all notes for a ticket |
list_priorities |
Returns all priority types |
list_status_types |
Returns all status types |
list_technicians |
Returns a paged list of technician accounts |
LLM-friendly design: create_ticket and update_ticket accept priority, request type,
and status values as display names (e.g. "High", "IT Support", "Resolved") in
addition to numeric IDs. The server resolves names to IDs automatically using the metadata
cache, which is populated lazily on first use.
dotnet build LS.WHD.Client.slnx
dotnet test LS.WHD.Client.slnxThe solution contains both the client library (LS.WHD.Client) and the MCP server
(LS.WHD.Client.Mcp). Running the commands above builds and tests both.
MIT