#Project Availability (Temporary Closures and Duty Cover)

7 min read

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 receives access_mode: "editable" and editable: 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-enumerating 404 not_found responses on both GET and PUT.
  • Missing or invalid authentication returns 401. A customer blocked by its own tenant IP allowlist receives the platform-wide 403 ip_allowlist_restricted before any project lookup (see Authentication). Errors raised by shared authentication before the route runs carry the platform error/status_code body rather than code/message: 401, a 404 when the principal has no active supported assignment or a partner is hidden by its own tenant IP policy, and a 503 when 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 return 422 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.

Bash
curl -s https://api.example.test/v1/projects/example-project/availability \
  -H "Authorization: Bearer <session-jwt>"
JSON
{
  "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
}
FieldDescription
project_id, timezoneProject 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, editableeditable/true for all five in-scope roster roles. Staff, operators and API-key callers cannot read the roster.
versionOpaque optimistic-concurrency token. Send it back as expected_version. availability:0 means nothing has been saved.
saved, last_changed_atWhether a block has been saved, and when.
availabilityThe latest saved block, or empty lists when nothing has been saved. Entries that have already ended are kept as saved.
publish_statuspublished 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_atWhen the installed configuration was observed; null with runtime_status_unavailable.
choices.availablefalse 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_rolesProvisioned 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.destinationsDestination 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_daysRelease 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).

Bash
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):

FieldTypeDescription
expected_versionstringThe 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):

  • start and end are YYYY-MM-DDTHH:MM:SS±HH:MM, and the offset must be the project timezone's UTC offset at that instant (see timezone above 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; end must be after start, 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.duty without echoing it. Every destination must 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_failed at availability.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 id up to 64, start/end up to 32, destination up to 128 and expected_version up to 64 characters.

The success response is the same shape as GET, with the new version and publish_status usually pending_publish.

Errors:

StatusCodeMeaning
401Missing or invalid authentication.
404not_foundUnsupported roster role/auth type, missing active target/assignment, empty or unlisted customer project scope, out-of-scope target or unsupported query override.
409availability_version_conflictexpected_version is stale; reload and retry.
409availability_choices_unavailableThe request contains duty roles but the installed configuration has not been observed yet. Closures-only saves still succeed.
409project_timezone_unavailableNeither the installed configuration nor the project has a valid timezone.
422validation_failedMalformed JSON, wrong shape, or a rule above.
503availability_unavailableTemporarily unavailable.