Skip to content
Registry StackDocsv0.34.0

Registry Scheduling API

View as Markdown

Registry Scheduling serves one HTTP surface for the published catalogue, availability, holds, and appointments. Every call presents a bearer access token, and one of three authority profiles decides what that token must carry: a read scope for catalogue and owned-record reads, a separate explain scope for the diagnostic availability read, or a complete task grant for every commitment. The contract is unreleased and carries no frozen compatibility promise.

Open the Registry Scheduling API operations for every route, parameter, schema, and reachable problem code. This page holds the authority, revision, and recovery rules those operations assume.

Every request presents Authorization: Bearer <token> holding an RFC 9068 access token, typ: at+jwt, whose claims name the caller’s actor kind in registry_actor_kind as human, agent, or service. Liveness and readiness are the only routes that take no token.

The four caller-driven commitments carry an Idempotency-Key the caller chooses, bounded to 128 bytes: creating a hold, creating an appointment, rescheduling one, and cancelling one. A retry under the same key with the same request returns the stored first answer, and the same key with a different request is refused with idempotency.key-reused. Releasing a hold carries no caller key: the hold’s own identifier is the idempotency key, so a retried release replays the first answer without the caller holding anything back.

Observed revisions travel in request bodies, not in an If-Match header: an admission names the policyRevision it resolved the offering under, and a reschedule or cancel names the appointment revision it acted on. A stale revision is refused with policy.changed or revision.mismatch, and the caller reloads the catalogue and tries again. An offering the deployed policy does not publish is a separate answer, request.not-found: it does not exist, so no reload produces it, and the offering listing every caller holding the reads scope can read is where the published ones are named.

A refusal answers application/problem+json with type, title, status, detail, code, and traceId. The type is the code with each dot replaced by a slash under https://id.registrystack.org/problems/registry-scheduling/, and detail is the fixed sentence that belongs to the code. No field repeats a submitted value. Every response, refusals included, carries Cache-Control: no-store and propagates or mints a traceparent value.

Availability pages list a slot only while it holds a free unit: a slot whose members are all committed, held, or closed leaves the page rather than appearing with zero free. A caller watching a slot disappear therefore reads the same fact as one reading free: 0.

The three profiles are disjoint. A token carrying a product scope does not thereby gain a commitment path, and a task grant does not thereby gain a catalogue read.

ProfileRoutesThe token must carry
Catalogue read/v1/scheduling, /v1/services, /v1/offerings, /v1/resources, /v1/locations, /v1/availability, one appointment read, its historyThe configured read scope, scheduling-read by default, in registry_scopes
Explain/v1/availability/explainThe separately configured explain scope, scheduling-explain by default, which is never the read scope
CommitmentHold create and release, appointment create, reschedule, cancelA complete task grant; no product scope

The explain path is separately authorized because it can name the member-level cause resource.unavailable. The public availability path answers the same state as capacity.exhausted and names no member.

A commitment carries no product scope. Its authority is a task grant inside the access token: the six registry_grant_* members that make a grant detectable, plus registry_purpose and registry_approver, bound to the verified client and to this deployment’s audience before the caller reaches the service. An incomplete grant set is refused as a credentials problem (authentication.refused), never honored partially.

The grant’s bounds carry one permission per location, and a permission names an offering’s service, an offering’s location, and the actions it allows:

  • hold.create and hold.release
  • appointment.create, appointment.reschedule, and appointment.cancel

A permission matches an offering only when its service and location both equal the offering’s own and the action is listed. The match is exact: a broader service or location never covers a narrower one, and the grant’s client must equal the client the deployment verified.

The grant’s bounds are matched once, at the service, before the capacity transaction opens. A grant that does not cover the request answers operation.not-authorized and nothing is written. Inside the transaction, immediately before the claim commits, exactly one fact is read again: the grant’s own expiry, against the clock that commitment is decided under. A grant that expired between the door and the commit therefore cannot take capacity, and the attempt answers operation.not-authorized like any other authority refusal.

The bounds themselves are not read again at that point, and this milestone has no revocation check, so a grant narrowed or withdrawn at the issuer after its token was minted keeps the authority its token carries until the token or the grant deadline passes. Keep grant deadlines short. Re-checking the full bounds inside the capacity transaction is a recorded deferral, not a promise this milestone keeps.

The audit journal records authorization.allowed or authorization.refused as an audit reason, never as a problem code, for the commitment and for a permission the service refuses before any commitment is reached.

