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.

NuGet


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 IScheduler instance

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 -- your IScheduler instance from Quartz.NET
  • logger -- any Microsoft.Extensions.Logging.ILogger (or null to 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.OpenApi

Without this package, the /openapi/v1.json and /scalar/v1 endpoints 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 bodies
  • Warning -- 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:

  1. 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 ContentResult with application/json content type instead of string.

  2. Incorrect deserialization logic -- SchedulerContext.FromJsonString and SchedulerMetaData.FromJsonString attempted to unwrap a JSON string layer that no longer existed after fix #1. Removed the unnecessary Deserialize<string> step.

  3. Missing public constructor -- SchedulerContext had an internal parameterless constructor marked with [JsonConstructor], preventing System.Text.Json from deserializing it. Changed to public.

  4. List-derived wrappers without parameterless constructors -- JobKeys, TriggerKeys, and Triggers inherit from List<T> but lacked parameterless constructors required by System.Text.Json. Added public parameterless 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.