QuartzRestApi
A self-hosted REST API library for Quartz.NET. This package is multi-targeted and provides native support for both modern and legacy frameworks:
- .NET 10 built on ASP.NET Core / Kestrel
- .NET Framework 4.6.2 self-hosted via OWIN / Web API 2
QuartzRestApi wraps your existing IScheduler instance behind an HTTP endpoint so other processes — on the same machine or across a network — can manage jobs and triggers without needing a direct Quartz.NET reference.
Architecture
graph TD
subgraph App["Your Application"]
HOST["SchedulerHost\nWraps Kestrel (.NET 10)\nor OWIN (.NET 4.6.2)"]
CLIENT["SchedulerConnector\nHttpClient-based C# client\nfor the REST API"]
end
subgraph API["REST API HTTP Pipeline"]
AUTH["ApiKeyMiddleware / OWIN Filter\nOptional API key\n+ profile-based access control"]
CTRL["SchedulerController\n/Scheduler/..."]
end
subgraph Quartz["Quartz.NET"]
SCHED["IScheduler\n- Jobs\n- Triggers\n- Calendars"]
end
HOST -- hosts --> AUTH
AUTH -- authorized --> CTRL
CLIENT -- "HTTP - GET / POST / DELETE\nX-Api-Key header" --> AUTH
CTRL -- delegates to --> SCHED
Key components:
| Component | Description |
|---|---|
SchedulerHost |
Starts the HTTP server (.NET 10 Kestrel or .NET 4.6.2 OWIN) that exposes the Quartz.NET scheduler as a REST API |
ApiKeyMiddleware |
Optional middleware that validates the X-Api-Key header and enforces per-profile route restrictions |
ApiKeyProfile |
A named profile that pairs an API key with an optional whitelist of allowed endpoints |
SchedulerController |
API controller with all scheduler endpoints under the /Scheduler/ route prefix |
SchedulerConnector |
A typed C# HTTP client that wraps all REST calls so you do not need to craft HTTP requests manually |
| Wrappers | DTO classes (de)serialised to/from JSON for jobs, triggers, calendars, etc. |
Requirements
- .NET 10 OR .NET Framework 4.6.2
- A configured Quartz.NET
ISchedulerinstance
How to host Quartz.NET via the REST API
The hosting setup code remains identical regardless of whether you are targeting .NET 10 or .NET Framework 4.6.2. Under the hood, the library automatically boots Kestrel (.NET 10) or OWIN (.NET 4.6.2).
// No authentication -- all endpoints publicly accessible
var host = new SchedulerHost("http://localhost:44344", scheduler, logger);
await host.Start();
scheduler-- yourISchedulerinstance from Quartz.NETlogger-- anyMicrosoft.Extensions.Logging.ILogger(ornullto disable logging)
Both Start and Stop return a Task and should be awaited. An optional CancellationToken can be passed to either method.
To stop the host:
await host.Stop();
How to connect to the host
var connector = new SchedulerConnector("http://localhost:44344");
Use the SchedulerConnector methods to call the API from C# without dealing with raw HTTP. The client library is shared and compatible across both platforms.
Security (API key authentication)
Authentication is opt-in. When no key or profiles are configured every request passes through without any check.
Single API key (full access)
// Host
var host = new SchedulerHost("http://localhost:44344", scheduler, logger, apiKey: "my-secret-key");
// Client
var connector = new SchedulerConnector("http://localhost:44344", apiKey: "my-secret-key");
The key is sent and validated via the X-Api-Key HTTP header.
Multiple API keys with profiles
Each ApiKeyProfile pairs an API key with a set of strongly-typed boolean properties -- one per endpoint. Use ApiKeyProfile.AllowAll to start with full access and selectively disable endpoints, or use ApiKeyProfile.DenyAll to start with no access and selectively enable only what is needed.
// Admin profile -- full access to all endpoints
var admin = ApiKeyProfile.AllowAll("Admin", "key-admin-abc123");
// Read-only profile -- start with nothing allowed, then enable only query endpoints
var readOnly = ApiKeyProfile.DenyAll("ReadOnly", "key-readonly-xyz");
readOnly.SchedulerName = true;
readOnly.SchedulerInstanceId = true;
readOnly.GetMetaData = true;
readOnly.GetJobGroupNames = true;
readOnly.GetTriggerGroupNames = true;
readOnly.GetJobKeys = true;
readOnly.GetJobDetail = true;
readOnly.GetTrigger = true;
readOnly.GetTriggerState = true;
readOnly.GetCurrentlyExecutingJobs = true;
// Monitoring profile -- start with full access, then remove mutating endpoints
var monitoring = ApiKeyProfile.AllowAll("Monitoring", "key-mon-def456");
monitoring.Start = false;
monitoring.StartDelayed = false;
monitoring.Standby = false;
monitoring.Shutdown = false;
monitoring.Clear = false;
monitoring.ScheduleJobWithJobDetailAndTrigger = false;
monitoring.ScheduleJobIdentifiedWithTrigger = false;
monitoring.ScheduleJobWithJobDetailAndTriggers = false;
monitoring.ScheduleJobs = false;
monitoring.RescheduleJob = false;
monitoring.UnscheduleJob = false;
monitoring.UnscheduleJobs = false;
monitoring.AddJob = false;
monitoring.DeleteJob = false;
monitoring.DeleteJobs = false;
monitoring.TriggerJobWithJobkey = false;
monitoring.TriggerJobWithDataMap = false;
monitoring.PauseJob = false;
monitoring.PauseJobs = false;
monitoring.PauseTrigger = false;
monitoring.PauseTriggers = false;
monitoring.PauseAllTriggers = false;
monitoring.ResumeJob = false;
monitoring.ResumeJobs = false;
monitoring.ResumeTrigger = false;
monitoring.ResumeTriggers = false;
monitoring.ResumeAllTriggers = false;
monitoring.AddCalendar = false;
monitoring.DeleteCalendar = false;
monitoring.ResetTriggerFromErrorState = false;
var host = new SchedulerHost("http://localhost:44344", scheduler, logger, profiles: [admin, readOnly, monitoring]);
Persisting profiles
Profiles can be saved to and loaded from JSON:
// Save
File.WriteAllText("readOnly.json", readOnly.ToJson());
// Load
var loaded = ApiKeyProfile.FromJson(File.ReadAllText("readOnly.json"));
HTTP responses
| Situation | Status |
|---|---|
| No profiles configured | Request passes through (no auth) |
X-Api-Key header missing |
401 Unauthorized |
| Key not recognised | 401 Unauthorized |
| Key valid, route allowed | Request passes through |
| Key valid, route not in whitelist | 403 Forbidden |
Interactive API documentation (Scalar / OpenAPI)
⚠️ Additional package required
OpenAPI and Scalar support are opt-in features that require an additional NuGet package:dotnet add package QuartzRestApi.OpenApiWithout this package, the
/openapi/v1.jsonand/scalar/v1endpoints will not be available.
Enabling OpenAPI/Scalar (.NET 10 only)
After installing QuartzRestApi.OpenApi, configure the host with OpenAPI and Scalar support:
using QuartzRestApi.OpenApi;
var host = new SchedulerHost("http://localhost:44344", scheduler, logger);
// Enable OpenAPI + Scalar
await host.Start(
configureServices: services => services.AddQuartzOpenApi(),
configureApp: app => app.UseQuartzScalar()
);
Available endpoints
When OpenAPI/Scalar is enabled, the following endpoints become available:
| URL | Description |
|---|---|
/openapi/v1.json |
Raw OpenAPI 3 document |
/scalar/v1 |
Scalar interactive API reference |
Open http://localhost:44344/scalar/v1 in a browser to explore and test all endpoints without writing any code.
Alternative: Use the hosted documentation
If you don't want to install the OpenAPI package, you can use the pre-built interactive API reference on this documentation site:
REST API Reference
This hosted version works without any additional packages and provides the same Scalar UI experience.
Logging
QuartzRestApi uses the Microsoft.Extensions.Logging.ILogger interface. Any compatible logging library works (Serilog, NLog, etc.).
Log levels used:
Information-- standard results (booleans, names, DateTimeOffsets)Debug-- full JSON request/response bodiesWarning-- rejected requests (missing or invalid API key, forbidden route)
Error handling
All SchedulerConnector methods throw a SchedulerConnectorException when the host returns a non-success HTTP status code (4xx / 5xx). This makes it easy to distinguish transport or server-side errors from other exceptions in your application.
using QuartzRestApi.Exceptions;
try
{
var fireTime = await connector.ScheduleJob(trigger);
}
catch (SchedulerConnectorException ex)
{
// ex.Message contains the HTTP status code and the response body,
// e.g. "The scheduler host returned HTTP 500 (Internal Server Error). Body: ..."
logger.LogError(ex, "Failed to schedule job");
}
All public methods on SchedulerConnector document this exception via <exception cref="SchedulerConnectorException"> in their XML documentation, so it surfaces as a tooltip in Visual Studio IntelliSense.
Testing
The QuartzRestApi.Tests project contains a comprehensive integration test suite with 50+ tests covering all scheduler operations:
- Scheduler state and metadata queries
- Job and trigger lifecycle (schedule, reschedule, unschedule, pause, resume, delete)
- Calendar operations (add, retrieve, delete)
- Existence checks and group queries
- Error state recovery
- Multi-job/trigger batch operations
All tests run against a live in-memory SchedulerHost instance to ensure end-to-end correctness of the REST API.
Running tests
dotnet test QuartzRestApi.Tests/QuartzRestApi.Tests.csproj
Recent fixes (January 2026)
The test suite previously had JSON serialization issues that caused 22 out of 50 tests to fail. These have been resolved:
Double JSON serialization -- Controller methods returning pre-serialized JSON strings were being serialized again by ASP.NET Core, wrapping the JSON in extra quotes. Fixed by returning
ContentResultwithapplication/jsoncontent type instead ofstring.Incorrect deserialization logic --
SchedulerContext.FromJsonStringandSchedulerMetaData.FromJsonStringattempted to unwrap a JSON string layer that no longer existed after fix #1. Removed the unnecessaryDeserialize<string>step.Missing public constructor --
SchedulerContexthad aninternalparameterless constructor marked with[JsonConstructor], preventingSystem.Text.Jsonfrom deserializing it. Changed topublic.List-derived wrappers without parameterless constructors --
JobKeys,TriggerKeys, andTriggersinherit fromList<T>but lacked parameterless constructors required bySystem.Text.Json. Addedpublicparameterless constructors to all three classes.
All 50 tests now pass successfully.
License
QuartzRestApi is Copyright (C) 2022 - 2026 Magic-Sessions and is licensed under the MIT license.