Every Scheduling problem response carries one code from this closed catalogue, always answered with the HTTP status listed beside it.

Two codes are reserved: eligibility.unavailable and hook.unavailable are defined, titled, and status-pinned for future synchronous decision paths, and no path in this milestone answers them yet. Scheduling hooks are asynchronous observers, so their delivery failures never become caller problem responses. The authored policy accepts only after URL handlers for appointment.confirmed, appointment.rescheduled, and appointment.cancelled. Confirmed and rescheduled events may project the appointment identifier, revision, offering, start, end, state, and policy revision. Cancelled events may project only the appointment identifier, revision, and state. Conditions, principals, local handlers, and observer proposals are refused.

CodeStatusWhen
authentication.refused401The bearer credential is missing, invalid, or expired. Sign in again.
booking.duplicate-active409An active booking already holds this party's duplicate key. Cancel it, or wait until it has passed, before booking again.
cancellation.cutoff-passed409The cancellation cutoff for this appointment has passed.
capability.unmatched422No backing member carries every capability the offering requires.
capacity.exhausted409The supply is fully committed for the requested interval. Choose another time.
cursor.expired410A page cursor has expired. Start again without a cursor and deduplicate entries by id.
cursor.invalid400A page cursor is malformed or was issued for a different request.
eligibility.unavailable503A required eligibility check could not run. Reserved: no milestone path answers it yet.
hold.expired410The hold expired before confirmation. Its capacity is bookable again; start a new request.
hold.released409The claim is not an active hold. It may already be confirmed or released.
hook.unavailable503A required lifecycle hook could not run, so the request was refused rather than half-applied. Reserved: no milestone path answers it yet.
horizon.outside422The requested start is earlier than the lead time allows or further ahead than the horizon allows.
idempotency.expired410The stored response for the idempotency key has expired. Reconcile the original operation before choosing a new key.
idempotency.key-reused409An idempotency key was presented for a different request.
location.closed409A closure covers the requested start. Choose a start outside the closure.
operation.not-authorized403The caller's task grant does not name this operation, or the deployment's client does not match the grant's client.
party.capacity-inadequate422The party is larger than the offering can ever serve, or its size falls outside the published units.
policy.changed412The policy revision changed before the request committed. Reload the catalogue and try again with the current revision.
precondition.failed412Reserved: no runtime path answers it. A stale revision answers policy.changed or revision.mismatch instead.
precondition.required428The mutation requires the revision the caller loaded, or the duplicate key the offering keys on.
prerequisite.missing422The party is missing a prerequisite the offering requires.
profile.not-authorized403The caller holds no profile that authorizes this request, for example a read scope check on a mutation route.
request.body-too-large413The request body exceeds the accepted size.
request.invalid400The request could not be read as a Scheduling request.
request.method-not-allowed405The route exists but not for this method.
request.not-found404The requested route does not exist, or the deployed policy publishes no offering by that id. The offering listing names the ones it does.
request.unprocessable422The request body could not be processed.
request.unsupported-media-type415The request body is not JSON.
resource.unavailable409Every capable member is unavailable for this interval. The separately authorized explain path reports this code; the public path answers capacity.exhausted.
revision.mismatch412The window revision changed before the request committed. Reload the catalogue and try again.
schedule.unpublished422No published schedule serves that start. Choose a start on the published grid.
service.unavailable503Scheduling storage is unavailable. Retry after the service recovers.

The served routes group into these families. A family name grants nothing on its own; authority is checked per route.

FamilyBase pathsAuthority profile
Service description/v1/schedulingCatalogue read
Catalogue/v1/services, /v1/offerings, /v1/resources, /v1/locationsCatalogue read
Availability/v1/availability, /v1/availability/explainCatalogue read; Explain
Holds/v1/holds, /v1/holds/{holdId}Commitment
Appointments/v1/appointments, /v1/appointments/{appointmentId}, its reschedule, cancel, and historyCommitment for the mutations; Catalogue read for the reads
Liveness and readiness/healthz, /readyzAny caller, without a token

The implementation is the source of truth, and the generated document is the published form of it. The product generator builds the OpenAPI document from the Rust-owned route inventory, authority profiles, wire schemas, and the exported problem catalogue, and the product check regenerates the document and refuses byte drift. A running Scheduling process serves the routes in that document, and does not serve the document itself.

The problem code table is generated from maintained data whose codes and statuses match the generated document. The route families table and the header rules are maintained by hand against the cited source.