#Project Availability (Temporary Closures and Duty Cover)
GET and PUT /v1/projects/{project_id}/availability provide the first-party AiDial Portal contract for temporary closures and on-call cover (roster release 1, S-PS34 access policy).
- A closure closes the business additionally during
[start, end), on top of the weekly schedule. - A duty period sends a provisioned duty role (for example
on_call) to one destination during[start, end). Outside every period the role uses its engineer-configured fallback destination.
Saving does not change calls by itself. A saved block becomes active only after the next publish-mode project deployment sync installs it, after which the status reads published.
#Access
- Bearer authentication only. Valid API-key callers receive
404 not_found. - Read and write:
client_admin,client_manager,partner_user,partner_admin,aidial_admin. Every admitted reader receivesaccess_mode: "editable"andeditable: true. - Customer owners/managers require the exact path project in their trusted live project list and ownership by their assigned client. An empty list or an unlisted project in the same client is denied.
- Partners require an active exact principal/project/data-environment assignment. Its owning client, rather than the token's first assigned client or partner organization, owns the read/write. Target-client IP policy and project ownership are rechecked.
- Internal admins use their own active bearer and the server's exact active-project owner lookup, followed by target-client IP policy and ownership recheck. No customer impersonation, customer token, caller-selected client or browser selection token is required for direct API access.
client_staff,aidial_operator, other roles, unassigned partners and out-of-scope/inactive projects receive non-enumerating404 not_foundresponses on both GET and PUT.- Missing or invalid authentication returns
401. A customer blocked by its own tenant IP allowlist receives the platform-wide403 ip_allowlist_restrictedbefore any project lookup (see Authentication). Errors raised by shared authentication before the route runs carry the platformerror/status_codebody rather thancode/message:401, a404when the principal has no active supported assignment or a partner is hidden by its own tenant IP policy, and a503when the identity, assignment or IP-policy lookup is unavailable. - The scope is the verified owning client, exact path project and unchanged trusted bearer data environment. Reads, projections, version/conflict checks, locks, revisions and audit all use that same scope, with the actual actor identity/role retained.
- No query parameters are supported: tenant, client or environment overrides and unknown query parameters return
404 not_found. Unknown/forbidden body fields return422 validation_failed; they cannot redirect storage. - This exception applies only to operational roster entries. Weekly hours, duty-role definitions/fallbacks, destination lists/numbers, transfer/cascade configuration and other settings/configuration/publish permissions retain their existing policy. Saving does not grant publish, calling or deployment authority.
#GET /v1/projects/{project_id}/availability
Returns the latest saved block, its version, activation status, and the choices available for duty cover.
curl -s https://api.example.test/v1/projects/example-project/availability \
-H "Authorization: Bearer <session-jwt>"{
"project_id": "example-project",
"timezone": "Australia/Brisbane",
"access_mode": "editable",
"editable": true,
"version": "availability:2",
"saved": true,
"last_changed_at": "2026-09-19T01:00:00Z",
"publish_status": "pending_publish",
"runtime_observed_at": "2026-09-18T23:00:00Z",
"availability": {
"closures": [
{"id": "c1", "start": "2026-09-20T12:00:00+10:00", "end": "2026-09-20T14:00:00+10:00"}
],
"duty": {
"on_call": [
{"id": "d1", "start": "2026-09-21T08:00:00+10:00", "end": "2026-09-28T08:00:00+10:00", "destination": "roster_primary"}
]
}
},
"choices": {
"available": true,
"duty_roles": [{
"key": "on_call",
"fallback": "roster_fallback",
"destinations": [{"key": "roster_primary", "display_name": "Roster primary"}]
}],
"destinations": [
{"key": "roster_fallback", "display_name": "Roster fallback"},
{"key": "roster_primary", "display_name": "Roster primary"}
]
},
"max_entries": 256,
"max_horizon_days": 366
}| Field | Description |
|---|---|
project_id, timezone | Project identity and the IANA timezone used for validation: the installed configuration's project.timezone when it has been observed and is a valid zone, otherwise the project's own timezone; null when neither is usable. |
access_mode, editable | editable/true for all five in-scope roster roles. Staff, operators and API-key callers cannot read the roster. |
version | Opaque optimistic-concurrency token. Send it back as expected_version. availability:0 means nothing has been saved. |
saved, last_changed_at | Whether a block has been saved, and when. |
availability | The latest saved block, or empty lists when nothing has been saved. Entries that have already ended are kept as saved. |
publish_status | published when the entries that have not yet ended match the installed project configuration; pending_publish when they differ; runtime_status_unavailable when the installed configuration has not been observed yet. |
runtime_observed_at | When the installed configuration was observed; null with runtime_status_unavailable. |
choices.available | false until the installed configuration has been observed and confirmed by its latest runtime observation. Duty cover cannot be saved while it is false. |
choices.duty_roles | Provisioned duty role keys, fallback keys, and each role's required destinations array of allowed {key, display_name} choices. The On-call card shows only when this role list is non-empty. |
choices.destinations | Destination keys and display names of the installed configuration. Transfer groups are never choices, because duty cover needs one person. Phone numbers and other destination settings are never returned; a display name that looks like routing data (a phone number, extension, SIP or tel: URI, or email address), contains a {token}, or is longer than 160 characters is returned as null. |
max_entries, max_horizon_days | Release 1 limits (see validation). |
Errors: 401, 404 not_found, 503 availability_unavailable.
S-PS31 adds the engineer-owned optional transfer.duty_roles.<role>.destinations list. Omission permits all installed destinations; an explicit [] permits no duty periods for that role. Each list item must be an installed destination key. The engineer fallback must be configured independently and need not be in the customer-choice list. Every response expands the effective choices per role; the common choices.destinations list remains available for fallback labels. Both projection extraction and verified reads reject malformed lists, unknown keys and disallowed installed periods. The extracted-facts source hash includes the list; request-time reads use the confirmed projection, never full YAML.
#PUT /v1/projects/{project_id}/availability
Replaces the whole block. Send every closure and every duty role's periods; anything omitted is deleted. Both closures and duty are required (use [] and {} for none).
curl -s -X PUT https://api.example.test/v1/projects/example-project/availability \
-H "Authorization: Bearer <session-jwt>" \
-H "Content-Type: application/json" \
-d '{
"expected_version": "availability:2",
"availability": {
"closures": [{"id": "c1", "start": "2026-09-20T12:00:00+10:00", "end": "2026-09-20T14:00:00+10:00"}],
"duty": {}
}
}'Request fields (closed shapes; unknown fields are rejected):
| Field | Type | Description |
|---|---|---|
expected_version | string | The version from the last read. |
availability.closures[] | {id, start, end} | Closure entries. |
availability.duty.<role>[] | {id, start, end, destination} | Periods for one provisioned duty role. |
Validation (422 validation_failed with field_errors keyed by path, for example availability.duty.on_call[0].destination):
startandendareYYYY-MM-DDTHH:MM:SS±HH:MM, and the offset must be the project timezone's UTC offset at that instant (seetimezoneabove for which zone applies). This rejects wall-clock times that do not exist in the project timezone (for example 02:30 on a daylight-saving start day), in either spelling.- A repeated fall-back time takes the first occurrence. A value written as the second occurrence (for example Sydney
2027-04-04T02:30:00+10:00) is stored and returned as the first occurrence of the same wall time (2027-04-04T02:30:00+11:00); all the rules below use the normalised instants. - Intervals are half-open;
endmust be afterstart, and no later than 366 days after the save. - Periods within one role must not overlap. Adjacent periods (one ends exactly when the next starts) are allowed.
- Ids are 1-64 letters, digits,
-or_, unique across the whole block. - Every role key must be a provisioned duty role of at most 64 characters, never equal to a destination key; an overlong key is reported at
availability.dutywithout echoing it. Everydestinationmust be a configured destination key; a transfer group is not one, so a saved period naming a group is refused with this same error. - A period's destination must also be allowed for its role. A disallowed key returns
422 validation_failedatavailability.duty.<role>[<index>].destination, without saving. This applies to every saved period, including ended entries. Prepare validates the whole saved block against the staged configuration before dropping ended entries. - At most 256 entries in total. Before these rules run, the request shape allows at most 64 roles, 256 closures and 256 periods per role, with
idup to 64,start/endup to 32,destinationup to 128 andexpected_versionup to 64 characters.
The success response is the same shape as GET, with the new version and publish_status usually pending_publish.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 401 | Missing or invalid authentication. | |
| 404 | not_found | Unsupported roster role/auth type, missing active target/assignment, empty or unlisted customer project scope, out-of-scope target or unsupported query override. |
| 409 | availability_version_conflict | expected_version is stale; reload and retry. |
| 409 | availability_choices_unavailable | The request contains duty roles but the installed configuration has not been observed yet. Closures-only saves still succeed. |
| 409 | project_timezone_unavailable | Neither the installed configuration nor the project has a valid timezone. |
| 422 | validation_failed | Malformed JSON, wrong shape, or a rule above. |
| 503 | availability_unavailable | Temporarily unavailable. |