Using the SDK
Creating a client
DimeSchedulerClient is the single entry point. It needs an API keyAPI keyA credential that authenticates calls to the Dime.Scheduler API. The preferred way to integrate over the deprecated JWT flow. and, optionally, an environment:
use dimescheduler::{DimeSchedulerClient, Environment};
let client = DimeSchedulerClient::new("MY_API_KEY"); // production
let client = DimeSchedulerClient::with_environment("MY_API_KEY", Environment::Sandbox);
let client = DimeSchedulerClient::from_env()?; // reads DIMESCHEDULER_API_KEY
Environment has two variants: Production (the default) and Sandbox. Each one maps to the base URL of that environment, which you can read back from client.base_url(). from_env returns Error::MissingApiKey when the DIMESCHEDULER_API_KEY variable is unset or empty, so a misconfigured deployment fails at startup rather than on the first request.
The client wraps a single reqwest::Client and is Send + Sync. Instantiate it once at startup and share it across tasks behind an Arc. There is no close(); dropping the client releases the connection pool.
The crate exposes no configuration beyond the key and the environment: the per-request timeout (60 seconds), the User-Agent (dimescheduler-rust/<version>) and the X-API-KEY header are fixed. If you need a proxy, set it through the environment variables reqwest honors (HTTPS_PROXY and friends).
Domain-grouped accessors
Every entity is a public field on the client. The naming mirrors the .NET, JavaScript and Python SDKs 1:1, with idiomatic snake_case:
// CRUD: create / create_many / update / update_many / delete / delete_many / get_all
client.categories // /category
client.resources // /resource
client.filter_groups // /filterGroup
client.filter_values // /filterValue
client.tags // /tag
client.project_members // /projectMember
client.task_dependencies // /taskDependency
// CRUD plus a purpose-built read
client.appointments // /appointment get(start, end, &resources), get_by_id(id)
client.jobs // /job get(job_no)
client.tasks // /task list(&TaskListOptions)
client.time_entries // /timeEntry get(start, end)
client.time_sessions // /timeSession get(start, end)
client.resource_capacities // /resourceCapacity get(start, end)
client.notifications // /notification get(page, limit, &NotificationListOptions)
// create / delete only
client.task_assignments // /taskAssignment
client.task_budget_entries // /taskBudgetEntry
// read-only: get_all only
client.appointment_dependencies, client.appointment_fields, client.calendars, client.resource_types
// their own shape
client.messages, client.geocoding, client.optimization, client.recommendation, client.imports,
client.connectors, client.recurring_appointments, client.users
The specialized accessors dereference to their underlying CrudApi, so client.jobs.create(&job) works exactly like client.categories.create(&category). task_assignments and task_budget_entries only expose create and delete (plus the _many variants where the API accepts batches): the API has no update or list for them. The read-only accessors expose get_all and nothing else.
CRUD-shaped entities take a reference to an entities::* value and return the API's response envelope. get_all returns the matching types::*Dto list:
use dimescheduler::entities::Category;
client.categories.create(&category).await?;
client.categories.create_many(&[category_a, category_b]).await?;
client.categories.update(&category).await?;
client.categories.delete(&category).await?;
let categories: Vec<dimescheduler::types::CategoryDto> = client.categories.get_all().await?;
Endpoints with their own shape get purpose-built methods:
use dimescheduler::{NotificationListOptions, TaskListOptions};
client.appointments.get("2026-05-01T00:00:00Z", "2026-05-31T23:59:59Z", &["R1", "R2"]).await?;
client.appointments.get_by_id("8f3c0b1e-...").await?;
client.jobs.get("CASE_123").await?;
client.tasks.list(&TaskListOptions { is_open: Some(true), page: Some(1), page_size: Some(50) }).await?;
client.time_entries.get("2026-05-01", "2026-05-31").await?;
client.notifications.get(1, 50, &NotificationListOptions::default()).await?;
client.geocoding.geocode_text("221B Baker Street", "GB").await?;
client.optimization.field_service(&request).await?;
client.messages.send(&message).await?;
Dates are plain Strings in the API's own format, there is no date type to convert to or from: RFC 3339 timestamps for appointments and resource capacities, YYYY-MM-DD for time entries and time sessions. GUIDs are strings too.
For the full list of accessors, see the API reference, the crate docs on docs.rs or the client source.
Generated types
The request and response types are generated from the OpenAPI specification and live in three modules:
dimescheduler::entitiesholds the import bodies you send:Category,Job,Task,Resource, ...dimescheduler::typesholds the DTOs you read back, the request types for the specialized endpoints, and the enums such asSeverity.dimescheduler::modelsholds the server's domain read-models that appear nested inside DTOs.
Every struct derives Default, so the idiomatic way to build one is to set the fields you care about and fill the rest with ..Default::default(). Required fields are plain String or f64; optional ones are Option<T> and are omitted from the JSON when None:
use dimescheduler::entities::Job;
let job = Job {
source_app: "CRM".into(),
source_type: "SERVICECASE".into(),
job_no: "CASE_123".into(),
short_description: "Repair HVAC in Berlin office".into(),
..Default::default()
};
Errors and retries
Every method returns dimescheduler::Result<T>, an alias for std::result::Result<T, dimescheduler::Error>. The error is a three-variant enum:
| Variant | When |
|---|---|
Error::Api { status, body } | The API answered with a non-2xx status. body is already the human-readable message when the API sent one of its known error shapes, otherwise the raw response body. |
Error::Http(reqwest::Error) | The request never got a response: DNS, TCP, TLS, or a timeout. |
Error::MissingApiKey | from_env() found no DIMESCHEDULER_API_KEY. |
Match on the variant to react:
use dimescheduler::Error;
match client.categories.create(&category).await {
Ok(_) => {}
Err(Error::Api { status: 400 | 422, body }) => eprintln!("rejected payload: {body}"),
Err(Error::Api { status, body }) => eprintln!("API error {status}: {body}"),
Err(Error::Http(e)) => eprintln!("transport failure: {e}"),
Err(Error::MissingApiKey) => unreachable!("the key was passed explicitly"),
}
Unlike the other Dime.Scheduler SDKs, the Rust crate does not retry. A 429 or a 5xx is returned to you as Error::Api, and transport failures as Error::Http. If your workload needs resilience, retry at the call site. A minimal exponential backoff, using Tokio's time feature:
use dimescheduler::Error;
use std::time::Duration;
let mut attempt = 0u32;
loop {
match client.categories.create(&category).await {
Ok(_) => break,
Err(Error::Api { status: 429 | 500 | 502 | 503 | 504, .. }) if attempt < 5 => {
attempt += 1;
tokio::time::sleep(Duration::from_millis(500 * 2u64.pow(attempt))).await;
}
Err(e) => return Err(e),
}
}
Async model
Every method is async and must be awaited inside a runtime. The crate depends on reqwest and is runtime-agnostic in principle, but reqwest itself runs on Tokio, so a #[tokio::main] entry point (or an existing Tokio runtime in a server framework such as Axum or Actix) is what you want. Calls on the same client can run concurrently with tokio::join! or futures::future::join_all; the client is Send + Sync.